diff --git a/.gitmodules b/.gitmodules index 558769d5d..b6d4ff594 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,9 @@ [submodule "third_party/googletest"] path = third_party/googletest url = git@github.com:google/googletest.git +[submodule "third_party/continuable"] + path = third_party/continuable + url = git@github.com:Naios/continuable.git +[submodule "third_party/function2"] + path = third_party/function2 + url = https://github.com/Naios/function2.git diff --git a/docs/UserGuide/Changelog/Changelog.md b/docs/UserGuide/Changelog/Changelog.md index 406ce87af..4e942934d 100644 --- a/docs/UserGuide/Changelog/Changelog.md +++ b/docs/UserGuide/Changelog/Changelog.md @@ -16,13 +16,15 @@ * **fonts:** Provide fallback font if EuroScope is not installed ([#434](https://github.com/VATSIM-UK/uk-controller-plugin/issues/434)) ([ce434f3](https://github.com/VATSIM-UK/uk-controller-plugin/commit/ce434f363ab4e2e4747420ab1145ddf2533ac0d0)), closes [#433](https://github.com/VATSIM-UK/uk-controller-plugin/issues/433) ## [4.2.1](https://github.com/VATSIM-UK/uk-controller-plugin/compare/4.2.0...4.2.1) (2022-02-12) - - -### Bug Fixes - * **initialaltitude:** Dont allow initial altitude recycle on aircraft out of range ([#430](https://github.com/VATSIM-UK/uk-controller-plugin/issues/430)) ([cd3fd0e](https://github.com/VATSIM-UK/uk-controller-plugin/commit/cd3fd0e553626d256863377d1cbe30c6b521261a)) * **initialheading:** Dont allow initial heading recycle on aircraft out of range ([#431](https://github.com/VATSIM-UK/uk-controller-plugin/issues/431)) ([e4fd211](https://github.com/VATSIM-UK/uk-controller-plugin/commit/e4fd211bfde01b0829deaf58a962f1c043654faf)), closes [#429](https://github.com/VATSIM-UK/uk-controller-plugin/issues/429) +# [5.0.0-alpha.1](https://github.com/VATSIM-UK/uk-controller-plugin/compare/4.2.0...5.0.0-alpha.1) (2022-02-12) + +### Features + +* **holds**: Hold timer by entry time rather than assigned time +* # [4.2.0](https://github.com/VATSIM-UK/uk-controller-plugin/compare/4.1.1...4.2.0) (2022-02-10) @@ -66,23 +68,8 @@ to the service provision options, rather than a filter for which airfields to us * chore(release): 4.0.0-beta.1 [skip ci] -# [4.0.0-beta.1](https://github.com/VATSIM-UK/uk-controller-plugin/compare/3.13.1...4.0.0-beta.1) (2022-01-05) - -### Features -* The old lists are replaced by a single list. New ASR settings -are also used for this list. - -* chore(release): 4.0.0-beta.2 [skip ci] - -# [4.0.0-beta.2](https://github.com/VATSIM-UK/uk-controller-plugin/compare/4.0.0-beta.1...4.0.0-beta.2) (2022-01-05) - -### Features - -* **departure:** Combine pending prenotes and departure releases lists into one ([91c0614](https://github.com/VATSIM-UK/uk-controller-plugin/commit/91c0614982e061a29e18db23e2d9c70c86449fcb)) - # [4.0.0-beta.7](https://github.com/VATSIM-UK/uk-controller-plugin/compare/4.0.0-beta.6...4.0.0-beta.7) (2022-01-23) - ### Features * **holds:** Hold Manager UX Improvements ([#404](https://github.com/VATSIM-UK/uk-controller-plugin/issues/404)) ([c0c999b](https://github.com/VATSIM-UK/uk-controller-plugin/commit/c0c999bcc9bd2a4014c1afcbb0ad3c0d1c6ec113)) diff --git a/src/loader/dllmain.cpp b/src/loader/dllmain.cpp index ba1940be0..8a9af64d0 100644 --- a/src/loader/dllmain.cpp +++ b/src/loader/dllmain.cpp @@ -4,6 +4,7 @@ #include "windows/WinApiInterface.h" #include "windows/WinApiBootstrap.h" #include "api/ApiBootstrap.h" +#include "api/ApiFactory.h" #include "setting/SettingRepositoryFactory.h" #include "curl/CurlApi.h" #include "data/PluginDataLocations.h" @@ -37,9 +38,9 @@ UKCP_LOADER_API void EuroScopePlugInInit(EuroScopePlugIn::CPlugIn** ppPlugInInst // Bootstrap the API, download the updater if we don't have it already and run it UKControllerPlugin::Curl::CurlApi curl; std::unique_ptr settings = - UKControllerPlugin::Setting::SettingRepositoryFactory::Create(*windows); - std::unique_ptr api = - UKControllerPlugin::Api::Bootstrap(*settings, curl); + UKControllerPlugin::Setting::SettingRepositoryFactory::Create(); + auto factory = UKControllerPluginUtils::Api::Bootstrap(*settings, *windows); + auto api = UKControllerPluginUtils::Api::BootstrapLegacy(*factory, curl); LogInfo("Loader build version " + std::string(UKControllerPlugin::Plugin::PluginVersion::version)); diff --git a/src/plugin/CMakeLists.txt b/src/plugin/CMakeLists.txt index c10cbb70c..787c0fa36 100644 --- a/src/plugin/CMakeLists.txt +++ b/src/plugin/CMakeLists.txt @@ -36,7 +36,9 @@ source_group("src\\airfield" FILES ${src__airfield}) set(src__api "api/ApiConfigurationMenuItem.cpp" "api/ApiConfigurationMenuItem.h" -) + api/FirstTimeApiConfigLoader.cpp api/FirstTimeApiConfigLoader.h + api/BootstrapApi.cpp api/BootstrapApi.h + api/FirstTimeApiAuthorisationChecker.cpp api/FirstTimeApiAuthorisationChecker.h) source_group("src\\api" FILES ${src__api}) set(src__bootstrap @@ -339,7 +341,7 @@ set(src__hold "hold/PublishedHoldCollection.h" "hold/PublishedHoldCollectionFactory.cpp" "hold/PublishedHoldCollectionFactory.h" - hold/AssignHoldCommand.cpp hold/AssignHoldCommand.h hold/AddToHoldCallsignProvider.cpp hold/AddToHoldCallsignProvider.h) + hold/AssignHoldCommand.cpp hold/AssignHoldCommand.h hold/AddToHoldCallsignProvider.cpp hold/AddToHoldCallsignProvider.h hold/ProximityHold.h hold/CompareProximityHolds.cpp hold/CompareProximityHolds.h hold/AircraftEnteredHoldingAreaEventHandler.cpp hold/AircraftEnteredHoldingAreaEventHandler.h hold/AircraftExitedHoldingAreaEventHandler.cpp hold/AircraftExitedHoldingAreaEventHandler.h) source_group("src\\hold" FILES ${src__hold}) set(src__initialaltitude diff --git a/src/plugin/api/ApiConfigurationMenuItem.cpp b/src/plugin/api/ApiConfigurationMenuItem.cpp index b2a45a86d..7627f8994 100644 --- a/src/plugin/api/ApiConfigurationMenuItem.cpp +++ b/src/plugin/api/ApiConfigurationMenuItem.cpp @@ -1,18 +1,54 @@ #include "ApiConfigurationMenuItem.h" -#include "api/LocateApiSettings.h" +#include "api/ApiRequestException.h" +#include "api/ApiRequestFactory.h" +#include "api/Response.h" +#include "api/ApiSettingsProviderInterface.h" using UKControllerPlugin::Plugin::PopupMenuItem; using UKControllerPlugin::Windows::WinApiInterface; +using UKControllerPluginUtils::Api::ApiRequestException; +using UKControllerPluginUtils::Api::ApiSettingsProviderInterface; +using UKControllerPluginUtils::Api::Response; namespace UKControllerPlugin::Api { - ApiConfigurationMenuItem::ApiConfigurationMenuItem(WinApiInterface& winApi, int menuCallbackId) - : menuCallbackId(menuCallbackId), winApi(winApi) + ApiConfigurationMenuItem::ApiConfigurationMenuItem( + UKControllerPluginUtils::Api::ApiSettingsProviderInterface& provider, + Windows::WinApiInterface& windows, + int menuCallbackId) + : provider(provider), windows(windows), menuCallbackId(menuCallbackId) { } void ApiConfigurationMenuItem::Configure(int functionId, std::string subject, RECT area) { - UserRequestedKeyUpdate(this->winApi); + if (!provider.Reload()) { + return; + }; + + ApiRequest() + .Get("authorise") + .Then([this]() { + LogInfo("Api configuration updated successfully"); + windows.OpenMessageBox( + L"API configuration has been replaced sucessfully", + L"Configuration Updated", + MB_OK | MB_ICONINFORMATION); + }) + .Catch([this](const ApiRequestException& exception) { + if (UKControllerPluginUtils::Http::IsAuthenticationError(exception.StatusCode())) { + windows.OpenMessageBox( + L"API authentication failed. Please re-download your credentails from the VATSIM UK website " + "and try again. If this problem persists, please contact the Web Services Department. Some " + "functionality such as stand and squawk allocations may not work as expected.", + L"UKCP API Config Invalid", + MB_OK | MB_ICONWARNING); + } else if (UKControllerPluginUtils::Http::IsServerError(exception.StatusCode())) + windows.OpenMessageBox( + L"Unable to perform API config check as the API responded with an error. Please try again " + L"later. Some functionality such as stand and squawk allocations may not work as expected.", + L"Server Error", + MB_OK | MB_ICONERROR); + }); } /* diff --git a/src/plugin/api/ApiConfigurationMenuItem.h b/src/plugin/api/ApiConfigurationMenuItem.h index 1eb739cc4..4aa5af501 100644 --- a/src/plugin/api/ApiConfigurationMenuItem.h +++ b/src/plugin/api/ApiConfigurationMenuItem.h @@ -2,6 +2,14 @@ #include "radarscreen/ConfigurableDisplayInterface.h" #include "windows/WinApiInterface.h" +namespace UKControllerPluginUtils::Api { + class ApiSettingsProviderInterface; +} // namespace UKControllerPluginUtils::Api + +namespace UKControllerPlugin::Windows { + class WinApiInterface; +} // namespace UKControllerPlugin::Windows + namespace UKControllerPlugin::Api { /* @@ -12,7 +20,10 @@ namespace UKControllerPlugin::Api { class ApiConfigurationMenuItem : public UKControllerPlugin::RadarScreen::ConfigurableDisplayInterface { public: - ApiConfigurationMenuItem(UKControllerPlugin::Windows::WinApiInterface& winApi, int menuCallbackId); + ApiConfigurationMenuItem( + UKControllerPluginUtils::Api::ApiSettingsProviderInterface& provider, + Windows::WinApiInterface& windows, + int menuCallbackId); // Inherited via ConfigurableDisplayInterface void Configure(int functionId, std::string subject, RECT area) override; @@ -22,10 +33,13 @@ namespace UKControllerPlugin::Api { // The item description const std::string itemDescription = "Replace Personal API Configuration"; + // Api credential provider + UKControllerPluginUtils::Api::ApiSettingsProviderInterface& provider; + + // Windows API for the dialogs + Windows::WinApiInterface& windows; + // The id of the callback function for when the menu item is clicked const int menuCallbackId; - - // The windows API - UKControllerPlugin::Windows::WinApiInterface& winApi; }; } // namespace UKControllerPlugin::Api diff --git a/src/plugin/api/BootstrapApi.cpp b/src/plugin/api/BootstrapApi.cpp new file mode 100644 index 000000000..fc3ce6070 --- /dev/null +++ b/src/plugin/api/BootstrapApi.cpp @@ -0,0 +1,38 @@ +#include "ApiConfigurationMenuItem.h" +#include "BootstrapApi.h" +#include "api/ApiBootstrap.h" +#include "api/ApiFactory.h" +#include "bootstrap/PersistenceContainer.h" +#include "euroscope/CallbackFunction.h" +#include "plugin/FunctionCallEventHandler.h" +#include "radarscreen/ConfigurableDisplayCollection.h" + +using UKControllerPlugin::Bootstrap::PersistenceContainer; +using UKControllerPlugin::Euroscope::CallbackFunction; + +namespace UKControllerPlugin::Api { + void BootstrapApi(PersistenceContainer& container) + { + container.apiFactory = + UKControllerPluginUtils::Api::Bootstrap(*container.settingsRepository, *container.windows); + container.api = UKControllerPluginUtils::Api::BootstrapLegacy(*container.apiFactory, *container.curl); + } + + void BootstrapConfigurationMenuItem( + const PersistenceContainer& container, RadarScreen::ConfigurableDisplayCollection& configurableDisplays) + { + unsigned int callbackId = container.pluginFunctionHandlers->ReserveNextDynamicFunctionId(); + std::shared_ptr menuItem = std::make_shared( + *container.apiFactory->SettingsProvider(), *container.windows, callbackId); + + CallbackFunction menuItemSelectedCallback( + callbackId, // NOLINT + "API Configuration Menu Item Selected", + [menuItem](int functionId, std::string subject, RECT screenObjectArea) { + menuItem->Configure(functionId, std::move(subject), screenObjectArea); + }); + + container.pluginFunctionHandlers->RegisterFunctionCall(menuItemSelectedCallback); + configurableDisplays.RegisterDisplay(menuItem); + } +} // namespace UKControllerPlugin::Api diff --git a/src/plugin/api/BootstrapApi.h b/src/plugin/api/BootstrapApi.h new file mode 100644 index 000000000..cf31966c4 --- /dev/null +++ b/src/plugin/api/BootstrapApi.h @@ -0,0 +1,17 @@ +#pragma once + +namespace UKControllerPlugin { + namespace Bootstrap { + struct PersistenceContainer; + } // namespace Bootstrap + namespace RadarScreen { + class ConfigurableDisplayCollection; + } // namespace RadarScreen +} // namespace UKControllerPlugin + +namespace UKControllerPlugin::Api { + void BootstrapApi(Bootstrap::PersistenceContainer& container); + void BootstrapConfigurationMenuItem( + const Bootstrap::PersistenceContainer& container, + RadarScreen::ConfigurableDisplayCollection& configurableDisplays); +} // namespace UKControllerPlugin::Api diff --git a/src/plugin/api/FirstTimeApiAuthorisationChecker.cpp b/src/plugin/api/FirstTimeApiAuthorisationChecker.cpp new file mode 100644 index 000000000..629858bf4 --- /dev/null +++ b/src/plugin/api/FirstTimeApiAuthorisationChecker.cpp @@ -0,0 +1,53 @@ +#include "FirstTimeApiAuthorisationChecker.h" +#include "api/ApiRequestFactory.h" +#include "api/ApiRequestException.h" +#include "api/ApiSettingsProviderInterface.h" +#include "windows/WinApiInterface.h" + +using UKControllerPluginUtils::Api::ApiRequestException; + +namespace UKControllerPlugin::Api { + + void FirstTimeApiAuthorisationCheck( + UKControllerPluginUtils::Api::ApiSettingsProviderInterface& settingsProviderInterface, + Windows::WinApiInterface& windows) + { + ApiRequest() + .Get("authorise") + .Then([]() { LogInfo("Api authorisation check was successful."); }) + .Catch([&windows, &settingsProviderInterface](const ApiRequestException& exception) { + LogWarning( + "Api authorisation check failed, status code was " + + std::to_string(static_cast(exception.StatusCode()))); + + if (UKControllerPluginUtils::Http::IsServerError(exception.StatusCode())) { + windows.OpenMessageBox( + L"Server error whilst checking API authentication, some functionality may not work as " + "expected. If your configuration is otherwise correct, functionality will resume when the " + "service is online again.", + L"UKCP API Server Error", + MB_OK | MB_ICONWARNING); + return; + } + + auto messageResponse = windows.OpenMessageBox( + L"API authentication failed. Please re-download your credentails from the VATSIM UK website " + "and click OK to try again. If this problem persists, please contact the Web Services Department.", + L"UKCP API Config Invalid", + MB_OKCANCEL | MB_ICONWARNING); + + if (messageResponse == IDCANCEL || !settingsProviderInterface.Reload()) { + LogInfo("User elected not to set API key after authentication failure"); + windows.OpenMessageBox( + L"You have elected not to complete API setup at this time. Some functionality of the plugin " + "may not work as expected.", + L"UKCP API Config Not Updated", + MB_OK | MB_ICONWARNING); + return; + } + + FirstTimeApiAuthorisationCheck(settingsProviderInterface, windows); + }) + .Await(); + } +} // namespace UKControllerPlugin::Api diff --git a/src/plugin/api/FirstTimeApiAuthorisationChecker.h b/src/plugin/api/FirstTimeApiAuthorisationChecker.h new file mode 100644 index 000000000..a02407cc3 --- /dev/null +++ b/src/plugin/api/FirstTimeApiAuthorisationChecker.h @@ -0,0 +1,14 @@ +#pragma once + +namespace UKControllerPluginUtils::Api { + class ApiSettingsProviderInterface; +} // namespace UKControllerPluginUtils::Api + +namespace UKControllerPlugin::Windows { + class WinApiInterface; +} // namespace UKControllerPlugin::Windows +namespace UKControllerPlugin::Api { + void FirstTimeApiAuthorisationCheck( + UKControllerPluginUtils::Api::ApiSettingsProviderInterface& settingsProviderInterface, + Windows::WinApiInterface& windows); +} // namespace UKControllerPlugin::Api diff --git a/src/plugin/api/FirstTimeApiConfigLoader.cpp b/src/plugin/api/FirstTimeApiConfigLoader.cpp new file mode 100644 index 000000000..8c530a3c7 --- /dev/null +++ b/src/plugin/api/FirstTimeApiConfigLoader.cpp @@ -0,0 +1,21 @@ +#include "FirstTimeApiConfigLoader.h" +#include "api/ApiSettingsProviderInterface.h" + +namespace UKControllerPlugin::Api { + + auto LocateConfig(UKControllerPluginUtils::Api::ApiSettingsProviderInterface& settingsProvider) -> bool + { + if (settingsProvider.Has()) { + LogInfo("Api configuration successfully loaded"); + return true; + } + + if (!settingsProvider.Reload()) { + LogInfo("First time api config load, user elected not to load config"); + return false; + } + + LogInfo("First time api config load completed"); + return true; + } +} // namespace UKControllerPlugin::Api diff --git a/src/plugin/api/FirstTimeApiConfigLoader.h b/src/plugin/api/FirstTimeApiConfigLoader.h new file mode 100644 index 000000000..289197718 --- /dev/null +++ b/src/plugin/api/FirstTimeApiConfigLoader.h @@ -0,0 +1,9 @@ +#pragma once + +namespace UKControllerPluginUtils::Api { + class ApiSettingsProviderInterface; +} // namespace UKControllerPluginUtils::Api + +namespace UKControllerPlugin::Api { + auto LocateConfig(UKControllerPluginUtils::Api::ApiSettingsProviderInterface& settingsProvider) -> bool; +} // namespace UKControllerPlugin::Api diff --git a/src/plugin/bootstrap/HelperBootstrap.cpp b/src/plugin/bootstrap/HelperBootstrap.cpp index e12844e3b..07af98d98 100644 --- a/src/plugin/bootstrap/HelperBootstrap.cpp +++ b/src/plugin/bootstrap/HelperBootstrap.cpp @@ -1,8 +1,6 @@ #include "HelperBootstrap.h" #include "PersistenceContainer.h" -#include "api/ApiConfigurationMenuItem.h" -#include "api/ApiHelper.h" -#include "api/LocateApiSettings.h" +#include "curl/CurlApi.h" #include "euroscope/CallbackFunction.h" #include "plugin/FunctionCallEventHandler.h" #include "radarscreen/ConfigurableDisplayCollection.h" @@ -10,10 +8,8 @@ #include "setting/SettingRepositoryFactory.h" #include "task/TaskRunner.h" -using UKControllerPlugin::Api::ApiConfigurationMenuItem; -using UKControllerPlugin::Api::ApiHelper; -using UKControllerPlugin::Api::ApiRequestBuilder; using UKControllerPlugin::Bootstrap::PersistenceContainer; +using UKControllerPlugin::Curl::CurlApi; using UKControllerPlugin::Euroscope::CallbackFunction; using UKControllerPlugin::RadarScreen::ConfigurableDisplayCollection; using UKControllerPlugin::Setting::SettingRepository; @@ -27,38 +23,8 @@ namespace UKControllerPlugin::Bootstrap { */ void HelperBootstrap::Bootstrap(PersistenceContainer& persistence) { - persistence.settingsRepository = SettingRepositoryFactory::Create(*persistence.windows); - - // Prompt for a settings file, if one isn't there. - Api::LocateApiSettings(*persistence.windows, *persistence.settingsRepository); - - ApiRequestBuilder requestBuilder( - persistence.settingsRepository->GetSetting("api-url", "https://ukcp.vatsim.uk"), - persistence.settingsRepository->GetSetting("api-key")); - persistence.api = std::make_unique(*persistence.curl, requestBuilder); - + persistence.settingsRepository = SettingRepositoryFactory::Create(); persistence.taskRunner = std::make_shared(3); SetTaskRunner(persistence.taskRunner); } - - /* - Create the ApiConfigurationMenuItem and bootstrap it in. - */ - void HelperBootstrap::BootstrapApiConfigurationItem( - const PersistenceContainer& persistence, ConfigurableDisplayCollection& configurableDisplays) - { - unsigned int callbackId = persistence.pluginFunctionHandlers->ReserveNextDynamicFunctionId(); - std::shared_ptr menuItem = - std::make_shared(*persistence.windows, callbackId); - - CallbackFunction menuItemSelectedCallback( - callbackId, // NOLINT - "API Configuration Menu Item Selected", - [menuItem](int functionId, std::string subject, RECT screenObjectArea) { - menuItem->Configure(functionId, std::move(subject), screenObjectArea); - }); - - persistence.pluginFunctionHandlers->RegisterFunctionCall(menuItemSelectedCallback); - configurableDisplays.RegisterDisplay(menuItem); - } } // namespace UKControllerPlugin::Bootstrap diff --git a/src/plugin/bootstrap/HelperBootstrap.h b/src/plugin/bootstrap/HelperBootstrap.h index a8a945d54..efbbee1da 100644 --- a/src/plugin/bootstrap/HelperBootstrap.h +++ b/src/plugin/bootstrap/HelperBootstrap.h @@ -4,14 +4,13 @@ namespace UKControllerPlugin { namespace Bootstrap { struct PersistenceContainer; - } // namespace Bootstrap + } // namespace Bootstrap namespace RadarScreen { class ConfigurableDisplayCollection; - } // namespace RadarScreen -} // namespace UKControllerPlugin + } // namespace RadarScreen +} // namespace UKControllerPlugin // END - namespace UKControllerPlugin { namespace Bootstrap { @@ -21,11 +20,7 @@ namespace UKControllerPlugin { class HelperBootstrap { public: - static void Bootstrap(UKControllerPlugin::Bootstrap::PersistenceContainer & persistence); - static void BootstrapApiConfigurationItem( - const UKControllerPlugin::Bootstrap::PersistenceContainer & persistence, - UKControllerPlugin::RadarScreen::ConfigurableDisplayCollection & configurableDisplays - ); + static void Bootstrap(UKControllerPlugin::Bootstrap::PersistenceContainer& persistence); }; - } // namespace Bootstrap -} // namespace UKControllerPlugin + } // namespace Bootstrap +} // namespace UKControllerPlugin diff --git a/src/plugin/bootstrap/InitialisePlugin.cpp b/src/plugin/bootstrap/InitialisePlugin.cpp index 30a1ce474..ed02a98ef 100644 --- a/src/plugin/bootstrap/InitialisePlugin.cpp +++ b/src/plugin/bootstrap/InitialisePlugin.cpp @@ -1,7 +1,11 @@ #include "aircraft/AircraftModule.h" #include "aircraft/CallsignSelectionListFactoryBootstrap.h" #include "airfield/AirfieldModule.h" -#include "api/ApiAuthChecker.h" +#include "api/ApiFactory.h" +#include "api/ApiRequestFactory.h" +#include "api/BootstrapApi.h" +#include "api/FirstTimeApiConfigLoader.h" +#include "api/FirstTimeApiAuthorisationChecker.h" #include "bootstrap/CollectionBootstrap.h" #include "bootstrap/EventHandlerCollectionBootstrap.h" #include "bootstrap/ExternalsBootstrap.h" @@ -57,7 +61,6 @@ #include "update/PluginVersion.h" #include "wake/WakeModule.h" -using UKControllerPlugin::Api::ApiAuthChecker; using UKControllerPlugin::Bootstrap::CollectionBootstrap; using UKControllerPlugin::Bootstrap::EventHandlerCollectionBootstrap; using UKControllerPlugin::Bootstrap::ExternalsBootstrap; @@ -93,6 +96,7 @@ namespace UKControllerPlugin { */ void InitialisePlugin::EuroScopeCleanup() { + this->container->apiFactory->RequestFactory().AwaitRequestCompletion(); this->container->taskRunner.reset(); this->container.reset(); this->duplicatePlugin.reset(); @@ -156,16 +160,19 @@ namespace UKControllerPlugin { // User messager UserMessagerBootstrap::BootstrapPlugin(*this->container); - // API + Websocket + // Settings, api, websocket HelperBootstrap::Bootstrap(*this->container); + Api::BootstrapApi(*this->container); Push::BootstrapPlugin(*this->container, this->duplicatePlugin->Duplicate()); // Datetime Datablock::BootstrapPlugin(*this->container); - // If we're not allowed to use the API because we've been banned or something... It's no go. - ApiAuthChecker::IsAuthorised( - *this->container->api, *this->container->windows, *this->container->settingsRepository); + // Perform a first-time load of API config and check we're authorised. + if (Api::LocateConfig(*this->container->apiFactory->SettingsProvider())) { + Api::FirstTimeApiAuthorisationCheck( + *this->container->apiFactory->SettingsProvider(), *this->container->windows); + }; // Dependency loading can happen regardless of plugin version or API status. Dependency::UpdateDependencies(*this->container->api, *this->container->windows); @@ -216,7 +223,8 @@ namespace UKControllerPlugin { *this->container->dialogManager, *this->container->pluginUserSettingHandler, *this->container->userSettingHandlers, - *this->container->settingsRepository); + *this->container->settingsRepository, + *this->container->windows); // Bootstrap the modules Metar::BootstrapPlugin(*this->container); diff --git a/src/plugin/bootstrap/PersistenceContainer.cpp b/src/plugin/bootstrap/PersistenceContainer.cpp index 7570f72e8..3b6635200 100644 --- a/src/plugin/bootstrap/PersistenceContainer.cpp +++ b/src/plugin/bootstrap/PersistenceContainer.cpp @@ -3,6 +3,7 @@ #include "aircraft/AircraftTypeMapperInterface.h" #include "aircraft/CallsignSelectionListFactory.h" #include "airfield/AirfieldCollection.h" +#include "api/ApiFactory.h" #include "api/ApiInterface.h" #include "command/CommandHandlerCollection.h" #include "controller/ActiveCallsignCollection.h" diff --git a/src/plugin/bootstrap/PersistenceContainer.h b/src/plugin/bootstrap/PersistenceContainer.h index e58bbb190..68c07bf28 100644 --- a/src/plugin/bootstrap/PersistenceContainer.h +++ b/src/plugin/bootstrap/PersistenceContainer.h @@ -149,6 +149,12 @@ namespace UKControllerPlugin { } // namespace Windows } // namespace UKControllerPlugin +namespace UKControllerPluginUtils { + namespace Api { + class ApiFactory; + } // namespace Api +} // namespace UKControllerPluginUtils + namespace UKControllerPlugin::Bootstrap { /* @@ -170,6 +176,7 @@ namespace UKControllerPlugin::Bootstrap { // The helpers and collections std::unique_ptr api; + std::shared_ptr apiFactory; std::shared_ptr taskRunner; std::unique_ptr activeCallsigns; std::unique_ptr flightplans; diff --git a/src/plugin/euroscope/GeneralSettingsConfigurationBootstrap.cpp b/src/plugin/euroscope/GeneralSettingsConfigurationBootstrap.cpp index 2efafd178..ff046d7bc 100644 --- a/src/plugin/euroscope/GeneralSettingsConfigurationBootstrap.cpp +++ b/src/plugin/euroscope/GeneralSettingsConfigurationBootstrap.cpp @@ -9,6 +9,7 @@ #include "euroscope/GeneralSettingsDialog.h" #include "euroscope/UserSettingAwareCollection.h" #include "setting/SettingRepository.h" +#include "setting/JsonFileSettingProvider.h" using UKControllerPlugin::Command::CommandHandlerCollection; using UKControllerPlugin::Dialog::DialogData; @@ -19,6 +20,8 @@ using UKControllerPlugin::Euroscope::UserSetting; using UKControllerPlugin::Euroscope::UserSettingAwareCollection; using UKControllerPlugin::Plugin::FunctionCallEventHandler; using UKControllerPlugin::RadarScreen::ConfigurableDisplayCollection; +using UKControllerPlugin::Setting::JsonFileSettingProvider; +using UKControllerPlugin::Windows::WinApiInterface; namespace UKControllerPlugin { namespace Euroscope { @@ -27,8 +30,11 @@ namespace UKControllerPlugin { DialogManager& dialogManager, UserSetting& userSettings, UserSettingAwareCollection& userSettingsHandlers, - Setting::SettingRepository& settings) + Setting::SettingRepository& settings, + WinApiInterface& windows) { + settings.AddProvider(std::make_shared( + L"release-channel.json", std::set{"release_channel"}, windows)); std::shared_ptr dialog = std::make_shared(userSettings, userSettingsHandlers, settings); dialogManager.AddDialog( diff --git a/src/plugin/euroscope/GeneralSettingsConfigurationBootstrap.h b/src/plugin/euroscope/GeneralSettingsConfigurationBootstrap.h index b731d39fb..17b800ef0 100644 --- a/src/plugin/euroscope/GeneralSettingsConfigurationBootstrap.h +++ b/src/plugin/euroscope/GeneralSettingsConfigurationBootstrap.h @@ -20,6 +20,9 @@ namespace UKControllerPlugin { namespace Setting { class SettingRepository; } // namespace Setting + namespace Windows { + class WinApiInterface; + } // namespace Windows } // namespace UKControllerPlugin namespace UKControllerPlugin { @@ -35,7 +38,8 @@ namespace UKControllerPlugin { UKControllerPlugin::Dialog::DialogManager& dialogManager, UKControllerPlugin::Euroscope::UserSetting& userSettings, UKControllerPlugin::Euroscope::UserSettingAwareCollection& userSettingsHandlers, - Setting::SettingRepository& settings); + Setting::SettingRepository& settings, + Windows::WinApiInterface& windows); static void BootstrapRadarScreen( UKControllerPlugin::Plugin::FunctionCallEventHandler& functionCalls, diff --git a/src/plugin/euroscope/GeneralSettingsDialog.cpp b/src/plugin/euroscope/GeneralSettingsDialog.cpp index 7fc4c8a8e..140473a00 100644 --- a/src/plugin/euroscope/GeneralSettingsDialog.cpp +++ b/src/plugin/euroscope/GeneralSettingsDialog.cpp @@ -129,12 +129,7 @@ namespace UKControllerPlugin { const std::string selectedChannel = reinterpret_cast( SendDlgItemMessage(hwnd, IDC_RELEASE_CHANNEL, CB_GETITEMDATA, selectedReleaseChannelIndex, 0)); - if (this->settings.HasSetting("release_channel")) { - this->settings.UpdateSetting("release_channel", selectedChannel); - } else { - this->settings.AddSettingValue({"release_channel", selectedChannel, "release-channel.json"}); - } - + this->settings.UpdateSetting("release_channel", selectedChannel); this->userSettingsHandlers.UserSettingsUpdateEvent(this->userSettings); } diff --git a/src/plugin/hold/AircraftEnteredHoldingAreaEventHandler.cpp b/src/plugin/hold/AircraftEnteredHoldingAreaEventHandler.cpp new file mode 100644 index 000000000..625c68b20 --- /dev/null +++ b/src/plugin/hold/AircraftEnteredHoldingAreaEventHandler.cpp @@ -0,0 +1,62 @@ +#include "AircraftEnteredHoldingAreaEventHandler.h" +#include "HoldManager.h" +#include "ProximityHold.h" +#include "navaids/NavaidCollection.h" +#include "api/ApiRequestFactory.h" +#include "time/ParseTimeStrings.h" + +namespace UKControllerPlugin::Hold { + + AircraftEnteredHoldingAreaEventHandler::AircraftEnteredHoldingAreaEventHandler( + HoldManager& holdManager, const Navaids::NavaidCollection& navaids) + : holdManager(holdManager), navaids(navaids) + { + } + + void AircraftEnteredHoldingAreaEventHandler::ProcessPushEvent(const Push::PushEvent& message) + { + this->ProcessData(message.data); + } + + auto AircraftEnteredHoldingAreaEventHandler::GetPushEventSubscriptions() const + -> std::set + { + return {{Push::PushEventSubscription::SUB_TYPE_EVENT, "hold.area-entered"}}; + } + + void AircraftEnteredHoldingAreaEventHandler::PluginEventsSynced() + { + ApiRequest().Get("hold/proximity").Then([this](const UKControllerPluginUtils::Api::Response response) { + if (!response.Data().is_array()) { + LogWarning("Aircraft holding proximity sync data invalid"); + return; + } + + for (const auto& item : response.Data()) { + ProcessData(item); + } + }); + } + + void AircraftEnteredHoldingAreaEventHandler::ProcessData(const nlohmann::json& data) const + { + if (!DataValid(data)) { + LogWarning("Invalid aircraft entered holding area event" + data.dump()); + return; + } + + holdManager.AddAircraftToProximityHold(std::make_shared( + data.at("callsign").get(), + navaids.Get(data.at("navaid_id").get()).identifier, + Time::ParseIsoZuluString(data.at("entered_at").get()))); + } + + auto AircraftEnteredHoldingAreaEventHandler::DataValid(const nlohmann::json& data) const -> bool + { + return data.is_object() && data.contains("callsign") && data.at("callsign").is_string() && + data.contains("navaid_id") && data.at("navaid_id").is_number_integer() && + navaids.Get(data.at("navaid_id").get()) != navaids.invalidNavaid && data.contains("entered_at") && + data.at("entered_at").is_string() && + Time::ParseIsoZuluString(data.at("entered_at").get()) != Time::invalidTime; + } +} // namespace UKControllerPlugin::Hold diff --git a/src/plugin/hold/AircraftEnteredHoldingAreaEventHandler.h b/src/plugin/hold/AircraftEnteredHoldingAreaEventHandler.h new file mode 100644 index 000000000..e8e25a8f8 --- /dev/null +++ b/src/plugin/hold/AircraftEnteredHoldingAreaEventHandler.h @@ -0,0 +1,29 @@ +#pragma once +#include "push/PushEventProcessorInterface.h" + +namespace UKControllerPlugin::Navaids { + class NavaidCollection; +} // namespace UKControllerPlugin::Navaids + +namespace UKControllerPlugin::Hold { + class HoldManager; + + class AircraftEnteredHoldingAreaEventHandler : public Push::PushEventProcessorInterface + { + public: + AircraftEnteredHoldingAreaEventHandler(HoldManager& holdManager, const Navaids::NavaidCollection& navaids); + void ProcessPushEvent(const Push::PushEvent& message) override; + [[nodiscard]] auto GetPushEventSubscriptions() const -> std::set override; + void PluginEventsSynced() override; + + private: + void ProcessData(const nlohmann::json& data) const; + [[nodiscard]] auto DataValid(const nlohmann::json& data) const -> bool; + + // For managing proximity holds + HoldManager& holdManager; + + // All the navaids + const Navaids::NavaidCollection& navaids; + }; +} // namespace UKControllerPlugin::Hold diff --git a/src/plugin/hold/AircraftExitedHoldingAreaEventHandler.cpp b/src/plugin/hold/AircraftExitedHoldingAreaEventHandler.cpp new file mode 100644 index 000000000..ee09b0355 --- /dev/null +++ b/src/plugin/hold/AircraftExitedHoldingAreaEventHandler.cpp @@ -0,0 +1,36 @@ +#include "AircraftExitedHoldingAreaEventHandler.h" +#include "HoldManager.h" +#include "navaids/NavaidCollection.h" + +namespace UKControllerPlugin::Hold { + + AircraftExitedHoldingAreaEventHandler::AircraftExitedHoldingAreaEventHandler( + HoldManager& holdManager, const Navaids::NavaidCollection& navaids) + : holdManager(holdManager), navaids(navaids) + { + } + + void AircraftExitedHoldingAreaEventHandler::ProcessPushEvent(const Push::PushEvent& message) + { + const auto data = message.data; + if (!DataValid(data)) { + LogWarning("Invalid aircraft exited holding area event" + data.dump()); + return; + } + + holdManager.RemoveAircraftFromProximityHold( + data.at("callsign").get(), navaids.Get(data.at("navaid_id").get()).identifier); + } + + auto AircraftExitedHoldingAreaEventHandler::GetPushEventSubscriptions() const + -> std::set + { + return {{Push::PushEventSubscription::SUB_TYPE_EVENT, "hold.area-exited"}}; + } + + auto AircraftExitedHoldingAreaEventHandler::DataValid(const nlohmann::json& data) const -> bool + { + return data.is_object() && data.contains("callsign") && data.at("callsign").is_string() && + data.contains("navaid_id") && data.at("navaid_id").is_number_integer(); + } +} // namespace UKControllerPlugin::Hold diff --git a/src/plugin/hold/AircraftExitedHoldingAreaEventHandler.h b/src/plugin/hold/AircraftExitedHoldingAreaEventHandler.h new file mode 100644 index 000000000..0f2daac31 --- /dev/null +++ b/src/plugin/hold/AircraftExitedHoldingAreaEventHandler.h @@ -0,0 +1,27 @@ +#pragma once +#include "push/PushEventProcessorInterface.h" + +namespace UKControllerPlugin::Navaids { + class NavaidCollection; +} // namespace UKControllerPlugin::Navaids + +namespace UKControllerPlugin::Hold { + class HoldManager; + + class AircraftExitedHoldingAreaEventHandler : public Push::PushEventProcessorInterface + { + public: + AircraftExitedHoldingAreaEventHandler(HoldManager& holdManager, const Navaids::NavaidCollection& navaids); + void ProcessPushEvent(const Push::PushEvent& message) override; + [[nodiscard]] auto GetPushEventSubscriptions() const -> std::set override; + + private: + [[nodiscard]] auto DataValid(const nlohmann::json& data) const -> bool; + + // For managing proximity holds + HoldManager& holdManager; + + // All the navaids + const Navaids::NavaidCollection& navaids; + }; +} // namespace UKControllerPlugin::Hold diff --git a/src/plugin/hold/CompareProximityHolds.cpp b/src/plugin/hold/CompareProximityHolds.cpp new file mode 100644 index 000000000..62f283dca --- /dev/null +++ b/src/plugin/hold/CompareProximityHolds.cpp @@ -0,0 +1,20 @@ +#include "CompareProximityHolds.h" +#include "ProximityHold.h" + +namespace UKControllerPlugin::Hold { + bool CompareProximityHolds::operator()(const std::shared_ptr& proximity, std::string navaid) const + { + return proximity->navaid < navaid; + } + + bool CompareProximityHolds::operator()(std::string navaid, const std::shared_ptr& proximity) const + { + return navaid < proximity->navaid; + } + + bool CompareProximityHolds::operator()( + const std::shared_ptr& a, const std::shared_ptr& b) const + { + return a->navaid < b->navaid; + } +} // namespace UKControllerPlugin::Hold diff --git a/src/plugin/hold/CompareProximityHolds.h b/src/plugin/hold/CompareProximityHolds.h new file mode 100644 index 000000000..fb8d84ded --- /dev/null +++ b/src/plugin/hold/CompareProximityHolds.h @@ -0,0 +1,14 @@ +#pragma once + +namespace UKControllerPlugin::Hold { + struct ProximityHold; + + using CompareProximityHolds = struct CompareProximityHolds + { + using is_transparent = std::string; + + bool operator()(const std::shared_ptr& proximity, std::string navaid) const; + bool operator()(std::string navaid, const std::shared_ptr& proximity) const; + bool operator()(const std::shared_ptr& a, const std::shared_ptr& b) const; + }; +} // namespace UKControllerPlugin::Hold diff --git a/src/plugin/hold/HoldDisplay.cpp b/src/plugin/hold/HoldDisplay.cpp index 1654d079f..cedb5b466 100644 --- a/src/plugin/hold/HoldDisplay.cpp +++ b/src/plugin/hold/HoldDisplay.cpp @@ -6,6 +6,7 @@ #include "HoldModule.h" #include "HoldingAircraft.h" #include "HoldingData.h" +#include "ProximityHold.h" #include "PublishedHoldCollection.h" #include "dialog/DialogManager.h" #include "euroscope/EuroScopeCFlightPlanInterface.h" @@ -933,9 +934,14 @@ namespace UKControllerPlugin { clearedLevelDisplay.Y + clearedLevelDisplay.Height}, false); - // Time in hold, if it's assigned - if ((*it)->GetAssignedHold() != (*it)->GetNoHoldAssigned()) { - std::wstring timeString = GetTimeInHoldDisplayString((*it)->GetAssignedHoldEntryTime()); + // Time in hold, if it's assigned to this one + if ((*it)->GetAssignedHold() == navaid.identifier) { + auto holdProximity = (*it)->GetProximityHold(navaid.identifier); + if (holdProximity == nullptr) { + return; + } + + std::wstring timeString = GetTimeInHoldDisplayString(holdProximity->enteredAt); graphics.DrawString(timeString, timeInHoldDisplay, this->dataBrush); } } diff --git a/src/plugin/hold/HoldEventHandler.cpp b/src/plugin/hold/HoldEventHandler.cpp index a2671d32d..5fbe49854 100644 --- a/src/plugin/hold/HoldEventHandler.cpp +++ b/src/plugin/hold/HoldEventHandler.cpp @@ -1,12 +1,14 @@ #include "HoldingAircraft.h" #include "HoldEventHandler.h" #include "HoldManager.h" +#include "ProximityHold.h" #include "euroscope/EuroScopeCFlightPlanInterface.h" #include "euroscope/EuroScopeCRadarTargetInterface.h" #include "euroscope/EuroscopePluginLoopbackInterface.h" #include "euroscope/EuroscopeSectorFileElementInterface.h" #include "navaids/NavaidCollection.h" #include "tag/TagData.h" +#include "time/SystemClock.h" using UKControllerPlugin::Euroscope::EuroScopeCFlightPlanInterface; using UKControllerPlugin::Euroscope::EuroScopeCRadarTargetInterface; @@ -56,7 +58,8 @@ namespace UKControllerPlugin::Hold { const std::shared_ptr& rt) { for (auto navaids = this->navaids.cbegin(); navaids != this->navaids.cend(); ++navaids) { if (rt->GetPosition().DistanceTo(navaids->coordinates) <= this->proximityDistance) { - this->holdManager.AddAircraftToProximityHold(fp->GetCallsign(), navaids->identifier); + this->holdManager.AddAircraftToProximityHold( + std::make_shared(fp->GetCallsign(), navaids->identifier, Time::TimeNow())); } else { this->holdManager.RemoveAircraftFromProximityHold(fp->GetCallsign(), navaids->identifier); } diff --git a/src/plugin/hold/HoldManager.cpp b/src/plugin/hold/HoldManager.cpp index 865038490..63b1ff2a9 100644 --- a/src/plugin/hold/HoldManager.cpp +++ b/src/plugin/hold/HoldManager.cpp @@ -1,5 +1,6 @@ #include "HoldingAircraft.h" #include "HoldManager.h" +#include "ProximityHold.h" #include "api/ApiException.h" #include "api/ApiInterface.h" #include "euroscope/EuroScopeCFlightPlanInterface.h" @@ -24,18 +25,19 @@ namespace UKControllerPlugin::Hold { /* Add an aircraft to a "hold" because it's within proximity to the fix */ - void HoldManager::AddAircraftToProximityHold(const std::string& callsign, const std::string& hold) + void HoldManager::AddAircraftToProximityHold(const std::shared_ptr& hold) { + auto lock = std::lock_guard(this->dataMutex); std::shared_ptr holdingAircraft; - if (this->aircraft.count(callsign) != 0) { - holdingAircraft = *this->aircraft.find(callsign); + if (this->aircraft.count(hold->callsign) != 0) { + holdingAircraft = *this->aircraft.find(hold->callsign); holdingAircraft->AddProximityHold(hold); } else { - holdingAircraft = std::make_shared(callsign, std::set({hold})); + holdingAircraft = std::make_shared(hold->callsign, hold); this->aircraft.insert(holdingAircraft); } - this->holds[hold].insert(holdingAircraft); + this->holds[hold->navaid].insert(holdingAircraft); } /* @@ -43,6 +45,7 @@ namespace UKControllerPlugin::Hold { */ void HoldManager::AssignAircraftToHold(const std::string& callsign, const std::string& hold, bool updateApi) { + auto lock = std::lock_guard(this->dataMutex); // Add it to the aircraft list or fetch it if needed std::shared_ptr holdingAircraft; @@ -83,11 +86,13 @@ namespace UKControllerPlugin::Hold { auto HoldManager::GetAircraftForHold(const std::string& hold) const -> const std::set, CompareHoldingAircraft>& { + auto lock = std::lock_guard(this->dataMutex); return this->holds.count(hold) != 0 ? this->holds.find(hold)->second : this->invalidHolds; } auto HoldManager::GetHoldingAircraft(const std::string& callsign) -> const std::shared_ptr& { + auto lock = std::lock_guard(this->dataMutex); auto aircraft = this->aircraft.find(callsign); return aircraft != this->aircraft.cend() ? *aircraft : this->invalidAircraft; } @@ -97,6 +102,7 @@ namespace UKControllerPlugin::Hold { */ void HoldManager::UnassignAircraftFromHold(const std::string& callsign, bool updateApi) { + auto lock = std::lock_guard(this->dataMutex); if (this->aircraft.find(callsign) == this->aircraft.cend()) { return; } @@ -135,6 +141,7 @@ namespace UKControllerPlugin::Hold { */ void HoldManager::RemoveAircraftFromProximityHold(const std::string& callsign, const std::string& hold) { + auto lock = std::lock_guard(this->dataMutex); if (this->aircraft.find(callsign) == this->aircraft.cend()) { return; } diff --git a/src/plugin/hold/HoldManager.h b/src/plugin/hold/HoldManager.h index 9aceacc6c..2b57c943e 100644 --- a/src/plugin/hold/HoldManager.h +++ b/src/plugin/hold/HoldManager.h @@ -17,6 +17,7 @@ namespace UKControllerPlugin { namespace UKControllerPlugin::Hold { class HoldingAircraft; + struct ProximityHold; /* A class that manages which aircraft are in which holds @@ -27,9 +28,9 @@ namespace UKControllerPlugin::Hold { HoldManager( const UKControllerPlugin::Api::ApiInterface& api, UKControllerPlugin::TaskManager::TaskRunnerInterface& taskRunner); - void AddAircraftToProximityHold(const std::string& callsign, const std::string& hold); + void AddAircraftToProximityHold(const std::shared_ptr& hold); void AssignAircraftToHold(const std::string& callsign, const std::string& hold, bool updateApi); - size_t CountHoldingAircraft(void) const; + size_t CountHoldingAircraft() const; const std::set, CompareHoldingAircraft>& GetAircraftForHold(const std::string& hold) const; const std::shared_ptr& GetHoldingAircraft(const std::string& callsign); @@ -53,5 +54,8 @@ namespace UKControllerPlugin::Hold { // The aircraft in the holds std::set, CompareHoldingAircraft> aircraft; + + // A mutex for async access + mutable std::mutex dataMutex; }; } // namespace UKControllerPlugin::Hold diff --git a/src/plugin/hold/HoldModule.cpp b/src/plugin/hold/HoldModule.cpp index 673923f8d..b24c905ed 100644 --- a/src/plugin/hold/HoldModule.cpp +++ b/src/plugin/hold/HoldModule.cpp @@ -1,4 +1,6 @@ #include "AbstractHoldLevelRestriction.h" +#include "AircraftEnteredHoldingAreaEventHandler.h" +#include "AircraftExitedHoldingAreaEventHandler.h" #include "AssignHoldCommand.h" #include "DeemedSeparatedHold.h" #include "HoldConfigurationDialog.h" @@ -171,6 +173,12 @@ namespace UKControllerPlugin::Hold { "Loaded " + std::to_string(container.holdManager->CountHoldingAircraft()) + " aircraft into assigned holds"); }); + + // Hold proximity detection handlers + container.pushEventProcessors->AddProcessor( + std::make_shared(*container.holdManager, *container.navaids)); + container.pushEventProcessors->AddProcessor( + std::make_shared(*container.holdManager, *container.navaids)); } /* diff --git a/src/plugin/hold/HoldingAircraft.cpp b/src/plugin/hold/HoldingAircraft.cpp index 66ce9af37..d82767443 100644 --- a/src/plugin/hold/HoldingAircraft.cpp +++ b/src/plugin/hold/HoldingAircraft.cpp @@ -3,17 +3,16 @@ namespace UKControllerPlugin::Hold { HoldingAircraft::HoldingAircraft(std::string callsign, std::string assignedHold) - : callsign(std::move(callsign)), entryTime(std::chrono::system_clock::now()), - assignedHold(std::move(assignedHold)) + : callsign(std::move(callsign)), assignedHold(std::move(assignedHold)) { } - HoldingAircraft::HoldingAircraft(std::string callsign, std::set proximityHolds) - : callsign(std::move(callsign)), proximityHolds(std::move(proximityHolds)) + HoldingAircraft::HoldingAircraft(std::string callsign, std::shared_ptr proximityHold) + : callsign(std::move(callsign)), proximityHolds({proximityHold}) { } - void HoldingAircraft::AddProximityHold(const std::string& hold) + void HoldingAircraft::AddProximityHold(std::shared_ptr hold) { this->proximityHolds.insert(hold); } @@ -23,17 +22,13 @@ namespace UKControllerPlugin::Hold { return this->assignedHold; } - auto HoldingAircraft::GetAssignedHoldEntryTime() const -> const std::chrono::system_clock::time_point& - { - return this->entryTime; - } - auto HoldingAircraft::GetCallsign() const -> std::string { return this->callsign; } - auto HoldingAircraft::GetProximityHolds() const -> std::set + auto HoldingAircraft::GetProximityHolds() const + -> const std::set, CompareProximityHolds>& { return this->proximityHolds; } @@ -56,7 +51,6 @@ namespace UKControllerPlugin::Hold { void HoldingAircraft::SetAssignedHold(std::string hold) { this->assignedHold = std::move(hold); - this->entryTime = std::chrono::system_clock::now(); } void HoldingAircraft::RemoveAssignedHold() @@ -78,4 +72,10 @@ namespace UKControllerPlugin::Hold { { return this->noHoldAssigned; } + + auto HoldingAircraft::GetProximityHold(const std::string& hold) const -> std::shared_ptr + { + auto proximity = this->proximityHolds.find(hold); + return proximity == this->proximityHolds.cend() ? nullptr : *proximity; + } } // namespace UKControllerPlugin::Hold diff --git a/src/plugin/hold/HoldingAircraft.h b/src/plugin/hold/HoldingAircraft.h index ece83d8b7..e9c51b91d 100644 --- a/src/plugin/hold/HoldingAircraft.h +++ b/src/plugin/hold/HoldingAircraft.h @@ -1,6 +1,8 @@ #pragma once +#include "CompareProximityHolds.h" namespace UKControllerPlugin::Hold { + struct ProximityHold; /* Data about a holding aircraft @@ -9,13 +11,13 @@ namespace UKControllerPlugin::Hold { { public: HoldingAircraft(std::string callsign, std::string assignedHold); - HoldingAircraft(std::string callsign, std::set proximityHolds); - - void AddProximityHold(const std::string& hold); + HoldingAircraft(std::string callsign, std::shared_ptr proximityHold); + void AddProximityHold(std::shared_ptr hold); [[nodiscard]] auto GetAssignedHold() const -> std::string; - [[nodiscard]] auto GetAssignedHoldEntryTime() const -> const std::chrono::system_clock::time_point&; [[nodiscard]] auto GetCallsign() const -> std::string; - [[nodiscard]] auto GetProximityHolds() const -> std::set; + [[nodiscard]] auto GetProximityHolds() const + -> const std::set, CompareProximityHolds>&; + [[nodiscard]] auto GetProximityHold(const std::string& hold) const -> std::shared_ptr; [[nodiscard]] auto IsInAnyHold() const -> bool; [[nodiscard]] auto IsInHold(const std::string& hold) const -> bool; [[nodiscard]] auto IsInHoldProximity(const std::string& hold) const -> bool; @@ -31,13 +33,10 @@ namespace UKControllerPlugin::Hold { // The callsign of the aircraft const std::string callsign; - // The time the aircraft entered the hold - std::chrono::system_clock::time_point entryTime; - // The assigned hold of the aircraft (if any) std::string assignedHold = noHoldAssigned; // The holds against which the aircraft is the the vicinity - std::set proximityHolds; + std::set, CompareProximityHolds> proximityHolds; }; } // namespace UKControllerPlugin::Hold diff --git a/src/plugin/hold/ProximityHold.h b/src/plugin/hold/ProximityHold.h new file mode 100644 index 000000000..e3b3a94f6 --- /dev/null +++ b/src/plugin/hold/ProximityHold.h @@ -0,0 +1,17 @@ +#pragma once + +namespace UKControllerPlugin::Hold { + using ProximityHold = struct ProximityHold + { + ProximityHold(std::string callsign, std::string navaid, std::chrono::system_clock::time_point enteredAt) + : callsign(std::move(callsign)), navaid(std::move(navaid)), enteredAt(std::move(enteredAt)) + { + } + + std::string callsign; + + std::string navaid; + + std::chrono::system_clock::time_point enteredAt; + }; +} // namespace UKControllerPlugin::Hold diff --git a/src/plugin/navaids/Navaid.cpp b/src/plugin/navaids/Navaid.cpp index e3db617f8..d14be0fc9 100644 --- a/src/plugin/navaids/Navaid.cpp +++ b/src/plugin/navaids/Navaid.cpp @@ -5,4 +5,9 @@ namespace UKControllerPlugin::Navaids { { return this->identifier == compare.identifier; } + + auto Navaid::Navaid::operator!=(const Navaid& compare) const -> bool + { + return !(*this == compare); + } } // namespace UKControllerPlugin::Navaids diff --git a/src/plugin/navaids/Navaid.h b/src/plugin/navaids/Navaid.h index 802ee0a9f..b814f81b5 100644 --- a/src/plugin/navaids/Navaid.h +++ b/src/plugin/navaids/Navaid.h @@ -18,5 +18,6 @@ namespace UKControllerPlugin::Navaids { EuroScopePlugIn::CPosition coordinates; auto operator==(const Navaid& compare) const -> bool; + auto operator!=(const Navaid& compare) const -> bool; }; } // namespace UKControllerPlugin::Navaids diff --git a/src/plugin/navaids/NavaidCollection.cpp b/src/plugin/navaids/NavaidCollection.cpp index 7ffb3e4be..6cae6f6b9 100644 --- a/src/plugin/navaids/NavaidCollection.cpp +++ b/src/plugin/navaids/NavaidCollection.cpp @@ -1,5 +1,4 @@ -#include "pch/pch.h" -#include "navaids/NavaidCollection.h" +#include "NavaidCollection.h" namespace UKControllerPlugin { namespace Navaids { @@ -22,5 +21,11 @@ namespace UKControllerPlugin { return navaid == this->navaids.cend() ? this->invalidNavaid : *navaid; } - } // namespace Navaids -} // namespace UKControllerPlugin + auto NavaidCollection::Get(int id) const -> const UKControllerPlugin::Navaids::Navaid& + { + auto navaid = std::find_if( + this->navaids.cbegin(), this->navaids.cend(), [&id](const Navaid& navaid) { return navaid.id == id; }); + return navaid == this->navaids.cend() ? this->invalidNavaid : *navaid; + } + } // namespace Navaids +} // namespace UKControllerPlugin diff --git a/src/plugin/navaids/NavaidCollection.h b/src/plugin/navaids/NavaidCollection.h index ae8260194..f66fcb884 100644 --- a/src/plugin/navaids/NavaidCollection.h +++ b/src/plugin/navaids/NavaidCollection.h @@ -13,6 +13,7 @@ namespace UKControllerPlugin { public: void AddNavaid(Navaid navaid); size_t Count(void) const; + [[nodiscard]] auto Get(int id) const -> const UKControllerPlugin::Navaids::Navaid&; const UKControllerPlugin::Navaids::Navaid& GetByIdentifier(std::string identifier) const; const Navaid invalidNavaid = {0, "INVALID", EuroScopePlugIn::CPosition()}; diff --git a/src/plugin/pch/pch.h b/src/plugin/pch/pch.h index 9c765b149..9a16b4378 100644 --- a/src/plugin/pch/pch.h +++ b/src/plugin/pch/pch.h @@ -20,6 +20,7 @@ #include "spdlog/include/spdlog/sinks/null_sink.h" #include "json/json.hpp" +#include "api/ApiRequestFacade.h" #include "euroscope/EuroScopePlugIn.h" #include "log/LoggerFunctions.h" #include "task/RunAsyncTask.h" diff --git a/src/plugin/radarscreen/RadarScreenFactory.cpp b/src/plugin/radarscreen/RadarScreenFactory.cpp index 25ce29efe..b049bc2ac 100644 --- a/src/plugin/radarscreen/RadarScreenFactory.cpp +++ b/src/plugin/radarscreen/RadarScreenFactory.cpp @@ -4,6 +4,7 @@ #include "RadarScreenFactory.h" #include "ScreenControlsBootstrap.h" #include "UKRadarScreen.h" +#include "api/BootstrapApi.h" #include "bootstrap/HelperBootstrap.h" #include "bootstrap/PersistenceContainer.h" #include "countdown/CountdownModule.h" @@ -24,6 +25,7 @@ #include "srd/SrdModule.h" #include "wake/WakeModule.h" +using UKControllerPlugin::Api::BootstrapConfigurationMenuItem; using UKControllerPlugin::Bootstrap::HelperBootstrap; using UKControllerPlugin::Bootstrap::PersistenceContainer; using UKControllerPlugin::Command::CommandHandlerCollection; @@ -57,7 +59,7 @@ namespace UKControllerPlugin::RadarScreen { MenuToggleableDisplayFactory displayFactory(*this->persistence.pluginFunctionHandlers, configurableDisplays); // Run bootstrap - HelperBootstrap::BootstrapApiConfigurationItem(persistence, configurableDisplays); + BootstrapConfigurationMenuItem(persistence, configurableDisplays); SectorFile::BootstrapRadarScreen(persistence, userSettingHandlers); diff --git a/src/plugin/time/ParseTimeStrings.cpp b/src/plugin/time/ParseTimeStrings.cpp index 39bf0b09e..6c1068316 100644 --- a/src/plugin/time/ParseTimeStrings.cpp +++ b/src/plugin/time/ParseTimeStrings.cpp @@ -4,6 +4,7 @@ namespace UKControllerPlugin::Time { const std::chrono::system_clock::time_point invalidTime = (std::chrono::system_clock::time_point::max)(); const std::string FORMAT = "%F %T"; + const std::string ISO_8601_FORMAT = "%FT%T%Z"; [[nodiscard]] auto ParseTimeString(const std::string& time) -> std::chrono::system_clock::time_point { @@ -17,4 +18,12 @@ namespace UKControllerPlugin::Time { { return date::format(FORMAT, date::floor(timePoint)); } + + auto ParseIsoZuluString(const std::string& time) -> std::chrono::system_clock::time_point + { + date::sys_time timePoint; + std::istringstream inputStream(time); + inputStream >> date::parse(ISO_8601_FORMAT, timePoint); + return static_cast(inputStream) ? timePoint : invalidTime; + } } // namespace UKControllerPlugin::Time diff --git a/src/plugin/time/ParseTimeStrings.h b/src/plugin/time/ParseTimeStrings.h index 195fe86d5..b96ffeec5 100644 --- a/src/plugin/time/ParseTimeStrings.h +++ b/src/plugin/time/ParseTimeStrings.h @@ -4,4 +4,6 @@ namespace UKControllerPlugin::Time { [[nodiscard]] auto ParseTimeString(const std::string& time) -> std::chrono::system_clock::time_point; extern const std::chrono::system_clock::time_point invalidTime; [[nodiscard]] auto ToDateTimeString(const std::chrono::system_clock::time_point& timePoint) -> std::string; + [[nodiscard]] auto ParseIsoZuluString(const std::string& time) -> std::chrono::system_clock::time_point; + extern const std::chrono::system_clock::time_point invalidTime; } // namespace UKControllerPlugin::Time diff --git a/src/updater/dllmain.cpp b/src/updater/dllmain.cpp index 6c5323222..60a6946fe 100644 --- a/src/updater/dllmain.cpp +++ b/src/updater/dllmain.cpp @@ -2,6 +2,7 @@ #include "windows/WinApiInterface.h" #include "windows/WinApiBootstrap.h" #include "api/ApiBootstrap.h" +#include "api/ApiFactory.h" #include "setting/SettingRepositoryFactory.h" #include "curl/CurlApi.h" #include "log/LoggerBootstrap.h" @@ -34,8 +35,10 @@ UKCP_UPDATER_API bool PerformUpdates() // Bootstrap the API, download the updater if we don't have it already and run it UKControllerPlugin::Curl::CurlApi curl; std::unique_ptr settings = - UKControllerPlugin::Setting::SettingRepositoryFactory::Create(*windows); - std::unique_ptr api = UKControllerPlugin::Api::Bootstrap(*settings, curl); + UKControllerPlugin::Setting::SettingRepositoryFactory::Create(); + + auto factory = UKControllerPluginUtils::Api::Bootstrap(*settings, *windows); + auto api = UKControllerPluginUtils::Api::BootstrapLegacy(*factory, curl); LogInfo("Updater build version " + std::string(UKControllerPlugin::Plugin::PluginVersion::version)); UKControllerPlugin::Duplicate::DuplicatePlugin duplicatePlugin; diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt index e5cb05e8d..045147615 100644 --- a/src/utils/CMakeLists.txt +++ b/src/utils/CMakeLists.txt @@ -25,8 +25,6 @@ ENDIF() # Source groups ################################################################################ set(api - "api/ApiAuthChecker.cpp" - "api/ApiAuthChecker.h" "api/ApiBootstrap.cpp" "api/ApiBootstrap.h" "api/ApiException.h" @@ -43,9 +41,24 @@ set(api "api/ApiResponseFactory.h" "api/ApiResponseValidator.cpp" "api/ApiResponseValidator.h" - "api/LocateApiSettings.cpp" - "api/LocateApiSettings.h" -) + api/ApiRequestFactory.cpp api/ApiRequestFactory.h + api/ApiSettings.cpp api/ApiSettings.h api/ApiRequestFactoryInterface.h + api/ApiRequestPerformerInterface.h + api/Response.cpp api/Response.h + api/ApiRequestData.h api/ApiRequestData.cpp + api/CurlApiRequestPerformer.cpp api/CurlApiRequestPerformer.h + api/ApiUrlBuilder.cpp api/ApiUrlBuilder.h + api/ApiHeaderApplicator.cpp api/ApiHeaderApplicator.h + api/ApiCurlRequestFactory.cpp api/ApiCurlRequestFactory.h + api/ApiRequest.cpp api/ApiRequest.h + api/ApiFactory.cpp api/ApiFactory.h + api/AbstractApiRequestPerformerFactory.h api/CurlApiRequestPerformerFactory.cpp + api/CurlApiRequestPerformerFactory.h + api/ChainableRequest.cpp api/ChainableRequest.h + api/ApiSettingsProviderInterface.h + api/ConfigApiSettingsProvider.cpp api/ConfigApiSettingsProvider.h + api/ApiRequestFacade.cpp api/ApiRequestFacade.h + api/ApiRequestException.cpp api/ApiRequestException.h) source_group("api" FILES ${api}) set(curl @@ -57,7 +70,7 @@ set(curl "curl/CurlResponse.cpp" "curl/CurlResponse.h" "curl/HttpException.h" - curl/CurlInterface.cpp) + curl/CurlInterface.cpp) source_group("curl" FILES ${curl}) set(data @@ -89,6 +102,12 @@ set(helper ) source_group("helper" FILES ${helper}) +set(http + http/HttpStatusCode.h + http/HttpMethod.h + http/HttpMethod.cpp) +source_group("http" FILES ${http}) + set(log "log/LoggerBootstrap.cpp" "log/LoggerBootstrap.h" @@ -108,8 +127,9 @@ set(setting "setting/SettingRepository.h" "setting/SettingRepositoryFactory.cpp" "setting/SettingRepositoryFactory.h" - "setting/SettingValue.h" -) + setting/SettingRepositoryInterface.h + setting/SettingProviderInterface.h + setting/JsonFileSettingProvider.cpp setting/JsonFileSettingProvider.h) source_group("setting" FILES ${setting}) set(squawk @@ -124,6 +144,10 @@ set(srd ) source_group("srd" FILES ${srd}) +set(string + string/StringTrimFunctions.cpp string/StringTrimFunctions.h) +source_group("string" FILES ${string}) + set(task "../utils/task/TaskRunner.cpp" "../utils/task/TaskRunner.h" @@ -158,15 +182,16 @@ set(ALL_FILES ${dialog} ${duplicate} ${helper} + ${http} ${log} ${pch} ${setting} ${squawk} ${srd} + ${string} ${task} ${update} - ${windows} -) + ${windows}) ################################################################################ # Target @@ -204,6 +229,8 @@ target_include_directories(${PROJECT_NAME} PUBLIC target_include_directories(${PROJECT_NAME} SYSTEM PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../../third_party" + "${CMAKE_CURRENT_SOURCE_DIR}/../../third_party/continuable/include" + "${CMAKE_CURRENT_SOURCE_DIR}/../../third_party/function2/include" ) ################################################################################ diff --git a/src/utils/api/AbstractApiRequestPerformerFactory.h b/src/utils/api/AbstractApiRequestPerformerFactory.h new file mode 100644 index 000000000..ca8315a83 --- /dev/null +++ b/src/utils/api/AbstractApiRequestPerformerFactory.h @@ -0,0 +1,17 @@ +#pragma once + +namespace UKControllerPluginUtils::Api { + class ApiRequestPerformerInterface; + class ApiSettings; + + /** + * Builds API request performers, allowing them to be subbed out for + * mocks in tests etc. + */ + class AbstractApiRequestPerformerFactory + { + public: + virtual ~AbstractApiRequestPerformerFactory() = default; + [[nodiscard]] virtual auto Make(const ApiSettings& apiSettings) -> ApiRequestPerformerInterface& = 0; + }; +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/ApiBootstrap.cpp b/src/utils/api/ApiBootstrap.cpp index c59b06828..010dc9ac8 100644 --- a/src/utils/api/ApiBootstrap.cpp +++ b/src/utils/api/ApiBootstrap.cpp @@ -1,21 +1,44 @@ -#include "pch/pch.h" -#include "api/ApiBootstrap.h" -#include "api/ApiHelper.h" -#include "api/ApiRequestBuilder.h" +#include "ApiBootstrap.h" +#include "ApiFactory.h" +#include "ApiHelper.h" +#include "ApiSettings.h" +#include "ConfigApiSettingsProvider.h" +#include "CurlApiRequestPerformerFactory.h" #include "curl/CurlApi.h" #include "setting/SettingRepository.h" +#include "setting/JsonFileSettingProvider.h" -namespace UKControllerPlugin { - namespace Api { - std::unique_ptr Bootstrap(const Setting::SettingRepository& settings, Curl::CurlInterface& curl) - { - std::string apiUrl = settings.GetSetting("api-url", "https://ukcp.vatsim.uk"); - LogInfo("API bootstrapped with URL " + apiUrl); - ApiRequestBuilder requestBuilder( - apiUrl, - settings.GetSetting("api-key") - ); - return std::make_unique(curl, requestBuilder); - } - } // namespace Api -} // namespace UKControllerPlugin +using UKControllerPlugin::Api::ApiHelper; +using UKControllerPlugin::Api::ApiInterface; +using UKControllerPlugin::Curl::CurlApi; +using UKControllerPlugin::Curl::CurlInterface; +using UKControllerPlugin::Setting::JsonFileSettingProvider; +using UKControllerPlugin::Setting::SettingRepository; +using UKControllerPlugin::Windows::WinApiInterface; + +namespace UKControllerPluginUtils::Api { + + /** + * Bootstrap the "new" way of doing the API + */ + auto Bootstrap(SettingRepository& settingRepository, WinApiInterface& windows) -> std::shared_ptr + { + settingRepository.AddProvider(std::make_shared( + L"api-settings.json", std::set{"api-key", "api-url"}, windows)); + + auto factory = std::make_shared( + std::make_shared(settingRepository, windows), + std::make_shared(std::make_unique())); + + SetApiRequestFactory(factory); + return factory; + } + + /** + * Bootstrap the "legacy" APIInterface + */ + auto BootstrapLegacy(ApiFactory& factory, CurlInterface& curl) -> std::unique_ptr + { + return std::make_unique(curl, factory.LegacyRequestBuilder()); + } +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/ApiBootstrap.h b/src/utils/api/ApiBootstrap.h index 45f03f987..c50e5dfcb 100644 --- a/src/utils/api/ApiBootstrap.h +++ b/src/utils/api/ApiBootstrap.h @@ -2,18 +2,27 @@ #include "api/ApiInterface.h" namespace UKControllerPlugin { + namespace Api { + class ApiInterface; + } // namespace Api namespace Setting { class SettingRepository; } // namespace Setting namespace Curl { class CurlInterface; - } // namespace Curl + } // namespace Curl + namespace Windows { + class WinApiInterface; + } // namespace Windows +} // namespace UKControllerPlugin - namespace Api { +namespace UKControllerPluginUtils::Api { + class ApiFactory; - std::unique_ptr Bootstrap( - const Setting::SettingRepository& settings, - Curl::CurlInterface& curl - ); - } // namespace Api -} // namespace UKControllerPlugin + [[nodiscard]] auto Bootstrap( + UKControllerPlugin::Setting::SettingRepository& settingRepository, + UKControllerPlugin::Windows::WinApiInterface& windows) -> std::shared_ptr; + + [[nodiscard]] auto BootstrapLegacy(ApiFactory& factory, UKControllerPlugin::Curl::CurlInterface& curl) + -> std::unique_ptr; +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/ApiCurlRequestFactory.cpp b/src/utils/api/ApiCurlRequestFactory.cpp new file mode 100644 index 000000000..f65128ea6 --- /dev/null +++ b/src/utils/api/ApiCurlRequestFactory.cpp @@ -0,0 +1,22 @@ +#include "ApiCurlRequestFactory.h" +#include "ApiHeaderApplicator.h" +#include "ApiRequestData.h" +#include "ApiUrlBuilder.h" + +using UKControllerPlugin::Curl::CurlRequest; + +namespace UKControllerPluginUtils::Api { + + ApiCurlRequestFactory::ApiCurlRequestFactory( + const ApiUrlBuilder& urlBuilder, const ApiHeaderApplicator& headerApplicator) + : urlBuilder(urlBuilder), headerApplicator(headerApplicator) + { + } + + auto ApiCurlRequestFactory::BuildCurlRequest(const ApiRequestData& data) const -> CurlRequest + { + CurlRequest request(urlBuilder.BuildUrl(data), data.Method()); + headerApplicator.ApplyHeaders(request); + return request; + } +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/ApiCurlRequestFactory.h b/src/utils/api/ApiCurlRequestFactory.h new file mode 100644 index 000000000..e360a0ce3 --- /dev/null +++ b/src/utils/api/ApiCurlRequestFactory.h @@ -0,0 +1,25 @@ +#pragma once +#include "curl/CurlRequest.h" + +namespace UKControllerPluginUtils::Api { + class ApiRequestData; + class ApiUrlBuilder; + class ApiHeaderApplicator; + + /** + * Creates a cURL request from an ApiRequest + */ + class ApiCurlRequestFactory + { + public: + ApiCurlRequestFactory(const ApiUrlBuilder& urlBuilder, const ApiHeaderApplicator& headerApplicator); + [[nodiscard]] auto BuildCurlRequest(const ApiRequestData& data) const -> UKControllerPlugin::Curl::CurlRequest; + + private: + // Builds URLs + const ApiUrlBuilder& urlBuilder; + + // Applies headers + const ApiHeaderApplicator& headerApplicator; + }; +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/ApiFactory.cpp b/src/utils/api/ApiFactory.cpp new file mode 100644 index 000000000..73fe67719 --- /dev/null +++ b/src/utils/api/ApiFactory.cpp @@ -0,0 +1,44 @@ +#include "AbstractApiRequestPerformerFactory.h" +#include "ApiFactory.h" +#include "ApiRequestBuilder.h" +#include "ApiRequestFactory.h" +#include "ApiSettings.h" +#include "ApiSettingsProviderInterface.h" + +using UKControllerPlugin::Api::ApiRequestBuilder; + +namespace UKControllerPluginUtils::Api { + + ApiFactory::ApiFactory( + std::shared_ptr settingsProvider, + std::shared_ptr requestPerformerFactory) + : settingsProvider(settingsProvider), requestPerformerFactory(std::move(requestPerformerFactory)) + { + } + + ApiFactory::~ApiFactory() = default; + + auto ApiFactory::SettingsProvider() -> const std::shared_ptr + { + return settingsProvider; + } + + auto ApiFactory::RequestFactory() -> ApiRequestFactory& + { + if (requestFactory == nullptr) { + requestFactory = + std::make_unique(requestPerformerFactory->Make(SettingsProvider()->Get())); + } + + return *requestFactory; + } + + auto ApiFactory::LegacyRequestBuilder() -> const ApiRequestBuilder& + { + if (legacyRequestBuilder == nullptr) { + legacyRequestBuilder = std::make_unique(SettingsProvider()->Get()); + } + + return *legacyRequestBuilder; + } +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/ApiFactory.h b/src/utils/api/ApiFactory.h new file mode 100644 index 000000000..f4d047b98 --- /dev/null +++ b/src/utils/api/ApiFactory.h @@ -0,0 +1,46 @@ +#pragma once + +namespace UKControllerPlugin { + namespace Api { + class ApiRequestBuilder; + } // namespace Api + namespace Curl { + class CurlInterface; + } // namespace Curl +} // namespace UKControllerPlugin + +namespace UKControllerPluginUtils::Api { + class AbstractApiRequestPerformerFactory; + class ApiRequestFactory; + class ApiSettings; + class ApiSettingsProviderInterface; + + /** + * Bootstraps and builds the API. Keeps objects alive throughout + * the plugins lifetime. + */ + class ApiFactory + { + public: + ApiFactory( + std::shared_ptr settingsProvider, + std::shared_ptr requestPerformerFactory); + ~ApiFactory(); + [[nodiscard]] auto LegacyRequestBuilder() -> const UKControllerPlugin::Api::ApiRequestBuilder&; + [[nodiscard]] auto RequestFactory() -> ApiRequestFactory&; + [[nodiscard]] auto SettingsProvider() -> const std::shared_ptr; + + private: + // Loads api settings - can be subbed out for a mock. + std::shared_ptr settingsProvider; + + // Starts performing requests - can be subbed out for a mock. + std::shared_ptr requestPerformerFactory; + + // Builds API requests + std::unique_ptr requestFactory; + + // Builds API requests, the legacy way + std::unique_ptr legacyRequestBuilder; + }; +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/ApiHeaderApplicator.cpp b/src/utils/api/ApiHeaderApplicator.cpp new file mode 100644 index 000000000..3bf8a870c --- /dev/null +++ b/src/utils/api/ApiHeaderApplicator.cpp @@ -0,0 +1,19 @@ +#include "ApiHeaderApplicator.h" +#include "ApiSettings.h" +#include "curl/CurlRequest.h" + +using UKControllerPlugin::Curl::CurlRequest; + +namespace UKControllerPluginUtils::Api { + + ApiHeaderApplicator::ApiHeaderApplicator(const ApiSettings& settings) : settings(settings) + { + } + + void ApiHeaderApplicator::ApplyHeaders(CurlRequest& request) const + { + request.AddHeader("Authorization", "Bearer " + settings.Key()); + request.AddHeader("Accept", "application/json"); + request.AddHeader("Content-Type", "application/json"); + } +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/ApiHeaderApplicator.h b/src/utils/api/ApiHeaderApplicator.h new file mode 100644 index 000000000..597fcd0a4 --- /dev/null +++ b/src/utils/api/ApiHeaderApplicator.h @@ -0,0 +1,23 @@ +#pragma once + +namespace UKControllerPlugin::Curl { + class CurlRequest; +} // namespace UKControllerPlugin::Curl + +namespace UKControllerPluginUtils::Api { + class ApiSettings; + + /** + * Applies necessary headers to the API requests. + */ + class ApiHeaderApplicator + { + public: + ApiHeaderApplicator(const ApiSettings& settings); + void ApplyHeaders(UKControllerPlugin::Curl::CurlRequest& request) const; + + private: + // Stores API settings including the key + const ApiSettings& settings; + }; +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/ApiHelper.cpp b/src/utils/api/ApiHelper.cpp index c661d53a0..b5f575c56 100644 --- a/src/utils/api/ApiHelper.cpp +++ b/src/utils/api/ApiHelper.cpp @@ -148,22 +148,6 @@ namespace UKControllerPlugin::Api { this->MakeApiRequest(this->requestBuilder.BuildSquawkAssignmentCheckRequest(callsign)), callsign); } - /* - Returns the API domain being used by the request builder - */ - auto ApiHelper::GetApiDomain() const -> std::string - { - return this->requestBuilder.GetApiDomain(); - } - - /* - Returns the API key being used to authenticate requests - */ - auto ApiHelper::GetApiKey() const -> std::string - { - return this->requestBuilder.GetApiKey(); - } - auto ApiHelper::GetDependencyList() const -> nlohmann::json { return this->MakeApiRequest(this->requestBuilder.BuildDependencyListRequest()).GetRawData(); @@ -207,7 +191,7 @@ namespace UKControllerPlugin::Api { auto ApiHelper::GetUri(std::string uri) const -> nlohmann::json { - if (uri.find(this->GetApiDomain()) == std::string::npos) { + if (uri.find(this->requestBuilder.GetApiDomain()) == std::string::npos) { LogCritical("Attempted to get URI on non-ukcp route"); throw ApiException("Attempted to get URI on non-ukcp route"); } @@ -327,22 +311,6 @@ namespace UKControllerPlugin::Api { static_cast(this->MakeApiRequest(this->requestBuilder.BuildReadNotificationRequest(id))); } - /* - Set api key on the request builder - */ - void ApiHelper::SetApiKey(std::string key) - { - this->requestBuilder.SetApiKey(key); - } - - /* - Set api domain on the request builder - */ - void ApiHelper::SetApiDomain(std::string domain) - { - this->requestBuilder.SetApiDomain(domain); - } - auto ApiHelper::CreatePrenoteMessage( const std::string& callsign, const std::string& departureAirfield, diff --git a/src/utils/api/ApiHelper.h b/src/utils/api/ApiHelper.h index 101a844cf..d0d245000 100644 --- a/src/utils/api/ApiHelper.h +++ b/src/utils/api/ApiHelper.h @@ -29,8 +29,6 @@ namespace UKControllerPlugin::Api { [[nodiscard]] auto FetchRemoteFile(std::string uri) const -> std::string override; [[nodiscard]] auto GetAssignedSquawk(std::string callsign) const -> UKControllerPlugin::Squawk::ApiSquawkAllocation override; - [[nodiscard]] auto GetApiDomain() const -> std::string override; - [[nodiscard]] auto GetApiKey() const -> std::string override; [[nodiscard]] auto GetDependencyList() const -> nlohmann::json override; [[nodiscard]] auto GetHoldDependency() const -> nlohmann::json override; [[nodiscard]] auto GetAssignedHolds() const -> nlohmann::json override; @@ -74,8 +72,6 @@ namespace UKControllerPlugin::Api { -> nlohmann::json override; void CancelDepartureReleaseRequest(int releaseId) const override; void ReadNotification(int id) const override; - void SetApiKey(std::string key) override; - void SetApiDomain(std::string domain) override; [[nodiscard]] auto CreatePrenoteMessage( const std::string& callsign, const std::string& departureAirfield, diff --git a/src/utils/api/ApiInterface.h b/src/utils/api/ApiInterface.h index ca1f31b16..6622b5b9b 100644 --- a/src/utils/api/ApiInterface.h +++ b/src/utils/api/ApiInterface.h @@ -28,8 +28,6 @@ namespace UKControllerPlugin::Api { [[nodiscard]] virtual auto FetchRemoteFile(std::string uri) const -> std::string = 0; [[nodiscard]] virtual auto GetAssignedSquawk(std::string callsign) const -> UKControllerPlugin::Squawk::ApiSquawkAllocation = 0; - [[nodiscard]] virtual auto GetApiDomain() const -> std::string = 0; - [[nodiscard]] virtual auto GetApiKey() const -> std::string = 0; [[nodiscard]] virtual auto GetHoldDependency() const -> nlohmann::json = 0; [[nodiscard]] virtual auto GetAssignedHolds() const -> nlohmann::json = 0; virtual void AssignAircraftToHold(std::string callsign, std::string navaid) const = 0; @@ -86,8 +84,5 @@ namespace UKControllerPlugin::Api { [[nodiscard]] virtual auto CreateMissedApproach(const std::string& callsign) const -> nlohmann::json = 0; virtual void AcknowledgeMissedApproach(int id, const std::string& remarks) const = 0; [[nodiscard]] virtual auto GetAllMetars() const -> nlohmann::json = 0; - - virtual void SetApiKey(std::string key) = 0; - virtual void SetApiDomain(std::string domain) = 0; }; } // namespace UKControllerPlugin::Api diff --git a/src/utils/api/ApiRequest.cpp b/src/utils/api/ApiRequest.cpp new file mode 100644 index 000000000..7ca7184e5 --- /dev/null +++ b/src/utils/api/ApiRequest.cpp @@ -0,0 +1,68 @@ +#include "ApiRequest.h" +#include "ApiRequestException.h" +#include "ApiRequestPerformerInterface.h" +#include "ChainableRequest.h" + +namespace UKControllerPluginUtils::Api { + + ApiRequest::ApiRequest( + const ApiRequestData& data, + ApiRequestPerformerInterface& performer, + std::function onCompletionHandler) + : chain(std::make_shared(data, performer, onCompletionHandler)) + { + } + + ApiRequest::~ApiRequest() + { + // Not dealing with an exception means we end up in an "everything blows up" situation. + // So have a default handler for API exceptions. + chain->Catch([](const ApiRequestException& exception) { + if (exception.StatusCode() != Http::HttpStatusCode::Ok && + exception.StatusCode() != Http::HttpStatusCode::Created && + exception.StatusCode() != Http::HttpStatusCode::NoContent) { + LogError( + "Unhandled API exception when making request to uri " + exception.Uri() + ". Status code was " + + std::to_string(static_cast(exception.StatusCode()))); + } else if (exception.InvalidJson()) { + LogError( + "Unhandled API exception when making request to uri " + exception.Uri() + + ". Response was invalid " + "JSON"); + } else { + LogError("Unknown unhandled API Exception when making request to uri " + exception.Uri()); + } + }); + + if (!async) { + try { + chain->Await(); + } catch (...) { + // No rethrow + } + } + } + + auto ApiRequest::Then(const std::function& function) -> ApiRequest& + { + chain->Then(function); + return *this; + } + + auto ApiRequest::Then(const std::function& function) -> ApiRequest& + { + chain->Then(function); + return *this; + } + + auto ApiRequest::Catch(const std::function& function) -> ApiRequest& + { + chain->Catch(function); + return *this; + } + + void ApiRequest::Await() + { + this->async = false; + } +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/ApiRequest.h b/src/utils/api/ApiRequest.h new file mode 100644 index 000000000..632813633 --- /dev/null +++ b/src/utils/api/ApiRequest.h @@ -0,0 +1,34 @@ +#pragma once +#include "ApiRequestData.h" +#include "Response.h" + +namespace UKControllerPluginUtils::Api { + class ApiRequestPerformerInterface; + class ApiRequestException; + class ChainableRequest; + + class ApiRequest + { + public: + ApiRequest( + const ApiRequestData& data, + ApiRequestPerformerInterface& performer, + std::function onCompletionHandler); + ~ApiRequest(); + ApiRequest(ApiRequest&&) = delete; + ApiRequest(const ApiRequest&) = delete; + auto operator=(const ApiRequest&) = delete; + auto operator=(ApiRequest&&) -> ApiRequest& = delete; + auto Then(const std::function& function) -> ApiRequest&; + auto Then(const std::function& function) -> ApiRequest&; + auto Catch(const std::function& function) -> ApiRequest&; + void Await(); + + private: + // Allows continuation + std::shared_ptr chain; + + // Run on main thread or async + bool async = true; + }; +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/ApiRequestBuilder.cpp b/src/utils/api/ApiRequestBuilder.cpp index 6bef4295c..a1a2f08d2 100644 --- a/src/utils/api/ApiRequestBuilder.cpp +++ b/src/utils/api/ApiRequestBuilder.cpp @@ -1,11 +1,12 @@ #include "api/ApiRequestBuilder.h" +#include "api/ApiSettings.h" using UKControllerPlugin::Curl::CurlRequest; using UKControllerPlugin::Srd::SrdSearchParameters; +using UKControllerPluginUtils::Api::ApiSettings; namespace UKControllerPlugin::Api { - ApiRequestBuilder::ApiRequestBuilder(std::string apiDomain, std::string apiKey) - : apiDomain(std::move(apiDomain)), apiKey(std::move(apiKey)) + ApiRequestBuilder::ApiRequestBuilder(const ApiSettings& settings) : settings(settings) { } @@ -14,7 +15,7 @@ namespace UKControllerPlugin::Api { */ auto ApiRequestBuilder::AddCommonHeaders(CurlRequest request) const -> CurlRequest { - request.AddHeader("Authorization", "Bearer " + this->apiKey); + request.AddHeader("Authorization", "Bearer " + this->settings.Key()); request.AddHeader("Accept", "application/json"); request.AddHeader("Content-Type", "application/json"); return request; @@ -25,7 +26,7 @@ namespace UKControllerPlugin::Api { */ auto ApiRequestBuilder::BuildAuthCheckRequest() const -> CurlRequest { - return this->AddCommonHeaders(CurlRequest(apiDomain + "/authorise", CurlRequest::METHOD_GET)); + return this->AddCommonHeaders(CurlRequest(this->BuildUrl("/authorise"), CurlRequest::METHOD_GET)); } /* @@ -33,7 +34,7 @@ namespace UKControllerPlugin::Api { */ auto ApiRequestBuilder::BuildDependencyListRequest() const -> CurlRequest { - return this->AddCommonHeaders(CurlRequest(apiDomain + "/dependency", CurlRequest::METHOD_GET)); + return this->AddCommonHeaders(CurlRequest(this->BuildUrl("/dependency"), CurlRequest::METHOD_GET)); } /* @@ -62,7 +63,7 @@ namespace UKControllerPlugin::Api { */ auto ApiRequestBuilder::BuildMinStackLevelRequest() const -> CurlRequest { - return this->AddCommonHeaders(CurlRequest(apiDomain + "/msl", CurlRequest::METHOD_GET)); + return this->AddCommonHeaders(CurlRequest(this->BuildUrl("/msl"), CurlRequest::METHOD_GET)); } /* @@ -70,7 +71,7 @@ namespace UKControllerPlugin::Api { */ auto ApiRequestBuilder::BuildRegionalPressureRequest() const -> CurlRequest { - return this->AddCommonHeaders(CurlRequest(apiDomain + "/regional-pressure", CurlRequest::METHOD_GET)); + return this->AddCommonHeaders(CurlRequest(this->BuildUrl("/regional-pressure"), CurlRequest::METHOD_GET)); } /* @@ -78,7 +79,7 @@ namespace UKControllerPlugin::Api { */ auto ApiRequestBuilder::BuildSrdQueryRequest(const SrdSearchParameters& parameters) const -> CurlRequest { - std::string uri = apiDomain + "/srd/route/search?"; + std::string uri = BuildUrl("/srd/route/search?"); uri += "origin=" + parameters.origin; uri += "&destination=" + parameters.destination; @@ -94,7 +95,7 @@ namespace UKControllerPlugin::Api { */ auto ApiRequestBuilder::BuildGetStandAssignmentsRequest() const -> CurlRequest { - return this->AddCommonHeaders(CurlRequest(apiDomain + "/stand/assignment", CurlRequest::METHOD_GET)); + return this->AddCommonHeaders(CurlRequest(BuildUrl("/stand/assignment"), CurlRequest::METHOD_GET)); } /* @@ -103,7 +104,7 @@ namespace UKControllerPlugin::Api { auto ApiRequestBuilder::BuildAssignStandToAircraftRequest(const std::string& callsign, int standId) const -> CurlRequest { - CurlRequest request(apiDomain + "/stand/assignment", CurlRequest::METHOD_PUT); + CurlRequest request(BuildUrl("/stand/assignment"), CurlRequest::METHOD_PUT); nlohmann::json body; body["callsign"] = callsign; body["stand_id"] = standId; @@ -119,7 +120,7 @@ namespace UKControllerPlugin::Api { -> CurlRequest { return this->AddCommonHeaders( - CurlRequest(apiDomain + "/stand/assignment/" + callsign, CurlRequest::METHOD_DELETE)); + CurlRequest(BuildUrl("/stand/assignment/" + callsign), CurlRequest::METHOD_DELETE)); } /* @@ -127,8 +128,7 @@ namespace UKControllerPlugin::Api { */ auto ApiRequestBuilder::BuildSquawkAssignmentCheckRequest(const std::string& callsign) const -> CurlRequest { - return this->AddCommonHeaders( - CurlRequest(apiDomain + "/squawk-assignment/" + callsign, CurlRequest::METHOD_GET)); + return this->AddCommonHeaders(CurlRequest(BuildUrl("/squawk-assignment/" + callsign), CurlRequest::METHOD_GET)); } /* @@ -137,7 +137,7 @@ namespace UKControllerPlugin::Api { auto ApiRequestBuilder::BuildSquawkAssignmentDeletionRequest(const std::string& callsign) const -> CurlRequest { return this->AddCommonHeaders( - CurlRequest(apiDomain + "/squawk-assignment/" + callsign, CurlRequest::METHOD_DELETE)); + CurlRequest(BuildUrl("/squawk-assignment/" + callsign), CurlRequest::METHOD_DELETE)); } /* @@ -146,7 +146,7 @@ namespace UKControllerPlugin::Api { auto ApiRequestBuilder::BuildLocalSquawkAssignmentRequest( const std::string& callsign, const std::string& unit, const std::string& flightRules) const -> CurlRequest { - CurlRequest request(apiDomain + "/squawk-assignment/" + callsign, CurlRequest::METHOD_PUT); + CurlRequest request(BuildUrl("/squawk-assignment/" + callsign), CurlRequest::METHOD_PUT); nlohmann::json body; body["type"] = this->localSquawkAssignmentType; @@ -164,7 +164,7 @@ namespace UKControllerPlugin::Api { auto ApiRequestBuilder::BuildGeneralSquawkAssignmentRequest( const std::string& callsign, const std::string& origin, const std::string& destination) const -> CurlRequest { - CurlRequest request(apiDomain + "/squawk-assignment/" + callsign, CurlRequest::METHOD_PUT); + CurlRequest request(BuildUrl("/squawk-assignment/" + callsign), CurlRequest::METHOD_PUT); nlohmann::json body; body["type"] = this->generalSquawkAssignmentType; @@ -181,7 +181,7 @@ namespace UKControllerPlugin::Api { */ auto ApiRequestBuilder::BuildHoldDependencyRequest() const -> CurlRequest { - return this->AddCommonHeaders(CurlRequest(apiDomain + "/hold", CurlRequest::METHOD_GET)); + return this->AddCommonHeaders(CurlRequest(BuildUrl("/hold"), CurlRequest::METHOD_GET)); } /* @@ -189,7 +189,7 @@ namespace UKControllerPlugin::Api { */ auto ApiRequestBuilder::BuildAllAssignedHoldsRequest() const -> CurlRequest { - return this->AddCommonHeaders(CurlRequest(apiDomain + "/hold/assigned", CurlRequest::METHOD_GET)); + return this->AddCommonHeaders(CurlRequest(BuildUrl("/hold/assigned"), CurlRequest::METHOD_GET)); } /* @@ -197,7 +197,7 @@ namespace UKControllerPlugin::Api { */ auto ApiRequestBuilder::BuildSetAssignedHoldRequest(std::string callsign, std::string navaid) const -> CurlRequest { - CurlRequest request(this->apiDomain + "/hold/assigned", CurlRequest::METHOD_PUT); + CurlRequest request(this->BuildUrl("/hold/assigned"), CurlRequest::METHOD_PUT); nlohmann::json data{{"callsign", callsign}, {"navaid", navaid}}; request.SetBody(data.dump()); @@ -209,8 +209,7 @@ namespace UKControllerPlugin::Api { */ auto ApiRequestBuilder::BuildDeleteAssignedHoldRequest(const std::string& callsign) const -> CurlRequest { - return this->AddCommonHeaders( - CurlRequest(apiDomain + "/hold/assigned/" + callsign, CurlRequest::METHOD_DELETE)); + return this->AddCommonHeaders(CurlRequest(BuildUrl("/hold/assigned/" + callsign), CurlRequest::METHOD_DELETE)); } auto ApiRequestBuilder::BuildEnrouteReleaseRequestWithReleasePoint( @@ -220,7 +219,7 @@ namespace UKControllerPlugin::Api { int releaseType, std::string releasePoint) const -> CurlRequest { - CurlRequest request(this->apiDomain + "/release/enroute", CurlRequest::METHOD_POST); + CurlRequest request(this->BuildUrl("/release/enroute"), CurlRequest::METHOD_POST); nlohmann::json data{ {"callsign", aircraftCallsign}, {"type", releaseType}, @@ -235,36 +234,35 @@ namespace UKControllerPlugin::Api { auto ApiRequestBuilder::BuildGetAllNotificationsRequest() const -> CurlRequest { - return this->AddCommonHeaders(CurlRequest(this->apiDomain + "/notifications", CurlRequest::METHOD_GET)); + return this->AddCommonHeaders(CurlRequest(this->BuildUrl("/notifications"), CurlRequest::METHOD_GET)); } auto ApiRequestBuilder::BuildGetUnreadNotificationsRequest() const -> CurlRequest { - return this->AddCommonHeaders(CurlRequest(this->apiDomain + "/notifications/unread", CurlRequest::METHOD_GET)); + return this->AddCommonHeaders(CurlRequest(this->BuildUrl("/notifications/unread"), CurlRequest::METHOD_GET)); } auto ApiRequestBuilder::BuildReadNotificationRequest(int id) const -> CurlRequest { return this->AddCommonHeaders( - CurlRequest(this->apiDomain + "/notifications/read/" + std::to_string(id), CurlRequest::METHOD_PUT)); + CurlRequest(this->BuildUrl("/notifications/read/" + std::to_string(id)), CurlRequest::METHOD_PUT)); } auto ApiRequestBuilder::BuildLatestGithubVersionRequest(const std::string& releaseChannel) const -> CurlRequest { return this->AddCommonHeaders( - CurlRequest(this->apiDomain + "/version/latest?channel=" + releaseChannel, CurlRequest::METHOD_GET)); + CurlRequest(this->BuildUrl("/version/latest?channel=" + releaseChannel), CurlRequest::METHOD_GET)); } auto ApiRequestBuilder::BuildPluginEventSyncRequest() const -> CurlRequest { - return this->AddCommonHeaders(CurlRequest(this->apiDomain + "/plugin-events/sync", CurlRequest::METHOD_GET)); + return this->AddCommonHeaders(CurlRequest(this->BuildUrl("/plugin-events/sync"), CurlRequest::METHOD_GET)); } auto ApiRequestBuilder::BuildGetLatestPluginEventsRequest(int lastEventId) const -> CurlRequest { return this->AddCommonHeaders(CurlRequest( - this->apiDomain + "/plugin-events/recent?previous=" + std::to_string(lastEventId), - CurlRequest::METHOD_GET)); + this->BuildUrl("/plugin-events/recent?previous=" + std::to_string(lastEventId)), CurlRequest::METHOD_GET)); } auto ApiRequestBuilder::BuildAcknowledgeDepartureReleaseRequest(int releaseId, int controllerPositionId) const @@ -274,7 +272,7 @@ namespace UKControllerPlugin::Api { body["controller_position_id"] = controllerPositionId; CurlRequest request( - this->apiDomain + "/departure/release/request/" + std::to_string(releaseId) + "/acknowledge", + this->BuildUrl("/departure/release/request/" + std::to_string(releaseId) + "/acknowledge"), CurlRequest::METHOD_PATCH); request.SetBody(body.dump()); return this->AddCommonHeaders(request); @@ -288,7 +286,7 @@ namespace UKControllerPlugin::Api { body["remarks"] = remarks; CurlRequest request( - this->apiDomain + "/departure/release/request/" + std::to_string(releaseId) + "/reject", + this->BuildUrl("/departure/release/request/" + std::to_string(releaseId) + "/reject"), CurlRequest::METHOD_PATCH); request.SetBody(body.dump()); return this->AddCommonHeaders(request); @@ -315,7 +313,7 @@ namespace UKControllerPlugin::Api { } CurlRequest request( - this->apiDomain + "/departure/release/request/" + std::to_string(releaseId) + "/approve", + this->BuildUrl("/departure/release/request/" + std::to_string(releaseId) + "/approve"), CurlRequest::METHOD_PATCH); request.SetBody(body.dump()); return this->AddCommonHeaders(request); @@ -325,7 +323,7 @@ namespace UKControllerPlugin::Api { const std::string& callsign, int requestingControllerId, int targetController, int expiresInSeconds) const -> CurlRequest { - CurlRequest request(this->apiDomain + "/departure/release/request", CurlRequest::METHOD_POST); + CurlRequest request(this->BuildUrl("/departure/release/request"), CurlRequest::METHOD_POST); nlohmann::json body; body["callsign"] = callsign; @@ -340,7 +338,7 @@ namespace UKControllerPlugin::Api { auto ApiRequestBuilder::BuildCancelReleaseRequest(int releaseId) const -> CurlRequest { return this->AddCommonHeaders(CurlRequest( - this->apiDomain + "/departure/release/request/" + std::to_string(releaseId), CurlRequest::METHOD_DELETE)); + this->BuildUrl("/departure/release/request/" + std::to_string(releaseId)), CurlRequest::METHOD_DELETE)); } auto ApiRequestBuilder::BuildEnrouteReleaseRequest( @@ -349,7 +347,7 @@ namespace UKControllerPlugin::Api { std::string targetController, int releaseType) const -> CurlRequest { - CurlRequest request(this->apiDomain + "/release/enroute", CurlRequest::METHOD_POST); + CurlRequest request(this->BuildUrl("/release/enroute"), CurlRequest::METHOD_POST); nlohmann::json data{ {"callsign", aircraftCallsign}, {"type", releaseType}, @@ -361,38 +359,6 @@ namespace UKControllerPlugin::Api { return this->AddCommonHeaders(request); } - /* - Returns the API Domain that the builder is using - */ - auto ApiRequestBuilder::GetApiDomain() const -> std::string - { - return this->apiDomain; - } - - /* - Returns the API key that is used to authorize requests - */ - auto ApiRequestBuilder::GetApiKey() const -> std::string - { - return this->apiKey; - } - - /* - Set the API key - */ - void ApiRequestBuilder::SetApiDomain(std::string domain) - { - this->apiDomain = std::move(domain); - } - - /* - Set the API domain - */ - void ApiRequestBuilder::SetApiKey(std::string key) - { - this->apiKey = std::move(key); - } - auto ApiRequestBuilder::BuildCreatePrenoteMessageRequest( const std::string& callsign, const std::string& departureAirfield, @@ -402,7 +368,7 @@ namespace UKControllerPlugin::Api { int targetController, int requestExpiry) const -> UKControllerPlugin::Curl::CurlRequest { - CurlRequest request(this->apiDomain + "/prenotes/messages", CurlRequest::METHOD_POST); + CurlRequest request(this->BuildUrl("/prenotes/messages"), CurlRequest::METHOD_POST); nlohmann::json data{ {"callsign", callsign}, {"departure_airfield", departureAirfield}, @@ -424,7 +390,7 @@ namespace UKControllerPlugin::Api { -> UKControllerPlugin::Curl::CurlRequest { CurlRequest request( - this->apiDomain + "/prenotes/messages/" + std::to_string(messageId) + "/acknowledge", + this->BuildUrl("/prenotes/messages/" + std::to_string(messageId) + "/acknowledge"), CurlRequest::METHOD_PATCH); nlohmann::json data{ {"controller_position_id", controllerId}, @@ -437,13 +403,13 @@ namespace UKControllerPlugin::Api { -> UKControllerPlugin::Curl::CurlRequest { return this->AddCommonHeaders( - {this->apiDomain + "/prenotes/messages/" + std::to_string(messageId), CurlRequest::METHOD_DELETE}); + {this->BuildUrl("/prenotes/messages/" + std::to_string(messageId)), CurlRequest::METHOD_DELETE}); } auto ApiRequestBuilder::BuildMissedApproachMessage(const std::string& callsign) const -> UKControllerPlugin::Curl::CurlRequest { - CurlRequest request(this->apiDomain + "/missed-approaches", CurlRequest::METHOD_POST); + CurlRequest request(this->BuildUrl("/missed-approaches"), CurlRequest::METHOD_POST); nlohmann::json data{ {"callsign", callsign}, }; @@ -455,7 +421,7 @@ namespace UKControllerPlugin::Api { auto ApiRequestBuilder::BuildMissedApproachAcknowledgeMessage(int id, const std::string& remarks) const -> UKControllerPlugin::Curl::CurlRequest { - CurlRequest request(this->apiDomain + "/missed-approaches/" + std::to_string(id), CurlRequest::METHOD_PATCH); + CurlRequest request(this->BuildUrl("/missed-approaches/" + std::to_string(id)), CurlRequest::METHOD_PATCH); nlohmann::json data{ {"remarks", remarks}, }; @@ -466,6 +432,16 @@ namespace UKControllerPlugin::Api { auto ApiRequestBuilder::BuildGetAllMetarsRequest() const -> UKControllerPlugin::Curl::CurlRequest { - return this->AddCommonHeaders(CurlRequest(this->apiDomain + "/metar", CurlRequest::METHOD_GET)); + return this->AddCommonHeaders(CurlRequest(this->BuildUrl("/metar"), CurlRequest::METHOD_GET)); + } + + auto ApiRequestBuilder::BuildUrl(const std::string uri) const -> std::string + { + return this->settings.Url() + uri; + } + + auto ApiRequestBuilder::GetApiDomain() const -> const std::string& + { + return this->settings.Url(); } } // namespace UKControllerPlugin::Api diff --git a/src/utils/api/ApiRequestBuilder.h b/src/utils/api/ApiRequestBuilder.h index 8b256267e..8b479f24f 100644 --- a/src/utils/api/ApiRequestBuilder.h +++ b/src/utils/api/ApiRequestBuilder.h @@ -2,6 +2,10 @@ #include "curl/CurlRequest.h" #include "srd/SrdSearchParameters.h" +namespace UKControllerPluginUtils::Api { + class ApiSettings; +} // namespace UKControllerPluginUtils::Api + namespace UKControllerPlugin::Api { /* @@ -11,7 +15,7 @@ namespace UKControllerPlugin::Api { class ApiRequestBuilder { public: - ApiRequestBuilder(std::string apiDomain, std::string apiKey); + ApiRequestBuilder(const UKControllerPluginUtils::Api::ApiSettings& settings); [[nodiscard]] auto BuildAuthCheckRequest() const -> UKControllerPlugin::Curl::CurlRequest; [[nodiscard]] auto BuildDependencyListRequest() const -> UKControllerPlugin::Curl::CurlRequest; [[nodiscard]] auto BuildGetUriRequest(std::string uri) const -> UKControllerPlugin::Curl::CurlRequest; @@ -93,15 +97,12 @@ namespace UKControllerPlugin::Api { [[nodiscard]] auto BuildMissedApproachAcknowledgeMessage(int id, const std::string& remarks) const -> UKControllerPlugin::Curl::CurlRequest; [[nodiscard]] auto BuildGetAllMetarsRequest() const -> UKControllerPlugin::Curl::CurlRequest; - - [[nodiscard]] auto GetApiDomain() const -> std::string; - [[nodiscard]] auto GetApiKey() const -> std::string; - void SetApiDomain(std::string domain); - void SetApiKey(std::string key); + [[nodiscard]] auto GetApiDomain() const -> const std::string&; private: [[nodiscard]] auto AddCommonHeaders(UKControllerPlugin::Curl::CurlRequest request) const -> UKControllerPlugin::Curl::CurlRequest; + [[nodiscard]] auto BuildUrl(const std::string uri) const -> std::string; // The type string to send in the payload if we want a general squawk const std::string generalSquawkAssignmentType = "general"; @@ -109,10 +110,7 @@ namespace UKControllerPlugin::Api { // The type string to send in the payload if we want a local squawk const std::string localSquawkAssignmentType = "local"; - // The base URL of the API - std::string apiDomain; - - // Our API key - std::string apiKey; + // Api settings + const UKControllerPluginUtils::Api::ApiSettings& settings; }; } // namespace UKControllerPlugin::Api diff --git a/src/utils/api/ApiRequestData.cpp b/src/utils/api/ApiRequestData.cpp new file mode 100644 index 000000000..a7bbacba5 --- /dev/null +++ b/src/utils/api/ApiRequestData.cpp @@ -0,0 +1,59 @@ +#include "ApiRequestData.h" + +using UKControllerPluginUtils::Http::HttpMethod; + +namespace UKControllerPluginUtils::Api { + + ApiRequestData::ApiRequestData(std::string uri, Http::HttpMethod method, nlohmann::json body) + : uri(std::move(uri)), method(std::move(method)), body(std::move(body)) + { + this->CheckForRequiredBody(); + } + + auto ApiRequestData::BodyMissing() const -> bool + { + return body.empty(); + } + + auto ApiRequestData::MethodRequiresBody() const -> bool + { + return method == HttpMethod::Post() || method == HttpMethod::Put() || method == HttpMethod::Patch(); + } + + void ApiRequestData::CheckForRequiredBody() + { + if (MethodRequiresBody() && BodyMissing()) { + throw std::invalid_argument("PUT and POST requests require a body"); + } + } + + auto ApiRequestData::Uri() const -> const std::string& + { + return uri; + } + + auto ApiRequestData::Method() const -> HttpMethod + { + return method; + } + + auto ApiRequestData::Body() const -> const nlohmann::json& + { + return body; + } + + auto ApiRequestData::operator==(const ApiRequestData& compare) const -> bool + { + return IsEqual(compare); + } + + auto ApiRequestData::operator!=(const ApiRequestData& compare) const -> bool + { + return !IsEqual(compare); + } + + auto ApiRequestData::IsEqual(const ApiRequestData& compare) const -> bool + { + return uri == compare.uri && method == compare.method && body == compare.body; + } +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/ApiRequestData.h b/src/utils/api/ApiRequestData.h new file mode 100644 index 000000000..e66ed2900 --- /dev/null +++ b/src/utils/api/ApiRequestData.h @@ -0,0 +1,33 @@ +#pragma once +#include "http/HttpMethod.h" + +namespace UKControllerPluginUtils::Api { + /** + * Contains the data for an API request + */ + class ApiRequestData + { + public: + ApiRequestData(std::string uri, Http::HttpMethod method, nlohmann::json body = {}); + [[nodiscard]] auto Uri() const -> const std::string&; + [[nodiscard]] auto Method() const -> Http::HttpMethod; + [[nodiscard]] auto Body() const -> const nlohmann::json&; + [[nodiscard]] auto operator==(const ApiRequestData& compare) const -> bool; + [[nodiscard]] auto operator!=(const ApiRequestData& compare) const -> bool; + + private: + void CheckForRequiredBody(); + [[nodiscard]] auto MethodRequiresBody() const -> bool; + [[nodiscard]] auto BodyMissing() const -> bool; + [[nodiscard]] auto IsEqual(const ApiRequestData& compare) const -> bool; + + // The URI to hit + std::string uri; + + // The HTTP method being called + Http::HttpMethod method; + + // The body of the request, for PUT and POST + nlohmann::json body; + }; +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/ApiRequestException.cpp b/src/utils/api/ApiRequestException.cpp new file mode 100644 index 000000000..bb540b88b --- /dev/null +++ b/src/utils/api/ApiRequestException.cpp @@ -0,0 +1,25 @@ +#include "ApiRequestException.h" + +namespace UKControllerPluginUtils::Api { + + ApiRequestException::ApiRequestException(const std::string uri, Http::HttpStatusCode statusCode, bool invalidJson) + : std::runtime_error("Api request resulted in status " + std::to_string(static_cast(statusCode))), + uri(uri), statusCode(statusCode), invalidJson(invalidJson) + { + } + + auto ApiRequestException::StatusCode() const -> const Http::HttpStatusCode& + { + return statusCode; + } + + auto ApiRequestException::Uri() const -> const std::string& + { + return uri; + } + + auto ApiRequestException::InvalidJson() const -> bool + { + return invalidJson; + } +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/ApiRequestException.h b/src/utils/api/ApiRequestException.h new file mode 100644 index 000000000..4ca40c6a3 --- /dev/null +++ b/src/utils/api/ApiRequestException.h @@ -0,0 +1,26 @@ +#pragma once +#include "http/HttpStatusCode.h" + +namespace UKControllerPluginUtils::Api { + /** + * An exception thrown by the "new" API flow if something goes wrong. + */ + class ApiRequestException : public std::runtime_error + { + public: + explicit ApiRequestException(const std::string uri, Http::HttpStatusCode statusCode, bool invalidJson); + [[nodiscard]] auto StatusCode() const -> const Http::HttpStatusCode&; + [[nodiscard]] auto Uri() const -> const std::string&; + [[nodiscard]] auto InvalidJson() const -> bool; + + private: + // The URI this request was trying to hit + const std::string uri; + + // The HTTP status code + Http::HttpStatusCode statusCode; + + // Is the response invalid JSON + bool invalidJson; + }; +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/ApiRequestFacade.cpp b/src/utils/api/ApiRequestFacade.cpp new file mode 100644 index 000000000..31ed820bf --- /dev/null +++ b/src/utils/api/ApiRequestFacade.cpp @@ -0,0 +1,24 @@ +#include "ApiFactory.h" +#include "ApiRequestFacade.h" + +std::shared_ptr apiFactory; + +[[nodiscard]] auto ApiRequest() -> UKControllerPluginUtils::Api::ApiRequestFactory& +{ + return apiFactory->RequestFactory(); +} + +void SetApiRequestFactory(std::shared_ptr factory) +{ + apiFactory = factory; +} + +auto ApiRequestFactorySet() -> bool +{ + return apiFactory != nullptr; +} + +void UnsetSetApiFactory() +{ + apiFactory.reset(); +} diff --git a/src/utils/api/ApiRequestFacade.h b/src/utils/api/ApiRequestFacade.h new file mode 100644 index 000000000..a6d2facda --- /dev/null +++ b/src/utils/api/ApiRequestFacade.h @@ -0,0 +1,11 @@ +#pragma once + +namespace UKControllerPluginUtils::Api { + class ApiFactory; + class ApiRequestFactory; +} // namespace UKControllerPluginUtils::Api + +[[nodiscard]] auto ApiRequest() -> UKControllerPluginUtils::Api::ApiRequestFactory&; +void SetApiRequestFactory(std::shared_ptr factory); +[[nodiscard]] auto ApiRequestFactorySet() -> bool; +void UnsetSetApiFactory(); diff --git a/src/utils/api/ApiRequestFactory.cpp b/src/utils/api/ApiRequestFactory.cpp new file mode 100644 index 000000000..0aca1d04b --- /dev/null +++ b/src/utils/api/ApiRequestFactory.cpp @@ -0,0 +1,84 @@ +#include "ApiRequestData.h" +#include "ApiRequestFactory.h" +#include "ApiRequestPerformerInterface.h" +#include "http/HttpMethod.h" + +namespace UKControllerPluginUtils::Api { + + ApiRequestFactory::ApiRequestFactory(ApiRequestPerformerInterface& requestPerformer) + : requestPerformer(requestPerformer), requestsInProgress(0) + { + } + + auto ApiRequestFactory::Get(std::string uri) -> ApiRequest + { + this->BeginRequest(); + return {ApiRequestData(std::move(uri), Http::HttpMethod::Get()), requestPerformer, OnCompletionFunction()}; + } + + auto ApiRequestFactory::Post(std::string uri, nlohmann::json body) -> ApiRequest + { + this->BeginRequest(); + return { + ApiRequestData(std::move(uri), Http::HttpMethod::Post(), body), requestPerformer, OnCompletionFunction()}; + } + + auto ApiRequestFactory::Put(std::string uri, nlohmann::json body) -> ApiRequest + { + this->BeginRequest(); + return { + ApiRequestData(std::move(uri), Http::HttpMethod::Put(), body), requestPerformer, OnCompletionFunction()}; + } + + auto ApiRequestFactory::Patch(std::string uri, nlohmann::json body) -> ApiRequest + { + this->BeginRequest(); + return { + ApiRequestData(std::move(uri), Http::HttpMethod::Patch(), body), requestPerformer, OnCompletionFunction()}; + } + + auto ApiRequestFactory::Delete(std::string uri) -> ApiRequest + { + this->BeginRequest(); + return {ApiRequestData(std::move(uri), Http::HttpMethod::Delete()), requestPerformer, OnCompletionFunction()}; + } + + auto ApiRequestFactory::OnCompletionFunction() -> std::function + { + return [this]() { + auto lock = std::lock_guard(this->requestsInProgressLock); + this->requestsInProgress--; + }; + } + + void ApiRequestFactory::BeginRequest() + { + auto lock = std::lock_guard(this->requestsInProgressLock); + this->requestsInProgress++; + } + + /** + * This method is for shutdown / tests, wait for all requests to complete. + */ + void ApiRequestFactory::AwaitRequestCompletion(const std::chrono::seconds& seconds) + { + auto startTime = std::chrono::system_clock::now(); + while (true) { + if (std::chrono::system_clock::now() > startTime + seconds) { + throw std::exception("Timeout whilst waiting for API request completion"); + } + + if (this->ApiRequestsCompleted()) { + break; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } + } + + auto ApiRequestFactory::ApiRequestsCompleted() -> bool + { + auto lock = std::lock_guard(this->requestsInProgressLock); + return this->requestsInProgress == 0; + } +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/ApiRequestFactory.h b/src/utils/api/ApiRequestFactory.h new file mode 100644 index 000000000..eb67f80f6 --- /dev/null +++ b/src/utils/api/ApiRequestFactory.h @@ -0,0 +1,32 @@ +#pragma once +#include "ApiRequest.h" + +namespace UKControllerPluginUtils::Api { + class ApiRequestPerformerInterface; + + class ApiRequestFactory + { + public: + ApiRequestFactory(ApiRequestPerformerInterface& requestPerformer); + [[nodiscard]] auto Get(std::string uri) -> ApiRequest; + [[nodiscard]] auto Post(std::string uri, nlohmann::json body) -> ApiRequest; + [[nodiscard]] auto Put(std::string uri, nlohmann::json body) -> ApiRequest; + [[nodiscard]] auto Delete(std::string uri) -> ApiRequest; + [[nodiscard]] auto Patch(std::string uri, nlohmann::json body) -> ApiRequest; + void AwaitRequestCompletion(const std::chrono::seconds& = std::chrono::seconds(10)); + + private: + [[nodiscard]] auto ApiRequestsCompleted() -> bool; + [[nodiscard]] auto OnCompletionFunction() -> std::function; + void BeginRequest(); + + // Helps with performing requests + ApiRequestPerformerInterface& requestPerformer; + + // How many requests are in progress + int requestsInProgress; + + // A lock for the progress variable + std::mutex requestsInProgressLock; + }; +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/ApiRequestFactoryInterface.h b/src/utils/api/ApiRequestFactoryInterface.h new file mode 100644 index 000000000..b90707e53 --- /dev/null +++ b/src/utils/api/ApiRequestFactoryInterface.h @@ -0,0 +1,13 @@ +#pragma once + +namespace UKControllerPluginUtils::Api { + + /** + * Interface for building API Requests + */ + class ApiRequestFactoryInterface + { + public: + virtual ~ApiRequestFactoryInterface() = default; + }; +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/ApiRequestPerformerInterface.h b/src/utils/api/ApiRequestPerformerInterface.h new file mode 100644 index 000000000..7f14268cd --- /dev/null +++ b/src/utils/api/ApiRequestPerformerInterface.h @@ -0,0 +1,23 @@ +#pragma once +#include "Response.h" + +namespace UKControllerPluginUtils::Api { + class ApiRequestData; + + /** + * Performs API requests and returns a response or throws an exception. + * + * Provides a handy mocking point for API-related fun. + */ + class ApiRequestPerformerInterface + { + public: + virtual ~ApiRequestPerformerInterface() = default; + /** + * Perform the request. + * + * @throws ApiRequestException on failure + */ + [[nodiscard]] virtual auto Perform(const ApiRequestData& data) -> Response = 0; + }; +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/ApiSettings.cpp b/src/utils/api/ApiSettings.cpp new file mode 100644 index 000000000..f8c1eb1ac --- /dev/null +++ b/src/utils/api/ApiSettings.cpp @@ -0,0 +1,28 @@ +#include "ApiSettings.h" + +namespace UKControllerPluginUtils::Api { + + ApiSettings::ApiSettings(std::string url, std::string key) : url(std::move(url)), key(std::move(key)) + { + } + + auto ApiSettings::Url() const -> const std::string& + { + return url; + } + + void ApiSettings::Url(const std::string& url) + { + this->url = url; + } + + auto ApiSettings::Key() const -> const std::string& + { + return key; + } + + void ApiSettings::Key(const std::string& key) + { + this->key = key; + } +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/ApiSettings.h b/src/utils/api/ApiSettings.h new file mode 100644 index 000000000..cf3eced39 --- /dev/null +++ b/src/utils/api/ApiSettings.h @@ -0,0 +1,23 @@ +#pragma once + +namespace UKControllerPluginUtils::Api { + /** + * Contains the settings for the API + */ + class ApiSettings + { + public: + ApiSettings(std::string url, std::string key); + [[nodiscard]] auto Url() const -> const std::string&; + void Url(const std::string& url); + [[nodiscard]] auto Key() const -> const std::string&; + void Key(const std::string& key); + + private: + // The URL for the api + std::string url; + + // The users API key + std::string key; + }; +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/ApiSettingsProviderInterface.h b/src/utils/api/ApiSettingsProviderInterface.h new file mode 100644 index 000000000..629305d56 --- /dev/null +++ b/src/utils/api/ApiSettingsProviderInterface.h @@ -0,0 +1,27 @@ +#pragma once +#include "ApiBootstrap.h" + +namespace UKControllerPluginUtils::Api { + class ApiSettings; + + class ApiSettingsProviderInterface + { + public: + virtual ~ApiSettingsProviderInterface() = default; + + /** + * Load from storage, using defaults if required. + */ + virtual auto Get() -> ApiSettings& = 0; + + /** + * Do we have the API settings? + */ + virtual auto Has() -> bool = 0; + + /** + * Triggers a reload of the settings from source. + */ + [[nodiscard]] virtual auto Reload() -> bool = 0; + }; +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/ApiUrlBuilder.cpp b/src/utils/api/ApiUrlBuilder.cpp new file mode 100644 index 000000000..39b74065a --- /dev/null +++ b/src/utils/api/ApiUrlBuilder.cpp @@ -0,0 +1,17 @@ +#include "ApiRequestData.h" +#include "ApiSettings.h" +#include "ApiUrlBuilder.h" +#include "string/StringTrimFunctions.h" + +namespace UKControllerPluginUtils::Api { + + ApiUrlBuilder::ApiUrlBuilder(const ApiSettings& settings) : settings(settings) + { + } + + auto ApiUrlBuilder::BuildUrl(const ApiRequestData& requestData) const -> const std::string + { + return String::rtrim(settings.Url(), URL_PATH_SEPARATOR) + URL_PATH_SEPARATOR + + String::trim(requestData.Uri(), URL_PATH_SEPARATOR); + } +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/ApiUrlBuilder.h b/src/utils/api/ApiUrlBuilder.h new file mode 100644 index 000000000..52c8ca8d4 --- /dev/null +++ b/src/utils/api/ApiUrlBuilder.h @@ -0,0 +1,22 @@ +#pragma once +#include "ApiSettings.h" + +namespace UKControllerPluginUtils::Api { + class ApiRequestData; + class ApiSettings; + + /** + * Builds API URLs from a URI + */ + class ApiUrlBuilder + { + public: + ApiUrlBuilder(const ApiSettings& settings); + [[nodiscard]] auto BuildUrl(const ApiRequestData& requestData) const -> const std::string; + + private: + const ApiSettings& settings; + + const std::string URL_PATH_SEPARATOR = "/"; + }; +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/ChainableRequest.cpp b/src/utils/api/ChainableRequest.cpp new file mode 100644 index 000000000..ef061431d --- /dev/null +++ b/src/utils/api/ChainableRequest.cpp @@ -0,0 +1,75 @@ +#include "ApiRequestData.h" +#include "ApiRequestException.h" +#include "ApiRequestPerformerInterface.h" +#include "ChainableRequest.h" + +namespace UKControllerPluginUtils::Api { + ChainableRequest::ChainableRequest( + const ApiRequestData& data, ApiRequestPerformerInterface& performer, std::function onCompletion) + : continuable(cti::make_continuable([data, &performer](auto&& promise) { + try { + promise.set_value(performer.Perform(data)); + } catch (ApiRequestException& exception) { + promise.set_exception(std::make_exception_ptr(exception)); + } + })), + onCompletion(onCompletion) + { + } + + ChainableRequest::~ChainableRequest() + { + if (!this->executed) { + this->ApplyOnCompletion(); + } + } + + void ChainableRequest::Then(const std::function& function) + { + continuable = std::move(continuable).then([function](Response response) { + function(response); + return response; + }); + } + + void ChainableRequest::Then(const std::function& function) + { + continuable = std::move(continuable).then([function](Response response) { + function(); + return response; + }); + } + + void ChainableRequest::Catch(const std::function& function) + { + auto complete = this->onCompletion; + continuable = std::move(continuable).fail([function, complete, this](std::exception_ptr exception) { + try { + std::rethrow_exception(exception); + } catch (const ApiRequestException& requestException) { + function(requestException); + this->executed = true; + complete(); + } catch (const std::exception&) { + // Everythings over now + } + + return cti::cancel(); + }); + } + + void ChainableRequest::Await() + { + this->ApplyOnCompletion(); + std::move(continuable).apply(cti::transforms::wait()); + } + + void ChainableRequest::ApplyOnCompletion() + { + auto complete = this->onCompletion; + this->Then([this, complete]() { + this->executed = true; + complete(); + }); + } +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/ChainableRequest.h b/src/utils/api/ChainableRequest.h new file mode 100644 index 000000000..7bc30e118 --- /dev/null +++ b/src/utils/api/ChainableRequest.h @@ -0,0 +1,37 @@ +#pragma once +#include "Response.h" + +namespace UKControllerPluginUtils::Api { + class ApiRequestData; + class ApiRequestException; + class ApiRequestPerformerInterface; + + /** + * Hides the details of the continuation library. + */ + class ChainableRequest + { + public: + ChainableRequest( + const ApiRequestData& data, + ApiRequestPerformerInterface& performer, + std::function onCompletion); + ~ChainableRequest(); + void Then(const std::function& function); + void Then(const std::function& function); + void Catch(const std::function& function); + void Await(); + + private: + void ApplyOnCompletion(); + + // Continuable instance + cti::continuable continuable; + + // A function to run when we're all done + std::function onCompletion; + + // Has the request executed + bool executed = false; + }; +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/ConfigApiSettingsProvider.cpp b/src/utils/api/ConfigApiSettingsProvider.cpp new file mode 100644 index 000000000..3af6199d3 --- /dev/null +++ b/src/utils/api/ConfigApiSettingsProvider.cpp @@ -0,0 +1,64 @@ +#include "ApiSettings.h" +#include "ConfigApiSettingsProvider.h" +#include "setting/SettingRepositoryInterface.h" +#include "windows/WinApiInterface.h" + +using UKControllerPlugin::Setting::SettingRepositoryInterface; +using UKControllerPlugin::Windows::WinApiInterface; + +namespace UKControllerPluginUtils::Api { + + ConfigApiSettingsProvider::ConfigApiSettingsProvider( + SettingRepositoryInterface& settingRepository, WinApiInterface& windows) + : settingRepository(settingRepository), windows(windows) + { + } + + ConfigApiSettingsProvider::~ConfigApiSettingsProvider() = default; + + auto ConfigApiSettingsProvider::Get() -> ApiSettings& + { + if (!this->settings) { + this->settings = std::make_unique( + settingRepository.GetSetting(API_URL_SETTING, DEFAULT_API_URL), + settingRepository.GetSetting(API_KEY_SETTING, DEFAULT_API_KEY)); + } + + return *this->settings; + } + + auto ConfigApiSettingsProvider::Reload() -> bool + { + // Select the file to get settings from + COMDLG_FILTERSPEC fileTypes[] = { + {L"JSON", L"*.json"}, + }; + + std::wstring filePath = windows.FileOpenDialog(L"Select API Settings File", 1, fileTypes); + if (filePath.empty()) { + LogInfo("User did not select a valid key file to replacte"); + return false; + } + + // Write file + windows.WriteToFile(L"settings/api-settings.json", windows.ReadFromFile(filePath, false), true, false); + + // Trigger a reload + this->settingRepository.ReloadSetting(API_KEY_SETTING); + this->settingRepository.ReloadSetting(API_URL_SETTING); + + if (!this->settings) { + this->settings = std::make_unique(DEFAULT_API_URL, DEFAULT_API_KEY); + } + + this->settings->Url(settingRepository.GetSetting(API_URL_SETTING, DEFAULT_API_URL)); + this->settings->Key(settingRepository.GetSetting(API_KEY_SETTING, DEFAULT_API_KEY)); + return true; + } + + auto ConfigApiSettingsProvider::Has() -> bool + { + const auto settings = this->Get(); + return !settings.Key().empty(); + } +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/ConfigApiSettingsProvider.h b/src/utils/api/ConfigApiSettingsProvider.h new file mode 100644 index 000000000..8a957b7e5 --- /dev/null +++ b/src/utils/api/ConfigApiSettingsProvider.h @@ -0,0 +1,42 @@ +#pragma once +#include "ApiSettingsProviderInterface.h" + +namespace UKControllerPlugin { + namespace Setting { + class SettingRepositoryInterface; + } // namespace Setting + namespace Windows { + class WinApiInterface; + } +} // namespace UKControllerPlugin + +namespace UKControllerPluginUtils::Api { + + class ConfigApiSettingsProvider : public ApiSettingsProviderInterface + { + public: + ConfigApiSettingsProvider( + UKControllerPlugin::Setting::SettingRepositoryInterface& settingRepository, + UKControllerPlugin::Windows::WinApiInterface& windows); + ~ConfigApiSettingsProvider(); + [[nodiscard]] auto Get() -> ApiSettings& override; + auto Has() -> bool override; + [[nodiscard]] auto Reload() -> bool override; + + private: + // Provides us config + UKControllerPlugin::Setting::SettingRepositoryInterface& settingRepository; + + // For reloading + UKControllerPlugin::Windows::WinApiInterface& windows; + + // The owned setting object + std::unique_ptr settings; + + // Some setting keys + const std::string API_KEY_SETTING = "api-key"; + const std::string API_URL_SETTING = "api-url"; + const std::string DEFAULT_API_URL = "https://ukcp.vatsim.uk"; + const std::string DEFAULT_API_KEY = ""; + }; +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/CurlApiRequestPerformer.cpp b/src/utils/api/CurlApiRequestPerformer.cpp new file mode 100644 index 000000000..cdaaf40e5 --- /dev/null +++ b/src/utils/api/CurlApiRequestPerformer.cpp @@ -0,0 +1,45 @@ +#include "ApiCurlRequestFactory.h" +#include "ApiRequestData.h" +#include "ApiRequestException.h" +#include "CurlApiRequestPerformer.h" +#include "curl/CurlInterface.h" +#include "curl/CurlRequest.h" + +using UKControllerPlugin::Curl::CurlInterface; +using UKControllerPlugin::Curl::CurlRequest; +using UKControllerPlugin::Curl::CurlResponse; +using UKControllerPluginUtils::Http::HttpStatusCode; +using UKControllerPluginUtils::Http::IsSuccessful; + +namespace UKControllerPluginUtils::Api { + + CurlApiRequestPerformer::CurlApiRequestPerformer(CurlInterface& curl, const ApiCurlRequestFactory& requestFactory) + : curl(curl), requestFactory(requestFactory) + { + } + + auto CurlApiRequestPerformer::Perform(const ApiRequestData& data) -> Response + { + auto curlResponse = curl.MakeCurlRequest(requestFactory.BuildCurlRequest(data)); + if (!ResponseSuccessful(curlResponse)) { + throw ApiRequestException(data.Uri(), static_cast(curlResponse.GetStatusCode()), false); + } + + return {static_cast(curlResponse.GetStatusCode()), ParseResponseBody(data, curlResponse)}; + } + + auto CurlApiRequestPerformer::ResponseSuccessful(const CurlResponse& response) -> bool + { + return !response.IsCurlError() && IsSuccessful(static_cast(response.GetStatusCode())); + } + + auto CurlApiRequestPerformer::ParseResponseBody(const ApiRequestData& data, const CurlResponse& response) + -> nlohmann::json + { + try { + return nlohmann::json::parse(response.GetResponse()); + } catch (nlohmann::json::exception& jsonException) { + throw ApiRequestException(data.Uri(), static_cast(response.GetStatusCode()), true); + } + } +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/CurlApiRequestPerformer.h b/src/utils/api/CurlApiRequestPerformer.h new file mode 100644 index 000000000..a3714375a --- /dev/null +++ b/src/utils/api/CurlApiRequestPerformer.h @@ -0,0 +1,34 @@ +#pragma once +#include "api/ApiRequestPerformerInterface.h" + +namespace UKControllerPlugin::Curl { + class CurlInterface; + class CurlResponse; +} // namespace UKControllerPlugin::Curl + +namespace UKControllerPluginUtils::Api { + class ApiCurlRequestFactory; + + /** + * Performs API requests. + */ + class CurlApiRequestPerformer : public ApiRequestPerformerInterface + { + public: + CurlApiRequestPerformer( + UKControllerPlugin::Curl::CurlInterface& curl, const ApiCurlRequestFactory& requestFactory); + auto Perform(const ApiRequestData& data) -> Response override; + + private: + [[nodiscard]] static auto ResponseSuccessful(const UKControllerPlugin::Curl::CurlResponse& response) -> bool; + [[nodiscard]] static auto + ParseResponseBody(const ApiRequestData& data, const UKControllerPlugin::Curl::CurlResponse& response) + -> nlohmann::json; + + // For making the cURL requests + UKControllerPlugin::Curl::CurlInterface& curl; + + // Settings for the API + const ApiCurlRequestFactory& requestFactory; + }; +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/CurlApiRequestPerformerFactory.cpp b/src/utils/api/CurlApiRequestPerformerFactory.cpp new file mode 100644 index 000000000..051d6b717 --- /dev/null +++ b/src/utils/api/CurlApiRequestPerformerFactory.cpp @@ -0,0 +1,30 @@ +#include "ApiCurlRequestFactory.h" +#include "ApiHeaderApplicator.h" +#include "ApiUrlBuilder.h" +#include "CurlApiRequestPerformer.h" +#include "CurlApiRequestPerformerFactory.h" +#include "curl/CurlInterface.h" + +using UKControllerPlugin::Curl::CurlInterface; + +namespace UKControllerPluginUtils::Api { + + CurlApiRequestPerformerFactory::CurlApiRequestPerformerFactory(std::unique_ptr curl) + : curl(std::move(curl)) + { + } + + CurlApiRequestPerformerFactory::~CurlApiRequestPerformerFactory() = default; + + ApiRequestPerformerInterface& CurlApiRequestPerformerFactory::Make(const ApiSettings& apiSettings) + { + if (performer == nullptr) { + urlBuilder = std::make_unique(apiSettings); + headerApplicator = std::make_unique(apiSettings); + curlRequestFactory = std::make_unique(*urlBuilder, *headerApplicator); + performer = std::make_unique(*curl, *curlRequestFactory); + } + + return *performer; + } +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/CurlApiRequestPerformerFactory.h b/src/utils/api/CurlApiRequestPerformerFactory.h new file mode 100644 index 000000000..2a0f28b0b --- /dev/null +++ b/src/utils/api/CurlApiRequestPerformerFactory.h @@ -0,0 +1,39 @@ +#pragma once +#include "AbstractApiRequestPerformerFactory.h" + +namespace UKControllerPlugin::Curl { + class CurlInterface; +} // namespace UKControllerPlugin::Curl + +namespace UKControllerPluginUtils::Api { + class ApiCurlRequestFactory; + class ApiHeaderApplicator; + class ApiUrlBuilder; + + /** + * Builds API requests during default running, non-mocked. + */ + class CurlApiRequestPerformerFactory : public AbstractApiRequestPerformerFactory + { + public: + CurlApiRequestPerformerFactory(std::unique_ptr curl); + ~CurlApiRequestPerformerFactory(); + [[nodiscard]] auto Make(const ApiSettings& apiSettings) -> ApiRequestPerformerInterface& override; + + private: + // For cURL requests + std::unique_ptr curl; + + // Applies headers + std::unique_ptr headerApplicator; + + // Builds the URLs + std::unique_ptr urlBuilder; + + // For making cURL requests + std::unique_ptr curlRequestFactory; + + // Performs requests + std::unique_ptr performer; + }; +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/Response.cpp b/src/utils/api/Response.cpp new file mode 100644 index 000000000..229b0063f --- /dev/null +++ b/src/utils/api/Response.cpp @@ -0,0 +1,19 @@ +#include "Response.h" + +namespace UKControllerPluginUtils::Api { + + Response::Response(Http::HttpStatusCode statusCode, nlohmann::json data) + : statusCode(statusCode), data(std::move(data)) + { + } + + auto Response::StatusCode() const -> Http::HttpStatusCode + { + return statusCode; + } + + auto Response::Data() const -> const nlohmann::json& + { + return data; + } +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/api/Response.h b/src/utils/api/Response.h new file mode 100644 index 000000000..ed3a5095a --- /dev/null +++ b/src/utils/api/Response.h @@ -0,0 +1,22 @@ +#pragma once +#include "http/HttpStatusCode.h" + +namespace UKControllerPluginUtils::Api { + /** + * Represents a response from the API + */ + class Response + { + public: + Response(Http::HttpStatusCode statusCode, nlohmann::json data); + [[nodiscard]] auto StatusCode() const -> Http::HttpStatusCode; + [[nodiscard]] auto Data() const -> const nlohmann::json&; + + private: + // The status code + Http::HttpStatusCode statusCode; + + // The data that came back + nlohmann::json data; + }; +} // namespace UKControllerPluginUtils::Api diff --git a/src/utils/curl/CurlRequest.cpp b/src/utils/curl/CurlRequest.cpp index 564a3d025..bf01ef616 100644 --- a/src/utils/curl/CurlRequest.cpp +++ b/src/utils/curl/CurlRequest.cpp @@ -1,5 +1,7 @@ #include "CurlRequest.h" +using UKControllerPluginUtils::Http::HttpMethod; + namespace UKControllerPlugin::Curl { CurlRequest::CurlRequest(std::string uri, std::string method) @@ -7,6 +9,10 @@ namespace UKControllerPlugin::Curl { { } + CurlRequest::CurlRequest(std::string uri, HttpMethod method) : method(method), uri(std::move(uri)) + { + } + void CurlRequest::AddHeader(const std::string& key, std::string value) { if (this->headers.count(key) != 0) { diff --git a/src/utils/curl/CurlRequest.h b/src/utils/curl/CurlRequest.h index e673e148c..beeb123f5 100644 --- a/src/utils/curl/CurlRequest.h +++ b/src/utils/curl/CurlRequest.h @@ -1,4 +1,5 @@ #pragma once +#include "http/HttpMethod.h" namespace UKControllerPlugin::Curl { @@ -10,6 +11,7 @@ namespace UKControllerPlugin::Curl { public: CurlRequest(std::string uri, std::string method); + CurlRequest(std::string uri, UKControllerPluginUtils::Http::HttpMethod method); void AddHeader(const std::string& key, std::string value); [[nodiscard]] auto GetBody() const -> const char*; [[nodiscard]] auto GetMethod() const -> const char*; diff --git a/src/utils/http/HttpMethod.cpp b/src/utils/http/HttpMethod.cpp new file mode 100644 index 000000000..9b7056a3f --- /dev/null +++ b/src/utils/http/HttpMethod.cpp @@ -0,0 +1,43 @@ +#include "HttpMethod.h" + +namespace UKControllerPluginUtils::Http { + + HttpMethod::HttpMethod(std::string method) : method(method) + { + } + + auto HttpMethod::Get() -> HttpMethod + { + return HttpMethod("GET"); + } + + auto HttpMethod::Post() -> HttpMethod + { + return HttpMethod("POST"); + } + + auto HttpMethod::Put() -> HttpMethod + { + return HttpMethod("PUT"); + } + + auto HttpMethod::Patch() -> HttpMethod + { + return HttpMethod("PATCH"); + } + + auto HttpMethod::Delete() -> HttpMethod + { + return HttpMethod("DELETE"); + } + + HttpMethod::operator std::string() const + { + return method; + } + + auto HttpMethod::operator==(const HttpMethod& compare) const -> bool + { + return method == compare.method; + } +} // namespace UKControllerPluginUtils::Http diff --git a/src/utils/http/HttpMethod.h b/src/utils/http/HttpMethod.h new file mode 100644 index 000000000..f28a2985b --- /dev/null +++ b/src/utils/http/HttpMethod.h @@ -0,0 +1,24 @@ +#pragma once + +namespace UKControllerPluginUtils::Http { + /** + * Wrapper around cURL HTTP methods + */ + class HttpMethod + { + public: + [[nodiscard]] static auto Get() -> HttpMethod; + [[nodiscard]] static auto Post() -> HttpMethod; + [[nodiscard]] static auto Put() -> HttpMethod; + [[nodiscard]] static auto Patch() -> HttpMethod; + [[nodiscard]] static auto Delete() -> HttpMethod; + [[nodiscard]] operator std::string() const; + [[nodiscard]] auto operator==(const HttpMethod& compare) const -> bool; + + protected: + HttpMethod(std::string method); + + private: + std::string method; + }; +} // namespace UKControllerPluginUtils::Http diff --git a/src/utils/http/HttpStatusCode.h b/src/utils/http/HttpStatusCode.h new file mode 100644 index 000000000..28d5e60d3 --- /dev/null +++ b/src/utils/http/HttpStatusCode.h @@ -0,0 +1,40 @@ +#pragma once + +namespace UKControllerPluginUtils::Http { + enum class HttpStatusCode : uint64_t + { + Unknown = 0L, + Ok = 200L, + Created = 201L, + NoContent = 204L, + BadRequest = 400L, + Unauthorised = 401L, + Forbidden = 403L, + NotFound = 404L, + MethodNotAllowed = 405L, + Unprocessable = 422L, + ServerError = 500L, + BadGateway = 502L, + }; + + inline auto operator==(uint64_t first, HttpStatusCode second) -> bool + { + return first == static_cast(second); + } + + inline auto IsSuccessful(HttpStatusCode status) -> bool + { + return status == HttpStatusCode::Ok || status == HttpStatusCode::Created || status == HttpStatusCode::NoContent; + } + + inline auto IsAuthenticationError(HttpStatusCode status) -> bool + { + return status == HttpStatusCode::Unauthorised || status == HttpStatusCode::Forbidden; + } + + inline auto IsServerError(HttpStatusCode status) -> bool + { + return status == HttpStatusCode::ServerError || status == HttpStatusCode::BadGateway || + status == HttpStatusCode::Unknown; + } +} // namespace UKControllerPluginUtils::Http diff --git a/src/utils/pch/pch.h b/src/utils/pch/pch.h index a74b23307..f838dbe94 100644 --- a/src/utils/pch/pch.h +++ b/src/utils/pch/pch.h @@ -17,6 +17,7 @@ #include #pragma warning(pop) +#include #include #include #include @@ -36,4 +37,6 @@ #include // Custom headers +#include "api/ApiRequestFacade.h" +#include "continuable/continuable.hpp" #include "log/LoggerFunctions.h" diff --git a/src/utils/setting/JsonFileSettingProvider.cpp b/src/utils/setting/JsonFileSettingProvider.cpp new file mode 100644 index 000000000..3d10b8660 --- /dev/null +++ b/src/utils/setting/JsonFileSettingProvider.cpp @@ -0,0 +1,68 @@ +#include "JsonFileSettingProvider.h" +#include "helper/HelperFunctions.h" +#include "windows/WinApiInterface.h" + +namespace UKControllerPlugin::Setting { + + JsonFileSettingProvider::JsonFileSettingProvider( + const std::wstring filename, std::set providedSettings, Windows::WinApiInterface& windows) + : filename(std::move(filename)), providedSettings(std::move(providedSettings)), windows(windows), + loadedSettings(LoadFromFile()) + { + } + + auto JsonFileSettingProvider::Get(const std::string& key) -> std::string + { + if (loadedSettings.count(key) == 0) { + return ""; + } + + return loadedSettings.at(key); + } + + void JsonFileSettingProvider::Save(const std::string& key, const std::string& value) + { + loadedSettings[key] = value; + windows.WriteToFile(L"settings/" + filename, nlohmann::json(loadedSettings).dump(), true, false); + } + + auto JsonFileSettingProvider::Provides() -> const std::set& + { + return providedSettings; + } + + auto JsonFileSettingProvider::LoadFromFile() const -> std::map + { + if (!windows.FileExists(L"settings/" + filename)) { + return {}; + } + + nlohmann::json data; + try { + data = nlohmann::json::parse(windows.ReadFromFile(L"settings/" + filename)); + } catch (nlohmann::json::exception&) { + LogError("Invalid JSON in setting file " + HelperFunctions::ConvertToRegularString(filename)); + return {}; + } + + if (!data.is_object()) { + LogError("JSON not object in setting file " + HelperFunctions::ConvertToRegularString(filename)); + return {}; + } + + for (auto it = data.cbegin(); it != data.cend(); ++it) { + if (!it->is_string()) { + LogError( + "JSON value not a string in setting file " + HelperFunctions::ConvertToRegularString(filename)); + return {}; + } + } + + return data.get>(); + } + + void JsonFileSettingProvider::Reload() + { + this->loadedSettings = this->LoadFromFile(); + } +} // namespace UKControllerPlugin::Setting diff --git a/src/utils/setting/JsonFileSettingProvider.h b/src/utils/setting/JsonFileSettingProvider.h new file mode 100644 index 000000000..b0cf4770f --- /dev/null +++ b/src/utils/setting/JsonFileSettingProvider.h @@ -0,0 +1,37 @@ +#pragma once +#include "SettingProviderInterface.h" + +namespace UKControllerPlugin::Windows { + class WinApiInterface; +} // namespace UKControllerPlugin::Windows + +namespace UKControllerPlugin::Setting { + /** + * Provides settings from a JSON object stored in a file. + */ + class JsonFileSettingProvider : public SettingProviderInterface + { + public: + JsonFileSettingProvider( + const std::wstring filename, std::set providedSettings, Windows::WinApiInterface& windows); + auto Get(const std::string& key) -> std::string override; + void Save(const std::string& key, const std::string& value) override; + auto Provides() -> const std::set& override; + void Reload() override; + + private: + [[nodiscard]] auto LoadFromFile() const -> std::map; + + // The filename to load + const std::wstring filename; + + // The settings provided in this config file + const std::set providedSettings; + + // Windows API for loading files + Windows::WinApiInterface& windows; + + // Loaded settings + std::map loadedSettings; + }; +} // namespace UKControllerPlugin::Setting diff --git a/src/utils/setting/SettingProviderInterface.h b/src/utils/setting/SettingProviderInterface.h new file mode 100644 index 000000000..34b0ff630 --- /dev/null +++ b/src/utils/setting/SettingProviderInterface.h @@ -0,0 +1,21 @@ +#pragma once + +namespace UKControllerPlugin::Setting { + + /** + * Provides an interface for classes that provide plugin settings. + */ + class SettingProviderInterface + { + public: + virtual ~SettingProviderInterface() = default; + + /** + * Returns settings values. Returns empty string if not found. + */ + [[nodiscard]] virtual auto Get(const std::string& key) -> std::string = 0; + virtual void Save(const std::string& key, const std::string& value) = 0; + [[nodiscard]] virtual auto Provides() -> const std::set& = 0; + virtual void Reload() = 0; + }; +} // namespace UKControllerPlugin::Setting diff --git a/src/utils/setting/SettingRepository.cpp b/src/utils/setting/SettingRepository.cpp index 4945ccfff..2f95a315d 100644 --- a/src/utils/setting/SettingRepository.cpp +++ b/src/utils/setting/SettingRepository.cpp @@ -1,150 +1,55 @@ +#include "SettingProviderInterface.h" #include "SettingRepository.h" -#include "helper/HelperFunctions.h" -#include "windows/WinApiInterface.h" - -using UKControllerPlugin::Setting::SettingValue; -using UKControllerPlugin::Windows::WinApiInterface; namespace UKControllerPlugin::Setting { - SettingRepository::SettingRepository(WinApiInterface& winApi) : winApi(winApi) - { - } - /* - Read a JSON file and creates settings from it. - */ - void SettingRepository::AddSettingsFromJsonFile(std::string relativePath, bool overwrite) + void SettingRepository::AddProvider(std::shared_ptr provider) { - std::wstring widePath = HelperFunctions::ConvertToWideString(relativePath); - - // If the file doesn't exist locally, there's no point. - if (!this->winApi.FileExists(this->settingFolder + L"/" + widePath)) { - LogError("Settings file does not exist"); - return; - } - - nlohmann::json settingsJson; - try { - settingsJson = nlohmann::json::parse(this->winApi.ReadFromFile(this->settingFolder + L"/" + widePath)); - } catch (nlohmann::json::exception) { - LogError("Settings file " + relativePath + " is corrupt"); - return; - } catch (std::ifstream::failure) { - LogError("Error reading from file " + relativePath); - return; - } - - // Settings should be an object. - if (!settingsJson.is_object()) { - LogError("Settings file " + relativePath + " is corrupt"); - return; - } - - // Process each setting in the file - for (nlohmann::json::iterator it = settingsJson.begin(); it != settingsJson.end(); ++it) { - if (this->settings.count(it.key()) > 0 && !overwrite) { - LogWarning("Duplicate setting for " + it.key() + " in " + relativePath + " has been ignored"); + for (const auto& setting : provider->Provides()) { + if (this->settings.count(setting)) { + LogWarning("Detected second provider for setting " + setting); continue; } - SettingValue value; - value.setting = it.key(); - value.value = HelperFunctions::StripQuotesFromJsonString(it->dump()); - value.storagePath = relativePath; - - this->settings[it.key()] = value; + this->settings[setting] = provider; } } - /* - Adds a setting to the repo - */ - void SettingRepository::AddSettingValue(SettingValue setting) + auto SettingRepository::GetSetting(const std::string& setting, const std::string& defaultValue) const -> std::string { - if (this->HasSetting(setting.setting)) { - return; + if (!this->HasSetting(setting)) { + return defaultValue; } - this->settings[setting.setting] = setting; - this->UpdateSettingsForFile(this->settings[setting.setting].storagePath); - } - - /* - Returns the number of unique settings. - */ - auto SettingRepository::SettingsCount() const -> size_t - { - return this->settings.size(); - } - - /* - Returns the value for a given setting, or empty string if not found. - */ - auto SettingRepository::GetSetting(std::string setting, std::string defaultValue) const -> std::string - { - return (this->HasSetting(setting)) ? this->settings.at(setting).value : defaultValue; + auto value = this->settings.at(setting)->Get(setting); + return value.empty() ? defaultValue : value; } - /* - Returns true if a value exists for a given setting. - */ - auto SettingRepository::HasSetting(std::string setting) const -> bool + auto SettingRepository::HasSetting(const std::string& setting) const -> bool { return this->settings.count(setting) > 0; } - /* - Updates a particular settings value. - */ - void SettingRepository::UpdateSetting(std::string setting, std::string value) + void SettingRepository::UpdateSetting(const std::string& setting, const std::string& value) { if (!this->HasSetting(setting)) { return; } - this->settings.at(setting).value = value; - this->UpdateSettingsForFile(this->settings[setting].storagePath); + this->settings.at(setting)->Save(setting, value); } - /* - Writes all settings to their respective files. - */ - void SettingRepository::WriteAllSettingsToFile() + void SettingRepository::ReloadSetting(const std::string& setting) { - std::map> settingsByFile; - for (const auto& setting : this->settings) { - settingsByFile[setting.second.storagePath][setting.second.setting] = setting.second.value; - } - - for (const auto& file : settingsByFile) { - this->WriteSettingsToFile(file.second, file.first); - } - } - - auto SettingRepository::AllSettingsForFile(const std::string& file) -> std::map - { - std::map settingsForFile; - for (const auto& setting : this->settings) { - if (setting.second.storagePath == file) { - settingsForFile[setting.second.setting] = setting.second.value; - } + if (!this->HasSetting(setting)) { + return; } - return settingsForFile; + this->settings.at(setting)->Reload(); } - void - SettingRepository::WriteSettingsToFile(const std::map& settings, const std::string& file) + auto SettingRepository::CountSettings() const -> size_t { - try { - std::wstring widePath = HelperFunctions::ConvertToWideString(file); - winApi.WriteToFile(this->settingFolder + L"/" + widePath, nlohmann::json(settings).dump(4), true, false); - } catch (std::ifstream::failure) { - } - } - - void SettingRepository::UpdateSettingsForFile(const std::string& file) - { - this->WriteSettingsToFile(this->AllSettingsForFile(file), file); + return this->settings.size(); } } // namespace UKControllerPlugin::Setting diff --git a/src/utils/setting/SettingRepository.h b/src/utils/setting/SettingRepository.h index a2f0a29ff..31ee75b70 100644 --- a/src/utils/setting/SettingRepository.h +++ b/src/utils/setting/SettingRepository.h @@ -1,43 +1,22 @@ #pragma once -#include "setting/SettingValue.h" - -namespace UKControllerPlugin::Windows { - class WinApiInterface; -} // namespace UKControllerPlugin::Windows +#include "setting/SettingRepositoryInterface.h" namespace UKControllerPlugin::Setting { + class SettingProviderInterface; - /* - A class for loading and storing settings from file. This class is used primarily - to store global settings for the plugin that are not specific to a particular EuroScope - profile or ASR. - - Settings must have a unique key, no matter which file they've come from. - */ - class SettingRepository + class SettingRepository : public SettingRepositoryInterface { public: - explicit SettingRepository(UKControllerPlugin::Windows::WinApiInterface& winApi); - void AddSettingsFromJsonFile(std::string relativePath, bool overwrite = false); - void AddSettingValue(UKControllerPlugin::Setting::SettingValue setting); - [[nodiscard]] auto SettingsCount() const -> size_t; - [[nodiscard]] auto GetSetting(std::string setting, std::string defaultValue = "") const -> std::string; - [[nodiscard]] auto HasSetting(std::string setting) const -> bool; - void UpdateSetting(std::string setting, std::string value); - void WriteAllSettingsToFile(); - - // The folder where we put all our settings. - const std::wstring settingFolder = L"settings"; + void AddProvider(std::shared_ptr provider); + [[nodiscard]] auto GetSetting(const std::string& setting, const std::string& defaultValue = "") const + -> std::string override; + [[nodiscard]] auto HasSetting(const std::string& setting) const -> bool override; + void UpdateSetting(const std::string& setting, const std::string& value) override; + void ReloadSetting(const std::string& setting) override; + [[nodiscard]] auto CountSettings() const -> size_t; private: - void UpdateSettingsForFile(const std::string& file); - [[nodiscard]] auto AllSettingsForFile(const std::string& file) -> std::map; - void WriteSettingsToFile(const std::map& settings, const std::string& file); - - // Settings - std::map settings; - - // Interface to windows - UKControllerPlugin::Windows::WinApiInterface& winApi; + // Setting key to provider map + std::map> settings; }; } // namespace UKControllerPlugin::Setting diff --git a/src/utils/setting/SettingRepositoryFactory.cpp b/src/utils/setting/SettingRepositoryFactory.cpp index 4d5ee53e5..bc4811d47 100644 --- a/src/utils/setting/SettingRepositoryFactory.cpp +++ b/src/utils/setting/SettingRepositoryFactory.cpp @@ -4,11 +4,8 @@ using UKControllerPlugin::Windows::WinApiInterface; namespace UKControllerPlugin::Setting { - std::unique_ptr SettingRepositoryFactory::Create(WinApiInterface& winApi) + std::unique_ptr SettingRepositoryFactory::Create() { - std::unique_ptr repo = std::make_unique(winApi); - repo->AddSettingsFromJsonFile("api-settings.json"); - repo->AddSettingsFromJsonFile("release-channel.json"); - return repo; + return std::make_unique(); } } // namespace UKControllerPlugin::Setting diff --git a/src/utils/setting/SettingRepositoryFactory.h b/src/utils/setting/SettingRepositoryFactory.h index afc82c1af..894d50a85 100644 --- a/src/utils/setting/SettingRepositoryFactory.h +++ b/src/utils/setting/SettingRepositoryFactory.h @@ -5,8 +5,8 @@ namespace UKControllerPlugin { namespace Windows { class WinApiInterface; - } // namespace Windows -} // namespace UKControllerPlugin + } // namespace Windows +} // namespace UKControllerPlugin namespace UKControllerPlugin { namespace Setting { @@ -18,9 +18,7 @@ namespace UKControllerPlugin { class SettingRepositoryFactory { public: - static std::unique_ptr Create( - UKControllerPlugin::Windows::WinApiInterface & winApi - ); + static std::unique_ptr Create(); }; - } // namespace Setting -} // namespace UKControllerPlugin + } // namespace Setting +} // namespace UKControllerPlugin diff --git a/src/utils/setting/SettingRepositoryInterface.h b/src/utils/setting/SettingRepositoryInterface.h new file mode 100644 index 000000000..83b98e4d9 --- /dev/null +++ b/src/utils/setting/SettingRepositoryInterface.h @@ -0,0 +1,17 @@ +#pragma once + +namespace UKControllerPlugin::Setting { + /** + * Interface for classes providing plugin settings + */ + class SettingRepositoryInterface + { + public: + virtual ~SettingRepositoryInterface() = default; + [[nodiscard]] virtual auto GetSetting(const std::string& setting, const std::string& defaultValue) const + -> std::string = 0; + [[nodiscard]] virtual auto HasSetting(const std::string& setting) const -> bool = 0; + virtual void UpdateSetting(const std::string& setting, const std::string& value) = 0; + virtual void ReloadSetting(const std::string& setting) = 0; + }; +} // namespace UKControllerPlugin::Setting diff --git a/src/utils/string/StringTrimFunctions.cpp b/src/utils/string/StringTrimFunctions.cpp new file mode 100644 index 000000000..1ca65883b --- /dev/null +++ b/src/utils/string/StringTrimFunctions.cpp @@ -0,0 +1,21 @@ +#include "StringTrimFunctions.h" + +namespace UKControllerPluginUtils::String { + + auto ltrim(const std::string& string, const std::string& charactersToTrim) -> std::string + { + size_t start = string.find_first_not_of(charactersToTrim); + return (start == std::string::npos) ? "" : string.substr(start); + } + + auto rtrim(const std::string& string, const std::string& charactersToTrim) -> std::string + { + size_t end = string.find_last_not_of(charactersToTrim); + return (end == std::string::npos) ? "" : string.substr(0, end + 1); + } + + auto trim(const std::string& string, const std::string& charactersToTrim) -> std::string + { + return ltrim(rtrim(string, charactersToTrim), charactersToTrim); + } +} // namespace UKControllerPluginUtils::String diff --git a/src/utils/string/StringTrimFunctions.h b/src/utils/string/StringTrimFunctions.h new file mode 100644 index 000000000..a89a75219 --- /dev/null +++ b/src/utils/string/StringTrimFunctions.h @@ -0,0 +1,12 @@ +#pragma once + +namespace UKControllerPluginUtils::String { + const std::string DEFAULT_CHARS_TO_TRIM = " \n\r\t\f\v"; + + [[nodiscard]] auto ltrim(const std::string& string, const std::string& charactersToTrim = DEFAULT_CHARS_TO_TRIM) + -> std::string; + [[nodiscard]] auto rtrim(const std::string& string, const std::string& charactersToTrim = DEFAULT_CHARS_TO_TRIM) + -> std::string; + [[nodiscard]] auto trim(const std::string& string, const std::string& charactersToTrim = DEFAULT_CHARS_TO_TRIM) + -> std::string; +} // namespace UKControllerPluginUtils::String diff --git a/test/plugin/CMakeLists.txt b/test/plugin/CMakeLists.txt index 6393bf151..18b96b671 100644 --- a/test/plugin/CMakeLists.txt +++ b/test/plugin/CMakeLists.txt @@ -24,7 +24,7 @@ source_group("test\\airfield" FILES ${test__airfield}) set(test__api "api/ApiConfigurationMenuItemTest.cpp" -) + api/BootstrapApiTest.cpp api/FirstTimeApiAuthorisationCheckerTest.cpp) source_group("test\\api" FILES ${test__api}) set(test__bootstrap @@ -178,7 +178,7 @@ set(test__hold "hold/MinStackHoldLevelRestrictionTest.cpp" "hold/PublishedHoldCollectionFactoryTest.cpp" "hold/PublishedHoldCollectionTest.cpp" - hold/AssignHoldCommandTest.cpp hold/AddToHoldCallsignProviderTest.cpp) + hold/AssignHoldCommandTest.cpp hold/AddToHoldCallsignProviderTest.cpp hold/CompareProximityHoldsTest.cpp hold/AircraftEnteredHoldingAreaEventHandlerTest.cpp hold/AircraftExitedHoldingAreaEventHandlerTest.cpp) source_group("test\\hold" FILES ${test__hold}) set(test__initialaltitude diff --git a/test/plugin/api/ApiConfigurationMenuItemTest.cpp b/test/plugin/api/ApiConfigurationMenuItemTest.cpp index 2fc97b71e..0015dfdbf 100644 --- a/test/plugin/api/ApiConfigurationMenuItemTest.cpp +++ b/test/plugin/api/ApiConfigurationMenuItemTest.cpp @@ -7,45 +7,80 @@ using ::testing::Test; using UKControllerPlugin::Api::ApiConfigurationMenuItem; using UKControllerPlugin::Plugin::PopupMenuItem; using UKControllerPluginTest::Windows::MockWinApi; +namespace UKControllerPluginTest::Api { -namespace UKControllerPluginTest { - namespace Api { - - class ApiConfigurationMenuItemTest : public Test - { - public: - ApiConfigurationMenuItemTest() : menuItem(mockWindows, 55) - { - } - NiceMock mockWindows; - ApiConfigurationMenuItem menuItem; - }; - - TEST_F(ApiConfigurationMenuItemTest, ItReturnsTheMenuItem) + class ApiConfigurationMenuItemTest : public ApiTestCase + { + public: + ApiConfigurationMenuItemTest() : ApiTestCase(), menuItem(this->SettingsProvider(), mockWindows, 55) { - PopupMenuItem expected; - expected.firstValue = "Replace Personal API Configuration"; - expected.secondValue = ""; - expected.callbackFunctionId = 55; - expected.checked = EuroScopePlugIn::POPUP_ELEMENT_NO_CHECKBOX; - expected.disabled = false; - expected.fixedPosition = false; - - EXPECT_TRUE(expected == this->menuItem.GetConfigurationMenuItem()); } + NiceMock mockWindows; + ApiConfigurationMenuItem menuItem; + }; - TEST_F(ApiConfigurationMenuItemTest, ConfigureStartsTheKeyReplacementProcedure) - { - EXPECT_CALL( - this->mockWindows, - OpenMessageBox( - testing::StrEq(L"Please select the key file to use, this will overwrite your previous key."), - testing::StrEq(L"UKCP Message"), - MB_OKCANCEL | MB_ICONINFORMATION)) - .Times(1) - .WillOnce(Return(IDCANCEL)); - - this->menuItem.Configure(55, "Test", {}); - } - } // namespace Api -} // namespace UKControllerPluginTest + TEST_F(ApiConfigurationMenuItemTest, ItReturnsTheMenuItem) + { + PopupMenuItem expected; + expected.firstValue = "Replace Personal API Configuration"; + expected.secondValue = ""; + expected.callbackFunctionId = 55; + expected.checked = EuroScopePlugIn::POPUP_ELEMENT_NO_CHECKBOX; + expected.disabled = false; + expected.fixedPosition = false; + + EXPECT_TRUE(expected == this->menuItem.GetConfigurationMenuItem()); + } + + TEST_F(ApiConfigurationMenuItemTest, ConfigureDoesntReplaceTheSettingsIfUserDoesntReload) + { + EXPECT_CALL(this->SettingsProvider(), Reload).Times(1).WillOnce(testing::Return(false)); + + this->ExpectNoApiRequests(); + + this->menuItem.Configure(55, "Test", {}); + } + + TEST_F(ApiConfigurationMenuItemTest, ConfigureReplacesTheApiSettings) + { + EXPECT_CALL(this->SettingsProvider(), Reload).Times(1).WillOnce(testing::Return(true)); + + this->ExpectApiRequest()->Get().To("authorise").WithoutBody().WillReturnOk(); + + EXPECT_CALL( + this->mockWindows, + OpenMessageBox(testing::_, testing::StrEq(L"Configuration Updated"), MB_OK | MB_ICONINFORMATION)) + .Times(1) + .WillOnce(Return(IDOK)); + + this->menuItem.Configure(55, "Test", {}); + } + + TEST_F(ApiConfigurationMenuItemTest, ConfigureHandlesAuthenticationErrorDuringAuthCheck) + { + EXPECT_CALL(this->SettingsProvider(), Reload).Times(1).WillOnce(testing::Return(true)); + + this->ExpectApiRequest()->Get().To("authorise").WithoutBody().WillReturnForbidden(); + + EXPECT_CALL( + this->mockWindows, + OpenMessageBox(testing::_, testing::StrEq(L"UKCP API Config Invalid"), MB_OK | MB_ICONWARNING)) + .Times(1) + .WillOnce(Return(IDOK)); + + this->menuItem.Configure(55, "Test", {}); + } + + TEST_F(ApiConfigurationMenuItemTest, ConfigureHandlesServerErrorDuringAuthCheck) + { + EXPECT_CALL(this->SettingsProvider(), Reload).Times(1).WillOnce(testing::Return(true)); + + this->ExpectApiRequest()->Get().To("authorise").WithoutBody().WillReturnServerError(); + + EXPECT_CALL(this->mockWindows, OpenMessageBox(testing::_, testing::StrEq(L"Server Error"), testing::_)) + .Times(1) + .WillOnce(Return(IDOK)); + + this->menuItem.Configure(55, "Test", {}); + } +} // namespace UKControllerPluginTest::Api diff --git a/test/plugin/api/BootstrapApiTest.cpp b/test/plugin/api/BootstrapApiTest.cpp new file mode 100644 index 000000000..173c12fa4 --- /dev/null +++ b/test/plugin/api/BootstrapApiTest.cpp @@ -0,0 +1,47 @@ +#include "api/BootstrapApi.h" +#include "bootstrap/PersistenceContainer.h" +#include "plugin/FunctionCallEventHandler.h" +#include "radarscreen/ConfigurableDisplayCollection.h" +#include "setting/SettingRepository.h" + +using UKControllerPlugin::Api::BootstrapApi; +using UKControllerPlugin::Api::BootstrapConfigurationMenuItem; +using UKControllerPlugin::Bootstrap::PersistenceContainer; +using UKControllerPlugin::Plugin::FunctionCallEventHandler; +using UKControllerPlugin::RadarScreen::ConfigurableDisplayCollection; +using UKControllerPlugin::Setting::SettingRepository; + +namespace UKControllerPluginTest::Api { + class BootstrapApiTest : public ApiTestCase + { + public: + BootstrapApiTest() : ApiTestCase() + { + container.windows = std::make_unique>(); + container.settingsRepository = std::make_unique(); + container.pluginFunctionHandlers = std::make_unique(); + } + ConfigurableDisplayCollection configurableDisplays; + PersistenceContainer container; + }; + + TEST_F(BootstrapApiTest, ItBootstrapsTheApiFactory) + { + BootstrapApi(container); + EXPECT_NE(nullptr, container.apiFactory); + } + + TEST_F(BootstrapApiTest, ItBootstrapsTheLegacyApiInterface) + { + BootstrapApi(container); + EXPECT_NE(nullptr, container.api); + } + + TEST_F(BootstrapApiTest, ItBootstrapsTheConfigurationMenuItem) + { + BootstrapApi(container); + BootstrapConfigurationMenuItem(container, configurableDisplays); + EXPECT_EQ(1, this->configurableDisplays.CountDisplays()); + EXPECT_TRUE(container.pluginFunctionHandlers->HasCallbackByDescription("API Configuration Menu Item Selected")); + } +} // namespace UKControllerPluginTest::Api diff --git a/test/plugin/api/FirstTimeApiAuthorisationCheckerTest.cpp b/test/plugin/api/FirstTimeApiAuthorisationCheckerTest.cpp new file mode 100644 index 000000000..c64c6bb81 --- /dev/null +++ b/test/plugin/api/FirstTimeApiAuthorisationCheckerTest.cpp @@ -0,0 +1,83 @@ +#include "api/FirstTimeApiAuthorisationChecker.h" +#include "api/ApiRequestData.h" +#include "api/ApiRequestException.h" +#include "api/Response.h" + +using UKControllerPlugin::Api::FirstTimeApiAuthorisationCheck; +using UKControllerPluginUtils::Api::ApiRequestData; +using UKControllerPluginUtils::Api::ApiRequestException; +using UKControllerPluginUtils::Api::Response; + +namespace UKControllerPluginTest::Api { + class FirstTimeApiAuthorisationCheckerTest : public ApiTestCase + { + public: + testing::NiceMock windows; + }; + + TEST_F(FirstTimeApiAuthorisationCheckerTest, ItPerformsASuccessfulCheck) + { + this->ExpectApiRequest()->Get().To("authorise").WithoutBody().WillReturnOk(); + + FirstTimeApiAuthorisationCheck(this->SettingsProvider(), windows); + } + + TEST_F(FirstTimeApiAuthorisationCheckerTest, ItHandlesAServerErrorDuringCheck) + { + EXPECT_CALL(windows, OpenMessageBox(testing::_, testing::StrEq(L"UKCP API Server Error"), testing::_)).Times(1); + + this->ExpectApiRequest()->Get().To("authorise").WithoutBody().WillReturnServerError(); + + FirstTimeApiAuthorisationCheck(this->SettingsProvider(), windows); + } + + TEST_F(FirstTimeApiAuthorisationCheckerTest, ItRetriesTheCheckIfTheUserReplacesConfig) + { + EXPECT_CALL(this->SettingsProvider(), Reload).Times(1).WillOnce(testing::Return(true)); + + EXPECT_CALL(windows, OpenMessageBox(testing::_, testing::StrEq(L"UKCP API Config Invalid"), testing::_)) + .Times(1) + .WillOnce(testing::Return(IDOK)); + + EXPECT_CALL(windows, OpenMessageBox(testing::_, testing::StrEq(L"UKCP API Config Not Updated"), testing::_)) + .Times(0); + + this->ExpectApiRequest()->Get().To("authorise").WithoutBody().WillReturnOk(); + + this->ExpectApiRequest()->Get().To("authorise").WithoutBody().WillReturnForbidden(); + + FirstTimeApiAuthorisationCheck(this->SettingsProvider(), windows); + } + + TEST_F(FirstTimeApiAuthorisationCheckerTest, ItDoesntRetryIfUserChoosesNotToReplaceConfigAfterAuthorisationFailure) + { + EXPECT_CALL(this->SettingsProvider(), Reload).Times(0); + + EXPECT_CALL(windows, OpenMessageBox(testing::_, testing::StrEq(L"UKCP API Config Invalid"), testing::_)) + .Times(1) + .WillOnce(testing::Return(IDCANCEL)); + + EXPECT_CALL(windows, OpenMessageBox(testing::_, testing::StrEq(L"UKCP API Config Not Updated"), testing::_)) + .Times(1); + + this->ExpectApiRequest()->Get().To("authorise").WithoutBody().WillReturnForbidden(); + + FirstTimeApiAuthorisationCheck(this->SettingsProvider(), windows); + } + + TEST_F(FirstTimeApiAuthorisationCheckerTest, ItDoesntRetryIfConfigReloadDoesntHappenAfterAuthorisationFailure) + { + EXPECT_CALL(this->SettingsProvider(), Reload).Times(1).WillOnce(testing::Return(false)); + + EXPECT_CALL(windows, OpenMessageBox(testing::_, testing::StrEq(L"UKCP API Config Invalid"), testing::_)) + .Times(1) + .WillOnce(testing::Return(IDOK)); + + EXPECT_CALL(windows, OpenMessageBox(testing::_, testing::StrEq(L"UKCP API Config Not Updated"), testing::_)) + .Times(1); + + this->ExpectApiRequest()->Get().To("authorise").WithoutBody().WillReturnForbidden(); + + FirstTimeApiAuthorisationCheck(this->SettingsProvider(), windows); + } +} // namespace UKControllerPluginTest::Api diff --git a/test/plugin/bootstrap/HelperBootstrapTest.cpp b/test/plugin/bootstrap/HelperBootstrapTest.cpp index ba0265306..10764c714 100644 --- a/test/plugin/bootstrap/HelperBootstrapTest.cpp +++ b/test/plugin/bootstrap/HelperBootstrapTest.cpp @@ -31,77 +31,15 @@ namespace UKControllerPluginTest::Bootstrap { std::unique_ptr> mockWinApi; }; - TEST_F(HelperBootstrapTest, BootstrapCreatesApiHelper) - { - EXPECT_CALL(*this->mockWinApi, FileExists(std::wstring(L"settings/release-channel.json"))) - .Times(1) - .WillOnce(Return(false)); - - EXPECT_CALL(*this->mockWinApi, FileExists(std::wstring(L"settings/api-settings.json"))) - .Times(1) - .WillOnce(Return(true)); - - EXPECT_CALL(*this->mockWinApi, ReadFromFileMock(std::wstring(L"settings/api-settings.json"), true)) - .Times(1) - .WillOnce(Return("{\"api-url\": \"testurl\", \"api-key\": \"testkey\"}")); - this->container.windows = std::move(this->mockWinApi); - - HelperBootstrap::Bootstrap(container); - EXPECT_TRUE("testurl" == this->container.api->GetApiDomain()); - } - TEST_F(HelperBootstrapTest, BootstrapCreatesSettingsRepository) { - EXPECT_CALL(*this->mockWinApi, FileExists(std::wstring(L"settings/release-channel.json"))) - .Times(1) - .WillOnce(Return(false)); - - EXPECT_CALL(*this->mockWinApi, FileExists(std::wstring(L"settings/api-settings.json"))) - .Times(1) - .WillOnce(Return(true)); - - EXPECT_CALL(*this->mockWinApi, ReadFromFileMock(std::wstring(L"settings/api-settings.json"), true)) - .Times(1) - .WillOnce(Return("{\"api-url\": \"testurl\", \"api-key\": \"testkey\"}")); - this->container.windows = std::move(this->mockWinApi); - - HelperBootstrap::Bootstrap(container); - EXPECT_EQ("testkey", this->container.settingsRepository->GetSetting("api-key")); + HelperBootstrap::Bootstrap(this->container); + EXPECT_NE(nullptr, this->container.settingsRepository); } TEST_F(HelperBootstrapTest, BootstrapCreatesTaskRunner) { - EXPECT_CALL(*this->mockWinApi, FileExists(std::wstring(L"settings/release-channel.json"))) - .Times(1) - .WillOnce(Return(false)); - - EXPECT_CALL(*this->mockWinApi, FileExists(std::wstring(L"settings/api-settings.json"))) - .Times(1) - .WillOnce(Return(true)); - - EXPECT_CALL(*this->mockWinApi, ReadFromFileMock(std::wstring(L"settings/api-settings.json"), true)) - .Times(1) - .WillOnce(Return("{\"api-key\": \"testkey\", \"api-url\": \"testurl\"}")); - this->container.windows = std::move(this->mockWinApi); - HelperBootstrap::Bootstrap(this->container); EXPECT_EQ(3, this->container.taskRunner->CountThreads()); } - - TEST_F(HelperBootstrapTest, BootstrapApiConfigurationItemAddsToConfigurables) - { - this->container.windows = std::move(this->mockWinApi); - HelperBootstrap::BootstrapApiConfigurationItem(this->container, this->configurables); - - EXPECT_EQ(1, this->configurables.CountDisplays()); - } - - TEST_F(HelperBootstrapTest, BootstrapApiConfigurationItemAddsCallbackFunctions) - { - this->container.windows = std::move(this->mockWinApi); - HelperBootstrap::BootstrapApiConfigurationItem(this->container, this->configurables); - - EXPECT_EQ(1, this->container.pluginFunctionHandlers->CountCallbacks()); - EXPECT_TRUE(this->container.pluginFunctionHandlers->HasCallbackFunction(5000)); - } } // namespace UKControllerPluginTest::Bootstrap diff --git a/test/plugin/bootstrap/LocateApiSettingsTest.cpp b/test/plugin/bootstrap/LocateApiSettingsTest.cpp index a629e8960..957dd2a28 100644 --- a/test/plugin/bootstrap/LocateApiSettingsTest.cpp +++ b/test/plugin/bootstrap/LocateApiSettingsTest.cpp @@ -1,6 +1,6 @@ #include "bootstrap/LocateApiSettings.h" #include "setting/SettingRepository.h" -#include "setting/SettingValue.h" +#include "setting/Setting.h" #include "helper/Matchers.h" using testing::_; diff --git a/test/plugin/euroscope/GeneralSettingsConfigurationBootstrapTest.cpp b/test/plugin/euroscope/GeneralSettingsConfigurationBootstrapTest.cpp index 505af2484..98bf62a52 100644 --- a/test/plugin/euroscope/GeneralSettingsConfigurationBootstrapTest.cpp +++ b/test/plugin/euroscope/GeneralSettingsConfigurationBootstrapTest.cpp @@ -28,7 +28,7 @@ namespace UKControllerPluginTest { { public: GeneralSettingsConfigurationBootstrapTest() - : userSettings(mockUserSettingProvider), settings(mockWindows), dialogManager(mockDialogProvider) + : userSettings(mockUserSettingProvider), settings(), dialogManager(mockDialogProvider) { } @@ -71,7 +71,7 @@ namespace UKControllerPluginTest { TEST_F(GeneralSettingsConfigurationBootstrapTest, BootstrapPluginAddsDialogToDialogManager) { GeneralSettingsConfigurationBootstrap::BootstrapPlugin( - this->dialogManager, this->userSettings, this->userSettingCollection, settings); + this->dialogManager, this->userSettings, this->userSettingCollection, settings, mockWindows); EXPECT_EQ(1, this->dialogManager.CountDialogs()); EXPECT_TRUE(this->dialogManager.HasDialog(IDD_GENERAL_SETTINGS)); diff --git a/test/plugin/hold/AircraftEnteredHoldingAreaEventHandlerTest.cpp b/test/plugin/hold/AircraftEnteredHoldingAreaEventHandlerTest.cpp new file mode 100644 index 000000000..1916c2ef6 --- /dev/null +++ b/test/plugin/hold/AircraftEnteredHoldingAreaEventHandlerTest.cpp @@ -0,0 +1,141 @@ +#include "hold/AircraftEnteredHoldingAreaEventHandler.h" +#include "hold/HoldManager.h" +#include "hold/HoldingAircraft.h" +#include "hold/ProximityHold.h" +#include "navaids/Navaid.h" +#include "navaids/NavaidCollection.h" +#include "push/PushEvent.h" +#include "push/PushEventSubscription.h" +#include "time/SystemClock.h" +#include "time/ParseTimeStrings.h" + +using UKControllerPlugin::Hold::AircraftEnteredHoldingAreaEventHandler; +using UKControllerPlugin::Hold::HoldManager; +using UKControllerPlugin::Navaids::Navaid; +using UKControllerPlugin::Navaids::NavaidCollection; +using UKControllerPlugin::Push::PushEvent; +using UKControllerPlugin::Push::PushEventSubscription; +using UKControllerPlugin::Time::ParseIsoZuluString; +using UKControllerPlugin::Time::SetTestNow; +using UKControllerPlugin::Time::TimeNow; + +namespace UKControllerPluginTest::Hold { + class AircraftEnteredHoldingAreaEventHandlerTest : public testing::Test + { + + public: + AircraftEnteredHoldingAreaEventHandlerTest() + : navaid({1, "BNN", EuroScopePlugIn::CPosition()}), holdManager(api, taskRunner), + handler(holdManager, navaids) + { + SetTestNow(TimeNow()); + navaids.AddNavaid(navaid); + } + + /* + * Make an event based on the merge of some base data and overriding data so we dont + * have to repeat ourselves + */ + [[nodiscard]] static auto MakePushEvent( + const nlohmann::json& overridingData = nlohmann::json::object(), const std::string& keyToRemove = "") + -> PushEvent + { + nlohmann::json eventData{ + {"callsign", "BAW123"}, {"navaid_id", 1}, {"entered_at", "2021-01-09T01:02:03.000000Z"}}; + if (overridingData.is_object()) { + eventData.update(overridingData); + } else { + eventData = overridingData; + } + + if (!keyToRemove.empty()) { + eventData.erase(eventData.find(keyToRemove)); + } + + return {"hold.area-entered", "test", eventData, eventData.dump()}; + }; + + Navaid navaid; + testing::NiceMock api; + testing::NiceMock taskRunner; + HoldManager holdManager; + NavaidCollection navaids; + AircraftEnteredHoldingAreaEventHandler handler; + }; + + TEST_F(AircraftEnteredHoldingAreaEventHandlerTest, ItHasPushEventSubscriptions) + { + std::set expected = { + {PushEventSubscription::SUB_TYPE_EVENT, + "hold" + ".area-entered"}}; + EXPECT_EQ(expected, handler.GetPushEventSubscriptions()); + } + + TEST_F(AircraftEnteredHoldingAreaEventHandlerTest, ItProcessesPushEvents) + { + handler.ProcessPushEvent(MakePushEvent()); + EXPECT_EQ(1, holdManager.CountHoldingAircraft()); + const auto holdingAircraft = holdManager.GetHoldingAircraft("BAW123"); + EXPECT_NE(nullptr, holdingAircraft); + const auto proximityHold = holdingAircraft->GetProximityHold("BNN"); + EXPECT_NE(nullptr, proximityHold); + EXPECT_EQ("BAW123", proximityHold->callsign); + EXPECT_EQ("BNN", proximityHold->navaid); + EXPECT_EQ(ParseIsoZuluString("2021-01-09T01:02:03.000000Z"), proximityHold->enteredAt); + } + + TEST_F(AircraftEnteredHoldingAreaEventHandlerTest, ItHandlesPushEventDataNotObject) + { + handler.ProcessPushEvent({"hold.area-entered", "test", nlohmann::json::array(), "[]"}); + EXPECT_EQ(0, holdManager.CountHoldingAircraft()); + } + + TEST_F(AircraftEnteredHoldingAreaEventHandlerTest, ItHandlesPushEventMissingCallsign) + { + handler.ProcessPushEvent(MakePushEvent(nlohmann::json::object(), "callsign")); + EXPECT_EQ(0, holdManager.CountHoldingAircraft()); + } + + TEST_F(AircraftEnteredHoldingAreaEventHandlerTest, ItHandlesPushEventNavaidIdNotAString) + { + handler.ProcessPushEvent(MakePushEvent(nlohmann::json::object({{"callsign", 123}}))); + EXPECT_EQ(0, holdManager.CountHoldingAircraft()); + } + + TEST_F(AircraftEnteredHoldingAreaEventHandlerTest, ItHandlesPushEventMissingNavaidId) + { + handler.ProcessPushEvent(MakePushEvent(nlohmann::json::object(), "navaid_id")); + EXPECT_EQ(0, holdManager.CountHoldingAircraft()); + } + + TEST_F(AircraftEnteredHoldingAreaEventHandlerTest, ItHandlesPushEventNavaidIdNotAnInteger) + { + handler.ProcessPushEvent(MakePushEvent(nlohmann::json::object({{"navaid_id", "abc"}}))); + EXPECT_EQ(0, holdManager.CountHoldingAircraft()); + } + + TEST_F(AircraftEnteredHoldingAreaEventHandlerTest, ItHandlesPushEventNavaidIdNotAValidNavaid) + { + handler.ProcessPushEvent(MakePushEvent(nlohmann::json::object({{"navaid_id", 123}}))); + EXPECT_EQ(0, holdManager.CountHoldingAircraft()); + } + + TEST_F(AircraftEnteredHoldingAreaEventHandlerTest, ItHandlesPushEventMissingEnteredAt) + { + handler.ProcessPushEvent(MakePushEvent(nlohmann::json::object(), "entered_at")); + EXPECT_EQ(0, holdManager.CountHoldingAircraft()); + } + + TEST_F(AircraftEnteredHoldingAreaEventHandlerTest, ItHandlesPushEventEnteredAtNotAString) + { + handler.ProcessPushEvent(MakePushEvent(nlohmann::json::object({{"entered_at", 123}}))); + EXPECT_EQ(0, holdManager.CountHoldingAircraft()); + } + + TEST_F(AircraftEnteredHoldingAreaEventHandlerTest, ItHandlesPushEventEnteredAtNotAValidTimestamp) + { + handler.ProcessPushEvent(MakePushEvent(nlohmann::json::object({{"entered_at", "abc"}}))); + EXPECT_EQ(0, holdManager.CountHoldingAircraft()); + } +} // namespace UKControllerPluginTest::Hold diff --git a/test/plugin/hold/AircraftExitedHoldingAreaEventHandlerTest.cpp b/test/plugin/hold/AircraftExitedHoldingAreaEventHandlerTest.cpp new file mode 100644 index 000000000..8ea17b37d --- /dev/null +++ b/test/plugin/hold/AircraftExitedHoldingAreaEventHandlerTest.cpp @@ -0,0 +1,112 @@ +#include "hold/AircraftExitedHoldingAreaEventHandler.h" +#include "hold/HoldManager.h" +#include "hold/HoldingAircraft.h" +#include "hold/ProximityHold.h" +#include "navaids/Navaid.h" +#include "navaids/NavaidCollection.h" +#include "push/PushEvent.h" +#include "push/PushEventSubscription.h" + +using UKControllerPlugin::Hold::AircraftExitedHoldingAreaEventHandler; +using UKControllerPlugin::Hold::HoldManager; +using UKControllerPlugin::Hold::ProximityHold; +using UKControllerPlugin::Navaids::Navaid; +using UKControllerPlugin::Navaids::NavaidCollection; +using UKControllerPlugin::Push::PushEvent; +using UKControllerPlugin::Push::PushEventSubscription; + +namespace UKControllerPluginTest::Hold { + class AircraftExitedHoldingAreaEventHandlerTest : public testing::Test + { + + public: + AircraftExitedHoldingAreaEventHandlerTest() + : navaid({1, "BNN", EuroScopePlugIn::CPosition()}), holdManager(api, taskRunner), + handler(holdManager, navaids) + { + this->navaids.AddNavaid(navaid); + this->holdManager.AddAircraftToProximityHold( + std::make_shared("BAW123", "BNN", std::chrono::system_clock::now())); + } + + /* + * Make an event based on the merge of some base data and overriding data so we dont + * have to repeat ourselves + */ + [[nodiscard]] static auto MakePushEvent( + const nlohmann::json& overridingData = nlohmann::json::object(), const std::string& keyToRemove = "") + -> PushEvent + { + nlohmann::json eventData{{"callsign", "BAW123"}, {"navaid_id", 1}}; + if (overridingData.is_object()) { + eventData.update(overridingData); + } else { + eventData = overridingData; + } + + if (!keyToRemove.empty()) { + eventData.erase(eventData.find(keyToRemove)); + } + + return {"hold.area-exited", "test", eventData, eventData.dump()}; + }; + + Navaid navaid; + NavaidCollection navaids; + testing::NiceMock api; + testing::NiceMock taskRunner; + HoldManager holdManager; + AircraftExitedHoldingAreaEventHandler handler; + }; + + TEST_F(AircraftExitedHoldingAreaEventHandlerTest, ItHasPushEventSubscriptions) + { + std::set expected = { + {PushEventSubscription::SUB_TYPE_EVENT, + "hold" + ".area-exited"}}; + EXPECT_EQ(expected, handler.GetPushEventSubscriptions()); + } + + TEST_F(AircraftExitedHoldingAreaEventHandlerTest, ItProcessesPushEvents) + { + handler.ProcessPushEvent(MakePushEvent()); + EXPECT_EQ(0, holdManager.CountHoldingAircraft()); + } + + TEST_F(AircraftExitedHoldingAreaEventHandlerTest, ItHandlesPushEventDataNotObject) + { + handler.ProcessPushEvent({"hold.area-entered", "test", nlohmann::json::array(), "[]"}); + EXPECT_EQ(1, holdManager.CountHoldingAircraft()); + } + + TEST_F(AircraftExitedHoldingAreaEventHandlerTest, ItHandlesPushEventMissingCallsign) + { + handler.ProcessPushEvent(MakePushEvent(nlohmann::json::object(), "callsign")); + EXPECT_EQ(1, holdManager.CountHoldingAircraft()); + } + + TEST_F(AircraftExitedHoldingAreaEventHandlerTest, ItHandlesPushEventNavaidIdNotAString) + { + handler.ProcessPushEvent(MakePushEvent(nlohmann::json::object({{"callsign", 123}}))); + EXPECT_EQ(1, holdManager.CountHoldingAircraft()); + } + + TEST_F(AircraftExitedHoldingAreaEventHandlerTest, ItHandlesPushEventMissingNavaidId) + { + handler.ProcessPushEvent(MakePushEvent(nlohmann::json::object(), "navaid_id")); + EXPECT_EQ(1, holdManager.CountHoldingAircraft()); + } + + TEST_F(AircraftExitedHoldingAreaEventHandlerTest, ItHandlesPushEventNavaidIdNotAnInteger) + { + handler.ProcessPushEvent(MakePushEvent(nlohmann::json::object({{"navaid_id", "abc"}}))); + EXPECT_EQ(1, holdManager.CountHoldingAircraft()); + } + + TEST_F(AircraftExitedHoldingAreaEventHandlerTest, ItHandlesPushEventNavaidIdNotAValidNavaid) + { + handler.ProcessPushEvent(MakePushEvent(nlohmann::json::object({{"navaid_id", 123}}))); + EXPECT_EQ(1, holdManager.CountHoldingAircraft()); + } +} // namespace UKControllerPluginTest::Hold diff --git a/test/plugin/hold/CompareProximityHoldsTest.cpp b/test/plugin/hold/CompareProximityHoldsTest.cpp new file mode 100644 index 000000000..304bcde93 --- /dev/null +++ b/test/plugin/hold/CompareProximityHoldsTest.cpp @@ -0,0 +1,34 @@ +#include "hold/CompareProximityHolds.h" +#include "hold/ProximityHold.h" +#include "time/SystemClock.h" + +using UKControllerPlugin::Hold::CompareProximityHolds; +using UKControllerPlugin::Hold::ProximityHold; +using UKControllerPlugin::Time::TimeNow; + +namespace UKControllerPluginTest::Hold { + class CompareProximityHoldsTest : public testing::Test + { + public: + CompareProximityHolds compare; + }; + + TEST_F(CompareProximityHoldsTest, LessThanStringReturnsTrueIfLessThan) + { + std::shared_ptr aircraft = std::make_shared("BAW123", "BNM", TimeNow()); + EXPECT_TRUE(compare(aircraft, "BNN")); + } + + TEST_F(CompareProximityHoldsTest, LessThanStructReturnsTrueIfLessThan) + { + std::shared_ptr aircraft = std::make_shared("BAW124", "BNN", TimeNow()); + EXPECT_TRUE(compare("BNM", aircraft)); + } + + TEST_F(CompareProximityHoldsTest, CompareReturnsTrueIfFirstLessThanLast) + { + std::shared_ptr aircraft1 = std::make_shared("BAW123", "BNM", TimeNow()); + std::shared_ptr aircraft2 = std::make_shared("BAW124", "BNN", TimeNow()); + EXPECT_TRUE(compare(aircraft1, aircraft2)); + } +} // namespace UKControllerPluginTest::Hold diff --git a/test/plugin/hold/HoldDisplayTest.cpp b/test/plugin/hold/HoldDisplayTest.cpp index 0463e25f4..475856de3 100644 --- a/test/plugin/hold/HoldDisplayTest.cpp +++ b/test/plugin/hold/HoldDisplayTest.cpp @@ -10,6 +10,7 @@ #include "hold/CompareHoldingAircraft.h" #include "hold/HoldingData.h" #include "hold/PublishedHoldCollection.h" +#include "hold/ProximityHold.h" using testing::_; using testing::NiceMock; @@ -25,6 +26,7 @@ using UKControllerPlugin::Hold::HoldDisplay; using UKControllerPlugin::Hold::HoldingAircraft; using UKControllerPlugin::Hold::HoldingData; using UKControllerPlugin::Hold::HoldManager; +using UKControllerPlugin::Hold::ProximityHold; using UKControllerPlugin::Hold::PublishedHoldCollection; using UKControllerPlugin::Navaids::Navaid; using UKControllerPluginTest::Api::MockApiInterface; @@ -572,7 +574,8 @@ namespace UKControllerPluginTest { this->display.GetDisplayPos().x, this->display.GetDisplayPos().y, this->display.windowWidth, 380}; std::map, CompareHoldingAircraft>> aircraft; aircraft[7000].insert(std::make_shared("BAW123", "TIMBA")); - aircraft[7000].insert(std::make_shared("EZY234", std::set())); + aircraft[7000].insert(std::make_shared( + "EZY234", std::make_shared("BAW123", "TIMBA", std::chrono::system_clock::now()))); aircraft[8000].insert(std::make_shared("VIR25A", "TIMBA")); aircraft[8000].insert(std::make_shared("LOT123", "TIMBA")); aircraft[8000].insert(std::make_shared("RYR93", "TIMBA")); @@ -587,8 +590,10 @@ namespace UKControllerPluginTest { { std::set, CompareHoldingAircraft> aircraft; aircraft.insert(std::make_shared("BAW123", "TIMBA")); - aircraft.insert(std::make_shared("EZY234", std::set({"TIMBA"}))); - aircraft.insert(std::make_shared("VIR25A", std::set({"TIMBA"}))); + aircraft.insert(std::make_shared( + "EZY234", std::make_shared("EZY234", "TIMBA", std::chrono::system_clock::now()))); + aircraft.insert(std::make_shared( + "VIR25A", std::make_shared("VIR25A", "TIMBA", std::chrono::system_clock::now()))); aircraft.insert(std::make_shared("RYR191", "TIMBA")); aircraft.insert(std::make_shared("BMI234", "TIMBA")); aircraft.insert(std::make_shared("LOT555", "TIMBA")); @@ -674,7 +679,8 @@ namespace UKControllerPluginTest { std::set, CompareHoldingAircraft> aircraft; aircraft.insert(std::make_shared("BAW123", "TIMBA")); - auto conflictingAircraft = std::make_shared("EZY234", std::set({"TIMBA"})); + auto conflictingAircraft = std::make_shared( + "EZY234", std::make_shared("EZY234", "TIMBA", std::chrono::system_clock::now())); conflictingAircraft->SetAssignedHold("WILLO"); aircraft.insert(conflictingAircraft); @@ -729,7 +735,8 @@ namespace UKControllerPluginTest { std::set, CompareHoldingAircraft> aircraft; aircraft.insert(std::make_shared("BAW123", "TIMBA")); - auto conflictingAircraft = std::make_shared("EZY234", std::set({"TIMBA"})); + auto conflictingAircraft = std::make_shared( + "EZY234", std::make_shared("EZY234", "TIMBA", std::chrono::system_clock::now())); conflictingAircraft->SetAssignedHold("WILLO"); aircraft.insert(conflictingAircraft); @@ -785,7 +792,8 @@ namespace UKControllerPluginTest { std::set, CompareHoldingAircraft> aircraft; aircraft.insert(std::make_shared("BAW123", "TIMBA")); - auto conflictingAircraft = std::make_shared("EZY234", std::set({"TIMBA"})); + auto conflictingAircraft = std::make_shared( + "EZY234", std::make_shared("EZY234", "TIMBA", std::chrono::system_clock::now())); conflictingAircraft->SetAssignedHold("WILLO"); aircraft.insert(conflictingAircraft); @@ -842,7 +850,8 @@ namespace UKControllerPluginTest { std::set, CompareHoldingAircraft> aircraft; aircraft.insert(std::make_shared("BAW123", "TIMBA")); - auto conflictingAircraft = std::make_shared("EZY234", std::set({"TIMBA"})); + auto conflictingAircraft = std::make_shared( + "EZY234", std::make_shared("EZY234", "TIMBA", std::chrono::system_clock::now())); conflictingAircraft->SetAssignedHold("WILLO"); aircraft.insert(conflictingAircraft); @@ -904,7 +913,8 @@ namespace UKControllerPluginTest { std::set, CompareHoldingAircraft> aircraft; aircraft.insert(std::make_shared("BAW123", "TIMBA")); - auto conflictingAircraft = std::make_shared("EZY234", std::set({"TIMBA"})); + auto conflictingAircraft = std::make_shared( + "EZY234", std::make_shared("EZY234", "TIMBA", std::chrono::system_clock::now())); conflictingAircraft->SetAssignedHold("MAY"); aircraft.insert(conflictingAircraft); @@ -961,10 +971,12 @@ namespace UKControllerPluginTest { std::set, CompareHoldingAircraft> aircraft; aircraft.insert(std::make_shared("BAW123", "TIMBA")); - auto conflictingAircraft = std::make_shared("EZY234", std::set({"TIMBA"})); + auto conflictingAircraft = std::make_shared( + "EZY234", std::make_shared("EZY234", "TIMBA", std::chrono::system_clock::now())); conflictingAircraft->SetAssignedHold("WILLO"); aircraft.insert(conflictingAircraft); - auto conflictingAircraft2 = std::make_shared("BAW012", std::set({"TIMBA"})); + auto conflictingAircraft2 = std::make_shared( + "BAW012", std::make_shared("BAW012", "TIMBA", std::chrono::system_clock::now())); conflictingAircraft2->SetAssignedHold("WILLO"); aircraft.insert(conflictingAircraft2); diff --git a/test/plugin/hold/HoldEventHandlerTest.cpp b/test/plugin/hold/HoldEventHandlerTest.cpp index 8bd221c0e..f0937ecc8 100644 --- a/test/plugin/hold/HoldEventHandlerTest.cpp +++ b/test/plugin/hold/HoldEventHandlerTest.cpp @@ -3,13 +3,15 @@ #include "hold/HoldingAircraft.h" #include "hold/HoldManager.h" #include "hold/HoldEventHandler.h" +#include "hold/HoldingData.h" +#include "hold/ProximityHold.h" #include "plugin/PopupMenuItem.h" #include "navaids/NavaidCollection.h" -#include "hold/HoldingData.h" #include "push/PushEventSubscription.h" #include "push/PushEvent.h" #include "sectorfile/SectorFileCoordinates.h" #include "tag/TagData.h" +#include "time/SystemClock.h" using ::testing::_; using ::testing::NiceMock; @@ -19,12 +21,15 @@ using ::testing::Throw; using UKControllerPlugin::Hold::HoldEventHandler; using UKControllerPlugin::Hold::HoldingData; using UKControllerPlugin::Hold::HoldManager; +using UKControllerPlugin::Hold::ProximityHold; using UKControllerPlugin::Navaids::NavaidCollection; using UKControllerPlugin::Plugin::PopupMenuItem; using UKControllerPlugin::Push::PushEvent; using UKControllerPlugin::Push::PushEventSubscription; using UKControllerPlugin::SectorFile::ParseSectorFileCoordinates; using UKControllerPlugin::Tag::TagData; +using UKControllerPlugin::Time::SetTestNow; +using UKControllerPlugin::Time::TimeNow; using UKControllerPluginTest::Api::MockApiInterface; using UKControllerPluginTest::Euroscope::MockEuroScopeCFlightPlanInterface; using UKControllerPluginTest::Euroscope::MockEuroScopeCRadarTargetInterface; @@ -61,6 +66,8 @@ namespace UKControllerPluginTest { ON_CALL(this->mockFlightplan, GetCallsign()).WillByDefault(Return("BAW123")); this->manager.AssignAircraftToHold("BAW123", "TIMBA", false); + + SetTestNow(TimeNow()); } void CreateFlightplanRadarTargetPair(std::string callsign, EuroScopePlugIn::CPosition position) @@ -116,7 +123,7 @@ namespace UKControllerPluginTest { TEST_F(HoldEventHandlerTest, ItReturnsNoHoldIfAircraftNotAssignedToHold) { this->manager.UnassignAircraftFromHold("BAW123", false); - this->manager.AddAircraftToProximityHold("BAW123", "TIMBA"); + this->manager.AddAircraftToProximityHold(std::make_shared("BAW123", "TIMBA", TimeNow())); handler.SetTagItemData(this->tagData); EXPECT_EQ("NOHOLD", this->tagData.GetItemString()); } @@ -314,15 +321,45 @@ namespace UKControllerPluginTest { this->handler.TimedEventTrigger(); - std::set expectedProximityHoldsEzy234({"MAY", "OLEVI", "TIMBA"}); + // EZY234 EXPECT_EQ("EZY234", (*this->manager.GetAircraftForHold("TIMBA").cbegin())->GetCallsign()); EXPECT_EQ("EZY234", (*this->manager.GetAircraftForHold("MAY").cbegin())->GetCallsign()); - EXPECT_EQ(expectedProximityHoldsEzy234, this->manager.GetHoldingAircraft("EZY234")->GetProximityHolds()); - std::set expectedProximityHoldsRyr123({"MAY", "TIMBA"}); + EXPECT_EQ(3, this->manager.GetHoldingAircraft("EZY234")->GetProximityHolds().size()); + auto mayfieldEzy234 = this->manager.GetHoldingAircraft("EZY234")->GetProximityHold("MAY"); + EXPECT_NE(nullptr, mayfieldEzy234); + EXPECT_EQ("EZY234", mayfieldEzy234->callsign); + EXPECT_EQ("MAY", mayfieldEzy234->navaid); + EXPECT_EQ(TimeNow(), mayfieldEzy234->enteredAt); + + auto oleviEzy234 = this->manager.GetHoldingAircraft("EZY234")->GetProximityHold("OLEVI"); + EXPECT_NE(nullptr, oleviEzy234); + EXPECT_EQ("EZY234", oleviEzy234->callsign); + EXPECT_EQ("OLEVI", oleviEzy234->navaid); + EXPECT_EQ(TimeNow(), oleviEzy234->enteredAt); + + auto timbaEzy234 = this->manager.GetHoldingAircraft("EZY234")->GetProximityHold("TIMBA"); + EXPECT_NE(nullptr, timbaEzy234); + EXPECT_EQ("EZY234", timbaEzy234->callsign); + EXPECT_EQ("TIMBA", timbaEzy234->navaid); + EXPECT_EQ(TimeNow(), timbaEzy234->enteredAt); + + // RYR123 EXPECT_EQ("RYR123", (*++this->manager.GetAircraftForHold("TIMBA").cbegin())->GetCallsign()); EXPECT_EQ("RYR123", (*++this->manager.GetAircraftForHold("MAY").cbegin())->GetCallsign()); - EXPECT_EQ(expectedProximityHoldsRyr123, this->manager.GetHoldingAircraft("RYR123")->GetProximityHolds()); + + EXPECT_EQ(2, this->manager.GetHoldingAircraft("RYR123")->GetProximityHolds().size()); + auto mayfieldRyr123 = this->manager.GetHoldingAircraft("RYR123")->GetProximityHold("MAY"); + EXPECT_NE(nullptr, mayfieldRyr123); + EXPECT_EQ("RYR123", mayfieldRyr123->callsign); + EXPECT_EQ("MAY", mayfieldRyr123->navaid); + EXPECT_EQ(TimeNow(), mayfieldRyr123->enteredAt); + + auto timbaRyr123 = this->manager.GetHoldingAircraft("RYR123")->GetProximityHold("TIMBA"); + EXPECT_NE(nullptr, timbaRyr123); + EXPECT_EQ("RYR123", timbaRyr123->callsign); + EXPECT_EQ("TIMBA", timbaRyr123->navaid); + EXPECT_EQ(TimeNow(), timbaRyr123->enteredAt); } TEST_F(HoldEventHandlerTest, TimedEventRemovesAircraftFromProximityHoldsIfNotCloseEnough) @@ -331,18 +368,16 @@ namespace UKControllerPluginTest { this->CreateFlightplanRadarTargetPair( "RYR123", ParseSectorFileCoordinates("N050.57.18.900", "W001.20.42.200")); - this->manager.AddAircraftToProximityHold("RYR123", "OLEVI"); - this->manager.AddAircraftToProximityHold("RYR123", "MAY"); + this->manager.AddAircraftToProximityHold(std::make_shared("RYR123", "OLEVI", TimeNow())); + this->manager.AddAircraftToProximityHold(std::make_shared("RYR123", "MAY", TimeNow())); this->handler.TimedEventTrigger(); - std::set expectedProximityHolds({ - "SAM", - }); EXPECT_EQ("RYR123", (*this->manager.GetAircraftForHold("SAM").cbegin())->GetCallsign()); EXPECT_EQ(0, this->manager.GetAircraftForHold("OLEVI").size()); EXPECT_EQ(0, this->manager.GetAircraftForHold("MAY").size()); - EXPECT_EQ(expectedProximityHolds, this->manager.GetHoldingAircraft("RYR123")->GetProximityHolds()); + EXPECT_EQ(1, this->manager.GetHoldingAircraft("RYR123")->GetProximityHolds().size()); + EXPECT_NE(nullptr, this->manager.GetHoldingAircraft("RYR123")->GetProximityHold("SAM")); } } // namespace Hold } // namespace UKControllerPluginTest diff --git a/test/plugin/hold/HoldManagerTest.cpp b/test/plugin/hold/HoldManagerTest.cpp index 72d96befb..bf330e6cf 100644 --- a/test/plugin/hold/HoldManagerTest.cpp +++ b/test/plugin/hold/HoldManagerTest.cpp @@ -1,6 +1,8 @@ +#include "api/ApiException.h" #include "hold/HoldingAircraft.h" #include "hold/HoldManager.h" -#include "api/ApiException.h" +#include "hold/ProximityHold.h" +#include "time/SystemClock.h" using ::testing::_; using ::testing::NiceMock; @@ -10,6 +12,9 @@ using ::testing::Throw; using UKControllerPlugin::Api::ApiException; using UKControllerPlugin::Hold::HoldingAircraft; using UKControllerPlugin::Hold::HoldManager; +using UKControllerPlugin::Hold::ProximityHold; +using UKControllerPlugin::Time::SetTestNow; +using UKControllerPlugin::Time::TimeNow; using UKControllerPluginTest::Api::MockApiInterface; using UKControllerPluginTest::Euroscope::MockEuroScopeCFlightPlanInterface; using UKControllerPluginTest::Euroscope::MockEuroScopeCRadarTargetInterface; @@ -30,6 +35,7 @@ namespace UKControllerPluginTest { ON_CALL(mockFlightplan2, GetCallsign()).WillByDefault(Return("EZY234")); this->manager.AssignAircraftToHold("EZY234", "TIMBA", false); + SetTestNow(TimeNow()); } NiceMock mockApi; @@ -43,28 +49,40 @@ namespace UKControllerPluginTest { TEST_F(HoldManagerTest, AddingAircraftToProximityHoldsCreatesNewInstance) { - this->manager.AddAircraftToProximityHold("BAW123", "LAM"); + this->manager.AddAircraftToProximityHold(std::make_shared("BAW123", "LAM", TimeNow())); - std::set expectedProximityHolds({"LAM"}); - EXPECT_EQ(expectedProximityHolds, this->manager.GetHoldingAircraft("BAW123")->GetProximityHolds()); EXPECT_EQ( this->manager.GetHoldingAircraft("BAW123")->GetNoHoldAssigned(), this->manager.GetHoldingAircraft("BAW123")->GetAssignedHold()); - auto test = this->manager.GetAircraftForHold("LAM"); + EXPECT_EQ(1, this->manager.GetAircraftForHold("LAM").size()); EXPECT_EQ("BAW123", (*this->manager.GetAircraftForHold("LAM").cbegin())->GetCallsign()); + EXPECT_EQ(1, this->manager.GetHoldingAircraft("BAW123")->GetProximityHolds().size()); + EXPECT_NE(nullptr, this->manager.GetHoldingAircraft("BAW123")->GetProximityHold("LAM")); + EXPECT_EQ("BAW123", this->manager.GetHoldingAircraft("BAW123")->GetProximityHold("LAM")->callsign); + EXPECT_EQ("LAM", this->manager.GetHoldingAircraft("BAW123")->GetProximityHold("LAM")->navaid); + EXPECT_EQ(TimeNow(), this->manager.GetHoldingAircraft("BAW123")->GetProximityHold("LAM")->enteredAt); } TEST_F(HoldManagerTest, AddingAircraftToProximityHoldsUpdatesExistingInstance) { - this->manager.AddAircraftToProximityHold("BAW123", "LAM"); - this->manager.AddAircraftToProximityHold("BAW123", "BNN"); + this->manager.AddAircraftToProximityHold(std::make_shared("BAW123", "LAM", TimeNow())); + this->manager.AddAircraftToProximityHold(std::make_shared("BAW123", "BNN", TimeNow())); - std::set expectedProximityHolds({"LAM", "BNN"}); - EXPECT_EQ(expectedProximityHolds, this->manager.GetHoldingAircraft("BAW123")->GetProximityHolds()); EXPECT_EQ( this->manager.GetHoldingAircraft("BAW123")->GetNoHoldAssigned(), this->manager.GetHoldingAircraft("BAW123")->GetAssignedHold()); + + EXPECT_EQ(1, this->manager.GetAircraftForHold("LAM").size()); + EXPECT_EQ("BAW123", (*this->manager.GetAircraftForHold("LAM").cbegin())->GetCallsign()); + EXPECT_EQ(1, this->manager.GetAircraftForHold("BNN").size()); + EXPECT_EQ("BAW123", (*this->manager.GetAircraftForHold("BNN").cbegin())->GetCallsign()); + + EXPECT_EQ(2, this->manager.GetHoldingAircraft("BAW123")->GetProximityHolds().size()); + EXPECT_NE(nullptr, this->manager.GetHoldingAircraft("BAW123")->GetProximityHold("BNN")); + EXPECT_EQ("BAW123", this->manager.GetHoldingAircraft("BAW123")->GetProximityHold("BNN")->callsign); + EXPECT_EQ("BNN", this->manager.GetHoldingAircraft("BAW123")->GetProximityHold("BNN")->navaid); + EXPECT_EQ(TimeNow(), this->manager.GetHoldingAircraft("BAW123")->GetProximityHold("BNN")->enteredAt); } TEST_F(HoldManagerTest, AssigningAircraftToHoldCreatesNewInstance) @@ -97,7 +115,7 @@ namespace UKControllerPluginTest { { EXPECT_CALL(this->mockApi, AssignAircraftToHold(_, _)).Times(0); - this->manager.AddAircraftToProximityHold("BAW123", "LAM"); + this->manager.AddAircraftToProximityHold(std::make_shared("BAW123", "LAM", TimeNow())); this->manager.AssignAircraftToHold("BAW123", "LAM", false); this->manager.AssignAircraftToHold("BAW123", "BNN", false); @@ -149,7 +167,7 @@ namespace UKControllerPluginTest { { EXPECT_CALL(this->mockApi, UnassignAircraftHold(_)).Times(0); - this->manager.AddAircraftToProximityHold("EZY234", "TIMBA"); + this->manager.AddAircraftToProximityHold(std::make_shared("EZY234", "TIMBA", TimeNow())); this->manager.UnassignAircraftFromHold("EZY234", false); EXPECT_EQ( this->manager.GetHoldingAircraft("EZY234")->GetNoHoldAssigned(), @@ -179,7 +197,7 @@ namespace UKControllerPluginTest { TEST_F(HoldManagerTest, RemoveAircraftFromProximityRemovesAircraftEntirelyIfNoHolds) { - this->manager.AddAircraftToProximityHold("BAW123", "WILLO"); + this->manager.AddAircraftToProximityHold(std::make_shared("BAW123", "WILLO", TimeNow())); this->manager.RemoveAircraftFromProximityHold("BAW123", "WILLO"); EXPECT_EQ(this->manager.invalidAircraft, this->manager.GetHoldingAircraft("BAW123")); EXPECT_EQ(0, this->manager.GetAircraftForHold("WILLO").size()); @@ -187,13 +205,13 @@ namespace UKControllerPluginTest { TEST_F(HoldManagerTest, RemoveAircraftFromProximityRetainsAircraftIfHoldingSomewhere) { - this->manager.AddAircraftToProximityHold("BAW123", "MAY"); - this->manager.AddAircraftToProximityHold("BAW123", "WILLO"); + this->manager.AddAircraftToProximityHold(std::make_shared("BAW123", "MAY", TimeNow())); + this->manager.AddAircraftToProximityHold(std::make_shared("BAW123", "WILLO", TimeNow())); this->manager.RemoveAircraftFromProximityHold("BAW123", "MAY"); - std::set expectedProximityHolds({"WILLO"}); EXPECT_EQ("BAW123", this->manager.GetHoldingAircraft("BAW123")->GetCallsign()); - EXPECT_EQ(expectedProximityHolds, this->manager.GetHoldingAircraft("BAW123")->GetProximityHolds()); + EXPECT_EQ(1, this->manager.GetHoldingAircraft("BAW123")->GetProximityHolds().size()); + EXPECT_NE(nullptr, this->manager.GetHoldingAircraft("BAW123")->GetProximityHold("WILLO")); EXPECT_EQ(0, this->manager.GetAircraftForHold("MAY").size()); EXPECT_EQ(1, this->manager.GetAircraftForHold("WILLO").size()); } diff --git a/test/plugin/hold/HoldModuleTest.cpp b/test/plugin/hold/HoldModuleTest.cpp index 91758dcb6..45d110152 100644 --- a/test/plugin/hold/HoldModuleTest.cpp +++ b/test/plugin/hold/HoldModuleTest.cpp @@ -387,4 +387,16 @@ namespace UKControllerPluginTest::Hold { EXPECT_EQ(1, this->radarScreenCommands.CountHandlers()); EXPECT_TRUE(this->radarScreenCommands.ProcessCommand(".ukcp hold")); } + + TEST_F(HoldModuleTest, ItAddsHandlerForAircraftEnteringHoldArea) + { + BootstrapPlugin(this->mockDependencyProvider, this->container); + EXPECT_EQ(1, this->container.pushEventProcessors->CountProcessorsForEvent("hold.area-entered")); + } + + TEST_F(HoldModuleTest, ItAddsHandlerForAircraftExitingHoldArea) + { + BootstrapPlugin(this->mockDependencyProvider, this->container); + EXPECT_EQ(1, this->container.pushEventProcessors->CountProcessorsForEvent("hold.area-exited")); + } } // namespace UKControllerPluginTest::Hold diff --git a/test/plugin/hold/HoldingAircraftTest.cpp b/test/plugin/hold/HoldingAircraftTest.cpp index bbd456051..fd2ac1939 100644 --- a/test/plugin/hold/HoldingAircraftTest.cpp +++ b/test/plugin/hold/HoldingAircraftTest.cpp @@ -1,7 +1,11 @@ #include "hold/HoldingAircraft.h" +#include "hold/ProximityHold.h" +#include "time/SystemClock.h" using ::testing::Test; using UKControllerPlugin::Hold::HoldingAircraft; +using UKControllerPlugin::Hold::ProximityHold; +using UKControllerPlugin::Time::TimeNow; namespace UKControllerPluginTest { namespace Hold { @@ -10,35 +14,42 @@ namespace UKControllerPluginTest { { public: HoldingAircraftTest() - : baseAircraft("BAW123", "BNN"), proximityAircraft("BAW123", std::set({"BNN", "LAM"})) + : proximity(std::make_shared("BAW123", "BNN", TimeNow())), baseAircraft("BAW123", "BNN"), + proximityAircraft("BAW123", proximity) { } + std::shared_ptr proximity; HoldingAircraft baseAircraft; HoldingAircraft proximityAircraft; }; - TEST_F(HoldingAircraftTest, AssignedConstructionSetsCallsignAssignedHoldAndEntryTime) + TEST_F(HoldingAircraftTest, AssignedConstructionSetsCallsignAndAssignedHold) { EXPECT_EQ("BAW123", this->baseAircraft.GetCallsign()); EXPECT_EQ("BNN", this->baseAircraft.GetAssignedHold()); - - std::chrono::seconds seconds = std::chrono::duration_cast( - this->baseAircraft.GetAssignedHoldEntryTime() - std::chrono::system_clock::now()); - - EXPECT_LT(seconds, std::chrono::seconds(3)); } TEST_F(HoldingAircraftTest, ProximityConstructionSetsCallsignAndProximityHolds) { EXPECT_EQ("BAW123", this->proximityAircraft.GetCallsign()); - EXPECT_EQ(std::set({"BNN", "LAM"}), this->proximityAircraft.GetProximityHolds()); + EXPECT_EQ(1, this->proximityAircraft.GetProximityHolds().size()); + EXPECT_EQ(proximity, this->proximityAircraft.GetProximityHold("BNN")); } TEST_F(HoldingAircraftTest, TestItAddsAProximityHold) { - this->baseAircraft.AddProximityHold("TIMBA"); - EXPECT_EQ(std::set({"TIMBA"}), this->baseAircraft.GetProximityHolds()); + this->baseAircraft.AddProximityHold(proximity); + EXPECT_EQ(1, this->baseAircraft.GetProximityHolds().size()); + EXPECT_EQ(proximity, this->baseAircraft.GetProximityHold("BNN")); + } + + TEST_F(HoldingAircraftTest, TestItDoesntDuplicateProximityHolds) + { + this->baseAircraft.AddProximityHold(proximity); + this->baseAircraft.AddProximityHold(proximity); + this->baseAircraft.AddProximityHold(proximity); + EXPECT_EQ(1, this->baseAircraft.GetProximityHolds().size()); } TEST_F(HoldingAircraftTest, IsInAnyHoldReturnsTrueIfAssigned) @@ -86,12 +97,22 @@ namespace UKControllerPluginTest { TEST_F(HoldingAircraftTest, RemoveProximityHoldRemovesProximity) { this->proximityAircraft.RemoveProximityHold("BNN"); - EXPECT_EQ(std::set({"LAM"}), this->proximityAircraft.GetProximityHolds()); + EXPECT_EQ(0, this->proximityAircraft.GetProximityHolds().size()); } TEST_F(HoldingAircraftTest, RemoveProximityHoldHandlesNotInProximity) { EXPECT_NO_THROW(this->proximityAircraft.RemoveProximityHold("ABCDEF")); } + + TEST_F(HoldingAircraftTest, ItReturnsProximityHoldIfSet) + { + EXPECT_NE(nullptr, this->proximityAircraft.GetProximityHold("BNN")); + } + + TEST_F(HoldingAircraftTest, ItReturnsNullptrIfNoProximityHold) + { + EXPECT_EQ(nullptr, this->proximityAircraft.GetProximityHold("ABC")); + } } // namespace Hold } // namespace UKControllerPluginTest diff --git a/test/plugin/navaids/NavaidCollectionTest.cpp b/test/plugin/navaids/NavaidCollectionTest.cpp index 4ff14d6b8..48c622d25 100644 --- a/test/plugin/navaids/NavaidCollectionTest.cpp +++ b/test/plugin/navaids/NavaidCollectionTest.cpp @@ -48,5 +48,17 @@ namespace UKControllerPluginTest { this->collection.AddNavaid(navaid1); EXPECT_EQ(this->collection.invalidNavaid, this->collection.GetByIdentifier("WILLO")); } + + TEST_F(NavaidCollectionTest, ItFindsNavaidsById) + { + this->collection.AddNavaid(navaid1); + EXPECT_EQ(navaid1, this->collection.Get(1)); + } + + TEST_F(NavaidCollectionTest, ItReturnsInvalidIfNavaidNotFoundById) + { + this->collection.AddNavaid(navaid1); + EXPECT_EQ(this->collection.invalidNavaid, this->collection.Get(33)); + } } // namespace Navaids } // namespace UKControllerPluginTest diff --git a/test/plugin/pch/pch.h b/test/plugin/pch/pch.h index 236d72ca7..0eb6437f9 100644 --- a/test/plugin/pch/pch.h +++ b/test/plugin/pch/pch.h @@ -12,10 +12,18 @@ // Testingutils mocks #include "../../testingutils/mock/MockApiInterface.h" +#include "../../testingutils/mock/MockApiSettingsProvider.h" #include "../../testingutils/mock/MockCurlApi.h" #include "../../testingutils/mock/MockDialogProvider.h" #include "../../testingutils/mock/MockWinApi.h" +// Test case +#include "../../testingutils/test/ApiTestCase.h" +#include "../../testingutils/test/ApiMethodExpectation.h" +#include "../../testingutils/test/ApiRequestExpectation.h" +#include "../../testingutils/test/ApiResponseExpectation.h" +#include "../../testingutils/test/ApiUriExpectation.h" + // Plugin mocks #include "../mock/MockAbstractTimedEvent.h" #include "../mock/MockActiveCallsignEventHandler.h" diff --git a/test/plugin/time/ParseTimeStringsTest.cpp b/test/plugin/time/ParseTimeStringsTest.cpp index cd7779177..a91fe831e 100644 --- a/test/plugin/time/ParseTimeStringsTest.cpp +++ b/test/plugin/time/ParseTimeStringsTest.cpp @@ -2,6 +2,7 @@ using ::testing::Test; using UKControllerPlugin::Time::invalidTime; +using UKControllerPlugin::Time::ParseIsoZuluString; using UKControllerPlugin::Time::ParseTimeString; using UKControllerPlugin::Time::ToDateTimeString; @@ -49,4 +50,15 @@ namespace UKControllerPluginTest::Time { { EXPECT_EQ("2021-11-12 13:14:15", ToDateTimeString(ParseTimeString("2021-11-12 13:14:15"))); } + + TEST_F(ParseTimeStringsTest, ItReturnsParsesCorrectIso8601ZuluTimeSingleDigits) + { + EXPECT_EQ(this->GetFromTimeNumbers(2021, 1, 9, 1, 2, 3), ParseIsoZuluString("2021-01-09T01:02:03.000000Z")); + } + + TEST_F(ParseTimeStringsTest, ItReturnsParsesCorrectIso8601ZuluTimeDoubleDigits) + { + EXPECT_EQ( + this->GetFromTimeNumbers(2021, 11, 12, 13, 14, 15), ParseIsoZuluString("2021-11-12T13:14:15.000000Z")); + } } // namespace UKControllerPluginTest::Time diff --git a/test/testingutils/CMakeLists.txt b/test/testingutils/CMakeLists.txt index bca5d6af3..b3d3c9f37 100644 --- a/test/testingutils/CMakeLists.txt +++ b/test/testingutils/CMakeLists.txt @@ -22,7 +22,10 @@ set(mock "mock/MockDialogProvider.h" "mock/MockTaskRunnerInterface.h" "mock/MockWinApi.h" -) + mock/MockApiSettingsProvider.h + mock/MockApiRequestPerformer.h + mock/MockApiRequestPerformerFactory.h + mock/MockSettingRepository.h) source_group("mock" FILES ${mock}) set(pch @@ -31,10 +34,20 @@ set(pch ) source_group("pch" FILES ${pch}) +set(test + test/ApiTestCase.cpp test/ApiTestCase.h + test/ApiExpectation.cpp test/ApiExpectation.h + test/ApiMethodExpectation.h + test/ApiUriExpectation.h + test/ApiRequestExpectation.h + test/ApiResponseExpectation.h) +source_group("test" FILES ${test}) + set(ALL_FILES ${helper} ${mock} ${pch} + ${test} ) ################################################################################ @@ -43,6 +56,8 @@ set(ALL_FILES add_library(${PROJECT_NAME} STATIC ${ALL_FILES}) set_target_properties(${PROJECT_NAME} PROPERTIES COMPILE_FLAGS "-m32" LINK_FLAGS "-m32") +target_precompile_headers(${PROJECT_NAME} PRIVATE "pch/pch.h") + use_props(${PROJECT_NAME} "${CMAKE_CONFIGURATION_TYPES}" "${DEFAULT_CXX_PROPS}") set(ROOT_NAMESPACE TestingUtils) diff --git a/test/testingutils/helper/ApiRequestHelperFunctions.cpp b/test/testingutils/helper/ApiRequestHelperFunctions.cpp index 643c09673..6331e587a 100644 --- a/test/testingutils/helper/ApiRequestHelperFunctions.cpp +++ b/test/testingutils/helper/ApiRequestHelperFunctions.cpp @@ -1,20 +1,19 @@ -#include "pch/pch.h" +#include "api/ApiSettings.h" #include "helper/ApiRequestHelperFunctions.h" -using UKControllerPlugin::Curl::CurlRequest; using UKControllerPlugin::Api::ApiRequestBuilder; +using UKControllerPlugin::Curl::CurlRequest; +using UKControllerPluginUtils::Api::ApiSettings; const std::string mockApiUrl = "http://ukcp.test.com"; const std::string mockApiKey = "areallyniceapikey"; +std::shared_ptr settings; /* Builds an expected cURL request */ -CurlRequest GetApiCurlRequest( - std::string route, - std::string method, - nlohmann::json body -) { +CurlRequest GetApiCurlRequest(std::string route, std::string method, nlohmann::json body) +{ CurlRequest request(mockApiUrl + route, method); request.SetBody(body.dump()); request.AddHeader("Authorization", "Bearer " + mockApiKey); @@ -26,10 +25,8 @@ CurlRequest GetApiCurlRequest( /* Builds an expected cURL request with no body */ -CurlRequest GetApiCurlRequest( - std::string route, - std::string method -) { +CurlRequest GetApiCurlRequest(std::string route, std::string method) +{ CurlRequest request(mockApiUrl + route, method); request.AddHeader("Authorization", "Bearer " + mockApiKey); request.AddHeader("Accept", "application/json"); @@ -40,10 +37,8 @@ CurlRequest GetApiCurlRequest( /* Builds an expected cURL request with no body */ -CurlRequest GetApiGetUriCurlRequest( - std::string route, - std::string method -) { +CurlRequest GetApiGetUriCurlRequest(std::string route, std::string method) +{ CurlRequest request(route, method); request.AddHeader("Authorization", "Bearer " + mockApiKey); request.AddHeader("Accept", "application/json"); @@ -54,7 +49,11 @@ CurlRequest GetApiGetUriCurlRequest( /* Returns an API Request Builder */ -ApiRequestBuilder GetApiRequestBuilder(void) +ApiRequestBuilder GetApiRequestBuilder() { - return ApiRequestBuilder(mockApiUrl, mockApiKey); + if (!settings) { + settings = std::make_shared(mockApiUrl, mockApiKey); + } + + return ApiRequestBuilder(*settings); } diff --git a/test/testingutils/helper/ApiRequestHelperFunctions.h b/test/testingutils/helper/ApiRequestHelperFunctions.h index 0a2406fbb..5c7e4914b 100644 --- a/test/testingutils/helper/ApiRequestHelperFunctions.h +++ b/test/testingutils/helper/ApiRequestHelperFunctions.h @@ -10,12 +10,9 @@ extern const std::string mockApiKey; */ UKControllerPlugin::Curl::CurlRequest GetApiCurlRequest(std::string route, std::string method); UKControllerPlugin::Curl::CurlRequest GetApiCurlRequest(std::string route, std::string method, nlohmann::json body); -UKControllerPlugin::Curl::CurlRequest GetApiGetUriCurlRequest( - std::string route, - std::string method -); +UKControllerPlugin::Curl::CurlRequest GetApiGetUriCurlRequest(std::string route, std::string method); /* Returns an API Request Builder */ -UKControllerPlugin::Api::ApiRequestBuilder GetApiRequestBuilder(void); +UKControllerPlugin::Api::ApiRequestBuilder GetApiRequestBuilder(); diff --git a/test/testingutils/mock/MockApiInterface.h b/test/testingutils/mock/MockApiInterface.h index f7718ddf5..ab951e6f5 100644 --- a/test/testingutils/mock/MockApiInterface.h +++ b/test/testingutils/mock/MockApiInterface.h @@ -49,8 +49,6 @@ namespace UKControllerPluginTest::Api { MOCK_CONST_METHOD0(SyncPluginEvents, nlohmann::json(void)); MOCK_CONST_METHOD1(GetLatestPluginEvents, nlohmann::json(int)); MOCK_CONST_METHOD1(GetUpdateDetails, nlohmann::json(const std::string&)); - MOCK_METHOD1(SetApiDomain, void(std::string)); - MOCK_METHOD1(SetApiKey, void(std::string)); MOCK_CONST_METHOD1(UpdateCheck, int(std::string)); MOCK_CONST_METHOD2(AcknowledgeDepartureReleaseRequest, void(int releaseId, int controllerPositionId)); MOCK_CONST_METHOD3( diff --git a/test/testingutils/mock/MockApiRequestPerformer.h b/test/testingutils/mock/MockApiRequestPerformer.h new file mode 100644 index 000000000..d959c8393 --- /dev/null +++ b/test/testingutils/mock/MockApiRequestPerformer.h @@ -0,0 +1,14 @@ +#pragma once +#include "api/ApiRequestPerformerInterface.h" + +using UKControllerPluginUtils::Api::ApiRequestData; +using UKControllerPluginUtils::Api::ApiRequestPerformerInterface; +using UKControllerPluginUtils::Api::Response; + +namespace UKControllerPluginUtilsTest::Api { + class MockApiRequestPerformer : public ApiRequestPerformerInterface + { + public: + MOCK_METHOD(Response, Perform, (const ApiRequestData&), (override)); + }; +} // namespace UKControllerPluginUtilsTest::Api diff --git a/test/testingutils/mock/MockApiRequestPerformerFactory.h b/test/testingutils/mock/MockApiRequestPerformerFactory.h new file mode 100644 index 000000000..2910e28f3 --- /dev/null +++ b/test/testingutils/mock/MockApiRequestPerformerFactory.h @@ -0,0 +1,14 @@ +#pragma once +#include "api/AbstractApiRequestPerformerFactory.h" + +using UKControllerPluginUtils::Api::AbstractApiRequestPerformerFactory; +using UKControllerPluginUtils::Api::ApiRequestPerformerInterface; +using UKControllerPluginUtils::Api::ApiSettings; + +namespace UKControllerPluginUtilsTest::Api { + class MockApiRequestPerformerFactory : public AbstractApiRequestPerformerFactory + { + public: + MOCK_METHOD(ApiRequestPerformerInterface&, Make, (const ApiSettings&), ()); + }; +} // namespace UKControllerPluginUtilsTest::Api diff --git a/test/testingutils/mock/MockApiSettingsProvider.h b/test/testingutils/mock/MockApiSettingsProvider.h new file mode 100644 index 000000000..d930a2d3a --- /dev/null +++ b/test/testingutils/mock/MockApiSettingsProvider.h @@ -0,0 +1,15 @@ +#pragma once +#include "api/ApiSettingsProviderInterface.h" + +using UKControllerPluginUtils::Api::ApiSettings; +using UKControllerPluginUtils::Api::ApiSettingsProviderInterface; + +namespace UKControllerPluginUtilsTest::Api { + class MockApiSettingsProvider : public ApiSettingsProviderInterface + { + public: + MOCK_METHOD(ApiSettings&, Get, (), (override)); + MOCK_METHOD(bool, Has, (), (override)); + MOCK_METHOD(bool, Reload, (), (override)); + }; +} // namespace UKControllerPluginUtilsTest::Api diff --git a/test/testingutils/mock/MockSettingRepository.h b/test/testingutils/mock/MockSettingRepository.h new file mode 100644 index 000000000..afe0548c8 --- /dev/null +++ b/test/testingutils/mock/MockSettingRepository.h @@ -0,0 +1,14 @@ +#include "setting/SettingRepositoryInterface.h" + +using UKControllerPlugin::Setting::SettingRepositoryInterface; + +namespace UKControllerPluginTest::Setting { + class MockSettingRepository : public SettingRepositoryInterface + { + public: + MOCK_METHOD(bool, HasSetting, (const std::string&), (const, override)); + MOCK_METHOD(void, ReloadSetting, (const std::string&), (override)); + MOCK_METHOD(void, UpdateSetting, (const std::string&, const std::string&), (override)); + MOCK_METHOD(std::string, GetSetting, (const std::string&, const std::string&), (const, override)); + }; +} // namespace UKControllerPluginTest::Setting diff --git a/test/testingutils/pch/pch.h b/test/testingutils/pch/pch.h index 6b5144f87..acd46b0b7 100644 --- a/test/testingutils/pch/pch.h +++ b/test/testingutils/pch/pch.h @@ -8,11 +8,11 @@ // add headers that you want to pre-compile here // Ignore warnings about uninitialised variables in the Gmock headers #include "json/json.hpp" -#pragma warning( push ) -#pragma warning( disable : 26495 26451 28251) +#pragma warning(push) +#pragma warning(disable : 26495 26451 28251) #include "gmock/gmock.h" #include "helper/Matchers.h" -#pragma warning( pop ) +#pragma warning(pop) #include #include @@ -21,8 +21,11 @@ #include "spdlog/include/spdlog/logger.h" #include "spdlog/include/spdlog/sinks/file_sinks.h" #include "spdlog/include/spdlog/sinks/null_sink.h" -using std::min; +#include "mock/MockApiRequestPerformer.h" +#include "mock/MockApiRequestPerformerFactory.h" +#include "mock/MockApiSettingsProvider.h" using std::max; +using std::min; #include #include #include diff --git a/test/testingutils/test/ApiExpectation.cpp b/test/testingutils/test/ApiExpectation.cpp new file mode 100644 index 000000000..98cb0cf3a --- /dev/null +++ b/test/testingutils/test/ApiExpectation.cpp @@ -0,0 +1,130 @@ +#include "ApiExpectation.h" +#include "api/ApiRequestData.h" +#include "api/ApiRequestException.h" +#include "api/Response.h" +#include "http/HttpMethod.h" + +using UKControllerPluginUtils::Api::ApiRequestData; +using UKControllerPluginUtils::Api::ApiRequestException; +using UKControllerPluginUtils::Api::Response; +using UKControllerPluginUtils::Http::HttpMethod; +using UKControllerPluginUtils::Http::HttpStatusCode; +using UKControllerPluginUtils::Http::IsSuccessful; + +namespace UKControllerPluginTest { + + ApiExpectation::ApiExpectation( + bool isPositive, UKControllerPluginUtilsTest::Api::MockApiRequestPerformer& performer) + : isPositive(isPositive), performer(performer), responseCode(HttpStatusCode::Unknown) + { + } + + ApiExpectation::~ApiExpectation() + { + if (this->isPositive) { + if (std::holds_alternative(ExpectedResponse())) { + EXPECT_CALL(performer, Perform(ExpectedRequest())) + .WillOnce(testing::Return(std::get(ExpectedResponse()))) + .RetiresOnSaturation(); + } else { + EXPECT_CALL(performer, Perform(ExpectedRequest())) + .WillOnce(testing::Throw(std::get(ExpectedResponse()))) + .RetiresOnSaturation(); + } + } else { + EXPECT_CALL(performer, Perform(ExpectedRequest())).Times(0); + } + } + + ApiUriExpectation& ApiExpectation::Get() + { + this->method = std::make_shared(HttpMethod::Get()); + return *this; + } + + ApiUriExpectation& ApiExpectation::Post() + { + this->method = std::make_shared(HttpMethod::Post()); + return *this; + } + + ApiUriExpectation& ApiExpectation::Put() + { + this->method = std::make_shared(HttpMethod::Put()); + return *this; + } + + ApiUriExpectation& ApiExpectation::Patch() + { + this->method = std::make_shared(HttpMethod::Patch()); + return *this; + } + + ApiUriExpectation& ApiExpectation::Delete() + { + this->method = std::make_shared(HttpMethod::Delete()); + return *this; + } + + ApiResponseExpectation& ApiExpectation::WithBody(const nlohmann::json& body) + { + this->requestBody = body; + return *this; + } + + ApiResponseExpectation& ApiExpectation::WithoutBody() + { + return *this; + } + + ApiResponseExpectation& ApiExpectation::WillReturnOk() + { + this->responseCode = HttpStatusCode::Ok; + return *this; + } + + ApiResponseExpectation& ApiExpectation::WillReturnServerError() + { + this->responseCode = HttpStatusCode::ServerError; + return *this; + } + + ApiResponseExpectation& ApiExpectation::WillReturnForbidden() + { + this->responseCode = HttpStatusCode::Forbidden; + return *this; + } + + ApiResponseExpectation& ApiExpectation::WithResponseBody(const nlohmann::json& body) + { + this->responseBody = body; + return *this; + } + + ApiResponseExpectation& ApiExpectation::WithInvalidBodyJson() + { + invalidBodyJson = true; + return *this; + } + + ApiRequestExpectation& ApiExpectation::To(const std::string& uri) + { + this->uri = uri; + return *this; + } + + auto ApiExpectation::ExpectedResponse() const + -> std::variant + { + if (IsSuccessful(this->responseCode) && !invalidBodyJson) { + return {Response(this->responseCode, this->responseBody)}; + } + + return {ApiRequestException(uri, responseCode, invalidBodyJson)}; + } + + auto ApiExpectation::ExpectedRequest() const -> UKControllerPluginUtils::Api::ApiRequestData + { + return {uri, *method, requestBody}; + } +} // namespace UKControllerPluginTest diff --git a/test/testingutils/test/ApiExpectation.h b/test/testingutils/test/ApiExpectation.h new file mode 100644 index 000000000..d3810f3fb --- /dev/null +++ b/test/testingutils/test/ApiExpectation.h @@ -0,0 +1,74 @@ +#pragma once +#include "ApiMethodExpectation.h" +#include "ApiRequestExpectation.h" +#include "ApiResponseExpectation.h" +#include "ApiUriExpectation.h" +#include "api/ApiRequestData.h" +#include "api/ApiRequestException.h" +#include "api/Response.h" +#include "http/HttpStatusCode.h" + +namespace UKControllerPluginUtils::Http { + class HttpMethod; +} // namespace UKControllerPluginUtils::Http + +namespace UKControllerPluginUtilsTest::Api { + class MockApiRequestPerformer; +} // namespace UKControllerPluginUtilsTest::Api + +namespace UKControllerPluginTest { + /** + * Builds up expectations during API tests in a more fluent way. + */ + class ApiExpectation : public ApiMethodExpectation, + public ApiRequestExpectation, + public ApiResponseExpectation, + public ApiUriExpectation + { + public: + ApiExpectation(bool isPositive, UKControllerPluginUtilsTest::Api::MockApiRequestPerformer& performer); + ~ApiExpectation(); + auto Get() -> ApiUriExpectation& override; + auto Post() -> ApiUriExpectation& override; + auto Put() -> ApiUriExpectation& override; + auto Patch() -> ApiUriExpectation& override; + auto Delete() -> ApiUriExpectation& override; + auto WithBody(const nlohmann::json& body) -> ApiResponseExpectation& override; + auto WithoutBody() -> ApiResponseExpectation& override; + auto WillReturnOk() -> ApiResponseExpectation& override; + auto WillReturnServerError() -> ApiResponseExpectation& override; + auto WillReturnForbidden() -> ApiResponseExpectation& override; + auto WithResponseBody(const nlohmann::json& body) -> ApiResponseExpectation& override; + auto WithInvalidBodyJson() -> ApiResponseExpectation& override; + auto To(const std::string& uri) -> ApiRequestExpectation& override; + + private: + [[nodiscard]] auto ExpectedRequest() const -> UKControllerPluginUtils::Api::ApiRequestData; + [[nodiscard]] auto ExpectedResponse() const + -> std::variant; + + // Is this a positive or negative assertion + bool isPositive; + + // Performs the expectation + UKControllerPluginUtilsTest::Api::MockApiRequestPerformer& performer; + + // The expected URI for the request + std::string uri; + + // The expected method + std::shared_ptr method; + + // The expected body + nlohmann::json requestBody; + + // The response HTTP status code + UKControllerPluginUtils::Http::HttpStatusCode responseCode; + + // The expected response body + nlohmann::json responseBody; + + // The response body is invalid + bool invalidBodyJson = false; + }; +} // namespace UKControllerPluginTest diff --git a/test/testingutils/test/ApiMethodExpectation.h b/test/testingutils/test/ApiMethodExpectation.h new file mode 100644 index 000000000..acc28c6b6 --- /dev/null +++ b/test/testingutils/test/ApiMethodExpectation.h @@ -0,0 +1,19 @@ +#pragma once + +namespace UKControllerPluginTest { + class ApiUriExpectation; + + /** + * An interface for expecting API methods to be called. + */ + class ApiMethodExpectation + { + public: + virtual ~ApiMethodExpectation() = default; + [[nodiscard]] virtual auto Get() -> ApiUriExpectation& = 0; + [[nodiscard]] virtual auto Post() -> ApiUriExpectation& = 0; + [[nodiscard]] virtual auto Put() -> ApiUriExpectation& = 0; + [[nodiscard]] virtual auto Patch() -> ApiUriExpectation& = 0; + [[nodiscard]] virtual auto Delete() -> ApiUriExpectation& = 0; + }; +} // namespace UKControllerPluginTest diff --git a/test/testingutils/test/ApiRequestExpectation.h b/test/testingutils/test/ApiRequestExpectation.h new file mode 100644 index 000000000..b778fa9f2 --- /dev/null +++ b/test/testingutils/test/ApiRequestExpectation.h @@ -0,0 +1,15 @@ +#pragma once + +namespace UKControllerPluginTest { + class ApiResponseExpectation; + + /** + * An interface for expecting a particular request to happen. + */ + class ApiRequestExpectation + { + public: + [[nodiscard]] virtual auto WithBody(const nlohmann::json& body) -> ApiResponseExpectation& = 0; + [[nodiscard]] virtual auto WithoutBody() -> ApiResponseExpectation& = 0; + }; +} // namespace UKControllerPluginTest diff --git a/test/testingutils/test/ApiResponseExpectation.h b/test/testingutils/test/ApiResponseExpectation.h new file mode 100644 index 000000000..b8511b4cd --- /dev/null +++ b/test/testingutils/test/ApiResponseExpectation.h @@ -0,0 +1,16 @@ +#pragma once + +namespace UKControllerPluginTest { + /** + * An interface for expecting a particular response to happen. + */ + class ApiResponseExpectation + { + public: + virtual auto WillReturnOk() -> ApiResponseExpectation& = 0; + virtual auto WillReturnServerError() -> ApiResponseExpectation& = 0; + virtual auto WillReturnForbidden() -> ApiResponseExpectation& = 0; + virtual auto WithResponseBody(const nlohmann::json& body) -> ApiResponseExpectation& = 0; + virtual auto WithInvalidBodyJson() -> ApiResponseExpectation& = 0; + }; +} // namespace UKControllerPluginTest diff --git a/test/testingutils/test/ApiTestCase.cpp b/test/testingutils/test/ApiTestCase.cpp new file mode 100644 index 000000000..acc265756 --- /dev/null +++ b/test/testingutils/test/ApiTestCase.cpp @@ -0,0 +1,69 @@ +#include "ApiExpectation.h" +#include "ApiTestCase.h" +#include "api/ApiFactory.h" +#include "api/ApiRequestData.h" +#include "api/ApiRequestException.h" +#include "api/ApiRequestFacade.h" +#include "api/ApiRequestFactory.h" +#include "api/ApiSettings.h" +#include "api/Response.h" +#include "http/HttpMethod.h" + +using UKControllerPluginUtils::Api::ApiFactory; +using UKControllerPluginUtils::Api::ApiRequestFactory; +using UKControllerPluginUtils::Api::ApiSettings; +using UKControllerPluginUtils::Http::HttpMethod; +using UKControllerPluginUtilsTest::Api::MockApiRequestPerformer; +using UKControllerPluginUtilsTest::Api::MockApiRequestPerformerFactory; +using UKControllerPluginUtilsTest::Api::MockApiSettingsProvider; + +namespace UKControllerPluginTest { + ApiTestCase::ApiTestCase() + : settings(std::make_shared("https://ukcp.vatsim.uk", "key")), + requestPerformer( + std::make_shared>()), + requestPerformerFactory(std::make_shared>()), + settingsProvider(std::make_shared>()), + factory(std::make_shared(settingsProvider, requestPerformerFactory)) + { + ON_CALL(*settingsProvider, Get).WillByDefault(testing::ReturnRef(*settings)); + + ON_CALL(*requestPerformerFactory, Make(testing::Ref(*settings))) + .WillByDefault(testing::ReturnRef(*requestPerformer)); + + SetApiRequestFactory(factory); + } + + void ApiTestCase::TearDown() + { + Test::TearDown(); + this->AwaitApiCallCompletion(); + UnsetSetApiFactory(); + } + + void ApiTestCase::ExpectNoApiRequests() + { + EXPECT_CALL(*requestPerformer, Perform(testing::_)).Times(0); + } + + void ApiTestCase::AwaitApiCallCompletion() + { + factory->RequestFactory().AwaitRequestCompletion(std::chrono::seconds(5)); + } + + auto ApiTestCase::SettingsProvider() + -> testing::NiceMock& + { + return *settingsProvider; + } + + auto ApiTestCase::ExpectApiRequest() -> std::shared_ptr + { + return std::make_shared(true, *requestPerformer); + } + + auto ApiTestCase::DontExpectApiRequest() -> std::shared_ptr + { + return std::make_shared(false, *requestPerformer); + } +} // namespace UKControllerPluginTest diff --git a/test/testingutils/test/ApiTestCase.h b/test/testingutils/test/ApiTestCase.h new file mode 100644 index 000000000..67e04fab2 --- /dev/null +++ b/test/testingutils/test/ApiTestCase.h @@ -0,0 +1,40 @@ +#pragma once +#include "ApiMethodExpectation.h" + +namespace UKControllerPluginUtils::Api { + class ApiFactory; + class ApiRequestFactory; + class ApiSettings; +} // namespace UKControllerPluginUtils::Api + +namespace UKControllerPluginUtilsTest::Api { + class MockApiRequestPerformer; + class MockApiRequestPerformerFactory; + class MockApiSettingsProvider; +} // namespace UKControllerPluginUtilsTest::Api + +namespace UKControllerPluginTest { + class ApiTestCase : public testing::Test + { + public: + ApiTestCase(); + ~ApiTestCase() = default; + void TearDown() override; + [[nodiscard]] auto ExpectApiRequest() -> std::shared_ptr; + [[nodiscard]] auto DontExpectApiRequest() -> std::shared_ptr; + void ExpectNoApiRequests(); + void AwaitApiCallCompletion(); + [[nodiscard]] auto SettingsProvider() + -> testing::NiceMock&; + + private: + std::shared_ptr settings; + + std::shared_ptr> requestPerformer; + std::shared_ptr> + requestPerformerFactory; + std::shared_ptr> settingsProvider; + + std::shared_ptr factory; + }; +} // namespace UKControllerPluginTest diff --git a/test/testingutils/test/ApiUriExpectation.h b/test/testingutils/test/ApiUriExpectation.h new file mode 100644 index 000000000..a600f0caa --- /dev/null +++ b/test/testingutils/test/ApiUriExpectation.h @@ -0,0 +1,14 @@ +#pragma once + +namespace UKControllerPluginTest { + class ApiRequestExpectation; + + /** + * An interface for expecting a particular URI to be called. + */ + class ApiUriExpectation + { + public: + [[nodiscard]] virtual auto To(const std::string& uri) -> ApiRequestExpectation& = 0; + }; +} // namespace UKControllerPluginTest diff --git a/test/utils/CMakeLists.txt b/test/utils/CMakeLists.txt index faddcea2e..cdc75fcd6 100644 --- a/test/utils/CMakeLists.txt +++ b/test/utils/CMakeLists.txt @@ -4,13 +4,25 @@ set(PROJECT_NAME UKControllerPluginUtilsTest) # Source groups ################################################################################ set(test__api - "api/ApiAuthCheckerTest.cpp" "api/ApiHelperTest.cpp" "api/ApiRequestBuilderTest.cpp" "api/ApiResponseFactoryTest.cpp" "api/ApiResponseTest.cpp" "api/ApiResponseValidatorTest.cpp" -) + api/ApiBootstrapTest.cpp + api/ApiCurlRequestFactoryTest.cpp + api/ApiFactoryTest.cpp + api/ApiHeaderApplicatorTest.cpp + api/ApiRequestTest.cpp + api/ApiRequestDataTest.cpp + api/ApiRequestExceptionTest.cpp + api/ApiRequestFactoryTest.cpp + api/ApiSettingsTest.cpp + api/ApiUrlBuilderTest.cpp + api/ConfigApiSettingsProviderTest.cpp + api/CurlApiRequestPerformerTest.cpp + api/CurlApiRequestPerformerFactoryTest.cpp + api/ResponseTest.cpp) source_group("test\\api" FILES ${test__api}) set(test__curl @@ -41,11 +53,20 @@ set(test__helper ) source_group("test\\helper" FILES ${test__helper}) +set(test__http + "helper/HelperFunctionsTest.cpp" + http/HttpMethodTest.cpp http/HttpStatusCodeTest.cpp) +source_group("test\\http" FILES ${test__http}) + set(test__log "log/LoggerBootstrapTest.cpp" ) source_group("test\\log" FILES ${test__log}) +set(test__mock + mock/MockSettingProvider.h) +source_group("test\\mock" FILES ${test__mock}) + set(test__pch "pch/pch.cpp" "pch/pch.h" @@ -55,7 +76,7 @@ source_group("test\\pch" FILES ${test__pch}) set(test__setting "setting/SettingRepositoryFactoryTest.cpp" "setting/SettingRepositoryTest.cpp" -) + setting/JsonFileSettingProviderTest.cpp) source_group("test\\setting" FILES ${test__setting}) set(test__squawk @@ -63,6 +84,10 @@ set(test__squawk ) source_group("test\\squawk" FILES ${test__squawk}) +set(test__string + string/StringTrimFunctionTest.cpp) +source_group("test\\string" FILES ${test__string}) + set(test__update "update/UpdateBinariesTest.cpp" "update/CheckDevelopmentVersionTest.cpp" @@ -76,10 +101,13 @@ set(ALL_FILES ${test__dialog} ${test__duplicate} ${test__helper} + ${test__http} ${test__log} + ${test__mock} ${test__pch} ${test__setting} ${test__squawk} + ${test__string} ${test__update} ) @@ -134,6 +162,8 @@ target_include_directories(${PROJECT_NAME} PUBLIC target_include_directories(${PROJECT_NAME} SYSTEM PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../../resource;" "${CMAKE_CURRENT_SOURCE_DIR}/../../third_party" + "${CMAKE_CURRENT_SOURCE_DIR}/../../third_party/continuable/include" + "${CMAKE_CURRENT_SOURCE_DIR}/../../third_party/function2/include" ) ################################################################################ @@ -243,6 +273,9 @@ set(ADDITIONAL_LIBRARY_DEPENDENCIES "crypt32;" "Winmm;" "dbghelp" + "Ws2_32;" + "Wldap32;" + "normaliz;" ) target_link_libraries(${PROJECT_NAME} PRIVATE "${ADDITIONAL_LIBRARY_DEPENDENCIES}") diff --git a/test/utils/api/ApiBootstrapTest.cpp b/test/utils/api/ApiBootstrapTest.cpp new file mode 100644 index 000000000..9455ff04f --- /dev/null +++ b/test/utils/api/ApiBootstrapTest.cpp @@ -0,0 +1,55 @@ +#include "api/ApiBootstrap.h" +#include "curl/CurlRequest.h" +#include "curl/CurlResponse.h" +#include "setting/SettingRepository.h" + +using UKControllerPlugin::Curl::CurlRequest; +using UKControllerPlugin::Curl::CurlResponse; +using UKControllerPlugin::Setting::SettingRepository; +using UKControllerPluginUtils::Api::Bootstrap; +using UKControllerPluginUtils::Api::BootstrapLegacy; + +namespace UKControllerPluginUtilsTest::Api { + class ApiBootstrapTest : public testing::Test + { + public: + testing::NiceMock windows; + testing::NiceMock curl; + SettingRepository settings; + }; + + TEST_F(ApiBootstrapTest, BootstrapReturnsAnApiFactory) + { + EXPECT_NE(nullptr, Bootstrap(settings, windows)); + } + + TEST_F(ApiBootstrapTest, BootstrapRegistersApiKeySetting) + { + static_cast(Bootstrap(settings, windows)); + EXPECT_TRUE(settings.HasSetting("api-key")); + } + + TEST_F(ApiBootstrapTest, BootstrapRegistersApiUrlSetting) + { + static_cast(Bootstrap(settings, windows)); + EXPECT_TRUE(settings.HasSetting("api-url")); + } + + TEST_F(ApiBootstrapTest, BootstrapLegacyReturnsLegacyApiInterface) + { + auto factory = Bootstrap(settings, windows); + auto legacyInterface = BootstrapLegacy(*factory, curl); + EXPECT_NE(nullptr, legacyInterface); + + CurlRequest expectedRequest("https://ukcp.vatsim.uk/authorise", CurlRequest::METHOD_GET); + expectedRequest.AddHeader("Authorization", "Bearer "); + expectedRequest.AddHeader("Accept", "application/json"); + expectedRequest.AddHeader("Content-Type", "application/json"); + + EXPECT_CALL(curl, MakeCurlRequest(expectedRequest)) + .Times(1) + .WillOnce(testing::Return(CurlResponse("", false, 200L))); + + static_cast(legacyInterface->CheckApiAuthorisation()); + } +} // namespace UKControllerPluginUtilsTest::Api diff --git a/test/utils/api/ApiCurlRequestFactoryTest.cpp b/test/utils/api/ApiCurlRequestFactoryTest.cpp new file mode 100644 index 000000000..90c9b7ca3 --- /dev/null +++ b/test/utils/api/ApiCurlRequestFactoryTest.cpp @@ -0,0 +1,41 @@ +#include "api/ApiCurlRequestFactory.h" +#include "api/ApiUrlBuilder.h" +#include "api/ApiHeaderApplicator.h" +#include "api/ApiRequestData.h" +#include "curl/CurlRequest.h" + +using UKControllerPlugin::Curl::CurlRequest; +using UKControllerPluginUtils::Api::ApiCurlRequestFactory; +using UKControllerPluginUtils::Api::ApiHeaderApplicator; +using UKControllerPluginUtils::Api::ApiRequestData; +using UKControllerPluginUtils::Api::ApiSettings; +using UKControllerPluginUtils::Api::ApiUrlBuilder; + +namespace UKControllerPluginUtilsTest::Api { + class ApiCurlRequestFactoryTest : public testing::Test + { + public: + ApiCurlRequestFactoryTest() + : settings("https://ukcp.vatsim.uk", "key"), builder(settings), headerApplicator(settings), + requestFactory(builder, headerApplicator) + { + } + ApiSettings settings; + ApiUrlBuilder builder; + ApiHeaderApplicator headerApplicator; + ApiCurlRequestFactory requestFactory; + }; + + TEST_F(ApiCurlRequestFactoryTest, ItBuildsRequests) + { + auto request = + requestFactory.BuildCurlRequest(ApiRequestData("test", UKControllerPluginUtils::Http::HttpMethod::Get())); + + CurlRequest expectedRequest("https://ukcp.vatsim.uk/test", CurlRequest::METHOD_GET); + expectedRequest.AddHeader("Authorization", "Bearer key"); + expectedRequest.AddHeader("Accept", "application/json"); + expectedRequest.AddHeader("Content-Type", "application/json"); + + EXPECT_EQ(expectedRequest, request); + } +} // namespace UKControllerPluginUtilsTest::Api diff --git a/test/utils/api/ApiFactoryTest.cpp b/test/utils/api/ApiFactoryTest.cpp new file mode 100644 index 000000000..96af36ce2 --- /dev/null +++ b/test/utils/api/ApiFactoryTest.cpp @@ -0,0 +1,75 @@ +#include "api/ApiFactory.h" +#include "api/ApiRequestBuilder.h" +#include "api/ApiRequestFactory.h" +#include "api/ApiSettings.h" +#include "curl/CurlRequest.h" + +using UKControllerPlugin::Curl::CurlRequest; +using UKControllerPluginUtils::Api::ApiFactory; +using UKControllerPluginUtils::Api::ApiSettings; + +namespace UKControllerPluginUtilsTest::Api { + class ApiFactoryTest : public testing::Test + { + public: + ApiFactoryTest() + : settings("https://ukcp.vatsim.uk", "key"), + requestFactory(std::make_shared>()), + settingsProvider(std::make_shared>()), + factory(settingsProvider, requestFactory) + { + ON_CALL(*settingsProvider, Get).WillByDefault(testing::ReturnRef(settings)); + ON_CALL(*requestFactory, Make(testing::Ref(settings))).WillByDefault(testing::ReturnRef(requestPerformer)); + } + + ApiSettings settings; + std::shared_ptr> requestFactory; + testing::NiceMock requestPerformer; + std::shared_ptr> settingsProvider; + ApiFactory factory; + }; + + TEST_F(ApiFactoryTest, ItReturnsSettingsProvider) + { + EXPECT_EQ(settingsProvider, factory.SettingsProvider()); + } + + TEST_F(ApiFactoryTest, ItReturnsARequestFactory) + { + auto& requestFactory = factory.RequestFactory(); + EXPECT_CALL(requestPerformer, Perform(ApiRequestData("test", UKControllerPluginUtils::Http::HttpMethod::Get()))) + .Times(1) + .WillOnce(testing::Return(Response(UKControllerPluginUtils::Http::HttpStatusCode::Ok, {}))); + + static_cast(requestFactory.Get("test")); + requestFactory.AwaitRequestCompletion(); + } + + TEST_F(ApiFactoryTest, ItReturnsARequestFactoryAsASingleton) + { + auto& requestFactory1 = factory.RequestFactory(); + auto& requestFactory2 = factory.RequestFactory(); + + EXPECT_EQ(&requestFactory1, &requestFactory2); + } + + TEST_F(ApiFactoryTest, ItReturnsTheLegacyRequestBuilder) + { + auto& builder = factory.LegacyRequestBuilder(); + + CurlRequest expectedRequest("https://ukcp.vatsim.uk/authorise", CurlRequest::METHOD_GET); + expectedRequest.AddHeader("Authorization", "Bearer key"); + expectedRequest.AddHeader("Accept", "application/json"); + expectedRequest.AddHeader("Content-Type", "application/json"); + + EXPECT_EQ(expectedRequest, builder.BuildAuthCheckRequest()); + } + + TEST_F(ApiFactoryTest, ItReturnsTheLegacyRequestBuilderAsASingleton) + { + auto& builder1 = factory.LegacyRequestBuilder(); + auto& builder2 = factory.LegacyRequestBuilder(); + + EXPECT_EQ(&builder1, &builder2); + } +} // namespace UKControllerPluginUtilsTest::Api diff --git a/test/utils/api/ApiHeaderApplicatorTest.cpp b/test/utils/api/ApiHeaderApplicatorTest.cpp new file mode 100644 index 000000000..c1b6eb304 --- /dev/null +++ b/test/utils/api/ApiHeaderApplicatorTest.cpp @@ -0,0 +1,34 @@ +#include "api/ApiHeaderApplicator.h" +#include "api/ApiSettings.h" +#include "curl/CurlRequest.h" + +using UKControllerPlugin::Curl::CurlRequest; +using UKControllerPluginUtils::Api::ApiHeaderApplicator; +using UKControllerPluginUtils::Api::ApiSettings; + +namespace UKControllerPluginUtilsTest::Api { + class ApiHeaderApplicatorTest : public testing::Test + { + public: + ApiHeaderApplicatorTest() + : request("https://ukcp.vatsim.uk/authorise", CurlRequest::METHOD_GET), + settings("https://ukcp.vatsim.uk", "apikey"), applicator(settings) + { + } + + CurlRequest request; + ApiSettings settings; + ApiHeaderApplicator applicator; + }; + + TEST_F(ApiHeaderApplicatorTest, ItAppliesHeaders) + { + CurlRequest expectedRequest("https://ukcp.vatsim.uk/authorise", CurlRequest::METHOD_GET); + expectedRequest.AddHeader("Authorization", "Bearer apikey"); + expectedRequest.AddHeader("Accept", "application/json"); + expectedRequest.AddHeader("Content-Type", "application/json"); + + applicator.ApplyHeaders(request); + EXPECT_EQ(expectedRequest, request); + } +} // namespace UKControllerPluginUtilsTest::Api diff --git a/test/utils/api/ApiHelperTest.cpp b/test/utils/api/ApiHelperTest.cpp index 5613a1f74..d496622b1 100644 --- a/test/utils/api/ApiHelperTest.cpp +++ b/test/utils/api/ApiHelperTest.cpp @@ -363,28 +363,6 @@ namespace UKControllerPluginUtilsTest::Api { EXPECT_EQ(data, this->helper.GetHoldDependency()); } - TEST_F(ApiHelperTest, ItHasAUrlToSendTo) - { - EXPECT_TRUE(this->helper.GetApiDomain() == mockApiUrl); - } - - TEST_F(ApiHelperTest, ItHasAnApiKeyToUseForAuthentication) - { - EXPECT_TRUE(this->helper.GetApiKey() == mockApiKey); - } - - TEST_F(ApiHelperTest, ItCanUpdateTheUrl) - { - this->helper.SetApiDomain("https://nottheurl"); - EXPECT_TRUE(this->helper.GetApiDomain() == "https://nottheurl"); - } - - TEST_F(ApiHelperTest, ItCanUpdateTheKey) - { - this->helper.SetApiKey("notthekey"); - EXPECT_TRUE(this->helper.GetApiKey() == "notthekey"); - } - TEST_F(ApiHelperTest, GetMinStackLevelsReturnsMinStackData) { nlohmann::json responseData; diff --git a/test/utils/api/ApiRequestBuilderTest.cpp b/test/utils/api/ApiRequestBuilderTest.cpp index e514a565f..3977b1f79 100644 --- a/test/utils/api/ApiRequestBuilderTest.cpp +++ b/test/utils/api/ApiRequestBuilderTest.cpp @@ -1,4 +1,5 @@ #include "api/ApiRequestBuilder.h" +#include "api/ApiSettings.h" #include "srd/SrdSearchParameters.h" using ::testing::Test; @@ -6,40 +7,21 @@ using ::testing::Test; using UKControllerPlugin::Api::ApiRequestBuilder; using UKControllerPlugin::Curl::CurlRequest; using UKControllerPlugin::Srd::SrdSearchParameters; +using UKControllerPluginUtils::Api::ApiSettings; namespace UKControllerPluginUtilsTest::Api { class ApiRequestBuilderTest : public Test { public: - ApiRequestBuilderTest() : builder("http://testurl.com", "apikey") + ApiRequestBuilderTest() : settings("http://testurl.com", "apikey"), builder(settings) { } + + ApiSettings settings; ApiRequestBuilder builder; }; - TEST_F(ApiRequestBuilderTest, ItHasAnApiDomain) - { - EXPECT_TRUE("http://testurl.com" == this->builder.GetApiDomain()); - } - - TEST_F(ApiRequestBuilderTest, ItHasAnApiKey) - { - EXPECT_TRUE("apikey" == this->builder.GetApiKey()); - } - - TEST_F(ApiRequestBuilderTest, ApiDomainCanBeUpdated) - { - this->builder.SetApiDomain("http://nottesturl.com"); - EXPECT_TRUE("http://nottesturl.com" == this->builder.GetApiDomain()); - } - - TEST_F(ApiRequestBuilderTest, ApiKeyCanBeUpdated) - { - this->builder.SetApiKey("notapikey"); - EXPECT_TRUE("notapikey" == this->builder.GetApiKey()); - } - TEST_F(ApiRequestBuilderTest, ItBuildsAuthCheckRequests) { CurlRequest expectedRequest("http://testurl.com/authorise", CurlRequest::METHOD_GET); diff --git a/test/utils/api/ApiRequestDataTest.cpp b/test/utils/api/ApiRequestDataTest.cpp new file mode 100644 index 000000000..18cb729da --- /dev/null +++ b/test/utils/api/ApiRequestDataTest.cpp @@ -0,0 +1,100 @@ +#include "api/ApiRequestData.h" + +using UKControllerPluginUtils::Api::ApiRequestData; +using UKControllerPluginUtils::Http::HttpMethod; + +namespace UKControllerPluginUtilsTest::Api { + class ApiRequestDataTest : public testing::Test + { + }; + + TEST_F(ApiRequestDataTest, ItBuildsGetRequest) + { + ApiRequestData data("uri", HttpMethod::Get()); + EXPECT_EQ("uri", data.Uri()); + EXPECT_EQ(HttpMethod::Get(), data.Method()); + EXPECT_TRUE(data.Body().empty()); + } + + TEST_F(ApiRequestDataTest, ItBuildsPostRequest) + { + nlohmann::json body = {{"foo", "bar"}}; + ApiRequestData data("uri", HttpMethod::Post(), body); + EXPECT_EQ("uri", data.Uri()); + EXPECT_EQ(HttpMethod::Post(), data.Method()); + EXPECT_EQ(body, data.Body()); + } + + TEST_F(ApiRequestDataTest, ItThrowsExceptionPostRequestWithNoBody) + { + EXPECT_THROW(ApiRequestData data("uri", HttpMethod::Post()), std::invalid_argument); + } + + TEST_F(ApiRequestDataTest, ItBuildsPutRequest) + { + nlohmann::json body = {{"foo", "bar"}}; + ApiRequestData data("uri", HttpMethod::Put(), body); + EXPECT_EQ("uri", data.Uri()); + EXPECT_EQ(HttpMethod::Put(), data.Method()); + EXPECT_EQ(body, data.Body()); + } + + TEST_F(ApiRequestDataTest, ItThrowsExceptionPutRequestWithNoBody) + { + EXPECT_THROW(ApiRequestData data("uri", HttpMethod::Put()), std::invalid_argument); + } + + TEST_F(ApiRequestDataTest, ItBuildsPatchRequest) + { + nlohmann::json body = {{"foo", "bar"}}; + ApiRequestData data("uri", HttpMethod::Patch(), body); + EXPECT_EQ("uri", data.Uri()); + EXPECT_EQ(HttpMethod::Patch(), data.Method()); + EXPECT_EQ(body, data.Body()); + } + + TEST_F(ApiRequestDataTest, ItThrowsExceptionPatchRequestWithNoBody) + { + EXPECT_THROW(ApiRequestData data("uri", HttpMethod::Patch()), std::invalid_argument); + } + + TEST_F(ApiRequestDataTest, ItBuildsDeleteRequest) + { + ApiRequestData data("uri", HttpMethod::Delete()); + EXPECT_EQ("uri", data.Uri()); + EXPECT_EQ(HttpMethod::Delete(), data.Method()); + EXPECT_TRUE(data.Body().empty()); + } + + TEST_F(ApiRequestDataTest, EqualityReturnsTrueIfSame) + { + ApiRequestData data1("uri", HttpMethod::Patch(), {{"foo", "bar"}}); + ApiRequestData data2("uri", HttpMethod::Patch(), {{"foo", "bar"}}); + + EXPECT_TRUE(data1 == data2); + } + + TEST_F(ApiRequestDataTest, EqualityReturnsFalseIfUriDifferent) + { + ApiRequestData data1("uri", HttpMethod::Patch(), {{"foo", "bar"}}); + ApiRequestData data2("uri2", HttpMethod::Patch(), {{"foo", "bar"}}); + + EXPECT_FALSE(data1 == data2); + } + + TEST_F(ApiRequestDataTest, EqualityReturnsFalseIfMethodDifferent) + { + ApiRequestData data1("uri", HttpMethod::Patch(), {{"foo", "bar"}}); + ApiRequestData data2("uri", HttpMethod::Post(), {{"foo", "bar"}}); + + EXPECT_FALSE(data1 == data2); + } + + TEST_F(ApiRequestDataTest, EqualityReturnsFalseIfBodyDifferent) + { + ApiRequestData data1("uri", HttpMethod::Patch(), {{"foo", "bar"}}); + ApiRequestData data2("uri", HttpMethod::Patch(), {{"baz", "bosh"}}); + + EXPECT_FALSE(data1 == data2); + } +} // namespace UKControllerPluginUtilsTest::Api diff --git a/test/utils/api/ApiRequestExceptionTest.cpp b/test/utils/api/ApiRequestExceptionTest.cpp new file mode 100644 index 000000000..9645eb1e3 --- /dev/null +++ b/test/utils/api/ApiRequestExceptionTest.cpp @@ -0,0 +1,36 @@ +#include "api/ApiRequestException.h" + +using UKControllerPluginUtils::Api::ApiRequestException; +using UKControllerPluginUtils::Http::HttpStatusCode; + +namespace UKControllerPluginUtilsTest::Api { + class ApiRequestExceptionTest : public testing::Test + { + public: + ApiRequestExceptionTest() : exception("testuri", HttpStatusCode::ServerError, false) + { + } + + ApiRequestException exception; + }; + + TEST_F(ApiRequestExceptionTest, ItHasAUri) + { + EXPECT_EQ("testuri", exception.Uri()); + } + + TEST_F(ApiRequestExceptionTest, ItHasAStatusCode) + { + EXPECT_EQ(HttpStatusCode::ServerError, exception.StatusCode()); + } + + TEST_F(ApiRequestExceptionTest, ItHasWhetherJsonWasInvalid) + { + EXPECT_FALSE(exception.InvalidJson()); + } + + TEST_F(ApiRequestExceptionTest, ItHasAnExplanation) + { + EXPECT_EQ("Api request resulted in status " + std::to_string(500), exception.what()); + } +} // namespace UKControllerPluginUtilsTest::Api diff --git a/test/utils/api/ApiRequestFactoryTest.cpp b/test/utils/api/ApiRequestFactoryTest.cpp new file mode 100644 index 000000000..cbe58e55b --- /dev/null +++ b/test/utils/api/ApiRequestFactoryTest.cpp @@ -0,0 +1,86 @@ +#include "api/ApiRequestData.h" +#include "api/ApiRequestFactory.h" + +using UKControllerPluginUtils::Api::ApiRequestData; +using UKControllerPluginUtils::Api::ApiRequestFactory; +using UKControllerPluginUtils::Http::HttpMethod; +using UKControllerPluginUtils::Http::HttpStatusCode; + +namespace UKControllerPluginUtilsTest::Api { + class ApiRequestFactoryTest : public testing::Test + { + public: + ApiRequestFactoryTest() : response(HttpStatusCode::Ok, {}), factory(requestPerformer) + { + } + + Response response; + testing::NiceMock requestPerformer; + ApiRequestFactory factory; + }; + + TEST_F(ApiRequestFactoryTest, ItPerformsAGetRequest) + { + EXPECT_CALL(requestPerformer, Perform(ApiRequestData("someuri", HttpMethod::Get()))) + .Times(1) + .WillOnce(testing::Return(response)); + + bool callbackPerformed = false; + + factory.Get("someuri").Then([&callbackPerformed]() { callbackPerformed = true; }); + + factory.AwaitRequestCompletion(); + } + + TEST_F(ApiRequestFactoryTest, ItPerformsAPostRequest) + { + EXPECT_CALL(requestPerformer, Perform(ApiRequestData("someuri", HttpMethod::Post(), {{"foo", "bar"}}))) + .Times(1) + .WillOnce(testing::Return(response)); + + bool callbackPerformed = false; + + factory.Post("someuri", {{"foo", "bar"}}).Then([&callbackPerformed]() { callbackPerformed = true; }); + + factory.AwaitRequestCompletion(); + } + + TEST_F(ApiRequestFactoryTest, ItPerformsAPutRequest) + { + EXPECT_CALL(requestPerformer, Perform(ApiRequestData("someuri", HttpMethod::Put(), {{"foo", "bar"}}))) + .Times(1) + .WillOnce(testing::Return(response)); + + bool callbackPerformed = false; + + factory.Put("someuri", {{"foo", "bar"}}).Then([&callbackPerformed]() { callbackPerformed = true; }); + + factory.AwaitRequestCompletion(); + } + + TEST_F(ApiRequestFactoryTest, ItPerformsAPatchRequest) + { + EXPECT_CALL(requestPerformer, Perform(ApiRequestData("someuri", HttpMethod::Patch(), {{"foo", "bar"}}))) + .Times(1) + .WillOnce(testing::Return(response)); + + bool callbackPerformed = false; + + factory.Patch("someuri", {{"foo", "bar"}}).Then([&callbackPerformed]() { callbackPerformed = true; }); + + factory.AwaitRequestCompletion(); + } + + TEST_F(ApiRequestFactoryTest, ItPerformsADeleteRequest) + { + EXPECT_CALL(requestPerformer, Perform(ApiRequestData("someuri", HttpMethod::Delete()))) + .Times(1) + .WillOnce(testing::Return(response)); + + bool callbackPerformed = false; + + factory.Delete("someuri").Then([&callbackPerformed]() { callbackPerformed = true; }); + + factory.AwaitRequestCompletion(); + } +} // namespace UKControllerPluginUtilsTest::Api diff --git a/test/utils/api/ApiRequestTest.cpp b/test/utils/api/ApiRequestTest.cpp new file mode 100644 index 000000000..d833ef594 --- /dev/null +++ b/test/utils/api/ApiRequestTest.cpp @@ -0,0 +1,160 @@ +#include "api/ApiRequest.h" +#include "api/ApiRequestData.h" +#include "api/ApiRequestException.h" + +using UKControllerPluginUtils::Api::ApiRequest; +using UKControllerPluginUtils::Api::ApiRequestData; +using UKControllerPluginUtils::Api::ApiRequestException; +using UKControllerPluginUtils::Api::Response; +using UKControllerPluginUtils::Http::HttpMethod; +using UKControllerPluginUtils::Http::HttpStatusCode; + +namespace UKControllerPluginUtilsTest::Api { + class ApiRequestTest : public testing::Test + { + public: + ApiRequestTest() : mockResponse(HttpStatusCode::Ok, {}), requestData("someuri", HttpMethod::Get(), {}) + { + ON_CALL(performer, Perform).WillByDefault(testing::Return(mockResponse)); + } + + [[nodiscard]] auto GetRequest() -> class ApiRequest + { + return {requestData, performer, [this]() { requestComplete = true; }}; + }; + + bool requestComplete = false; + Response mockResponse; + testing::NiceMock performer; + ApiRequestData requestData; + }; + + TEST_F(ApiRequestTest, ItExecutesAChainedRequest) + { + auto callback1CallTime = (std::chrono::system_clock::time_point::min)(); + auto callback2CallTime = (std::chrono::system_clock::time_point::min)(); + auto callback3CallTime = (std::chrono::system_clock::time_point::min)(); + GetRequest() + .Then([&callback1CallTime]() -> void { + callback1CallTime = std::chrono::system_clock::now(); + std::this_thread::sleep_for(std::chrono::seconds(1)); + }) + .Then([&callback2CallTime](Response response) -> void { + EXPECT_EQ(HttpStatusCode::Ok, response.StatusCode()); + callback2CallTime = std::chrono::system_clock::now(); + std::this_thread::sleep_for(std::chrono::seconds(1)); + }) + .Then([&callback3CallTime](Response response) -> Response { + callback3CallTime = std::chrono::system_clock::now(); + return response; + }); + + auto testStart = std::chrono::system_clock::now(); + while (true) { + if (std::chrono::system_clock::now() > testStart + std::chrono::seconds(5)) { + throw std::exception("Test time exceeded"); + } + + if (requestComplete) { + break; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } + + EXPECT_NE((std::chrono::system_clock::time_point::min)(), callback1CallTime); + EXPECT_NE((std::chrono::system_clock::time_point::min)(), callback2CallTime); + EXPECT_NE((std::chrono::system_clock::time_point::min)(), callback3CallTime); + EXPECT_TRUE(callback1CallTime < callback2CallTime); + EXPECT_TRUE(callback2CallTime < callback3CallTime); + } + + TEST_F(ApiRequestTest, ItExecutesAChainedRequestSynchronously) + { + auto callback1CallTime = (std::chrono::system_clock::time_point::min)(); + auto callback2CallTime = (std::chrono::system_clock::time_point::min)(); + auto callback3CallTime = (std::chrono::system_clock::time_point::min)(); + + GetRequest() + .Then([&callback1CallTime]() { + callback1CallTime = std::chrono::system_clock::now(); + std::this_thread::sleep_for(std::chrono::seconds(1)); + }) + .Then([&callback2CallTime](Response response) { + EXPECT_EQ(HttpStatusCode::Ok, response.StatusCode()); + callback2CallTime = std::chrono::system_clock::now(); + std::this_thread::sleep_for(std::chrono::seconds(1)); + }) + .Then([&callback3CallTime](Response response) -> Response { + callback3CallTime = std::chrono::system_clock::now(); + return response; + }) + .Await(); + + EXPECT_NE((std::chrono::system_clock::time_point::min)(), callback1CallTime); + EXPECT_NE((std::chrono::system_clock::time_point::min)(), callback2CallTime); + EXPECT_NE((std::chrono::system_clock::time_point::min)(), callback3CallTime); + EXPECT_TRUE(callback1CallTime < callback2CallTime); + EXPECT_TRUE(callback2CallTime < callback3CallTime); + } + + TEST_F(ApiRequestTest, ItCallsExceptionHandler) + { + ON_CALL(performer, Perform) + .WillByDefault(testing::Throw(ApiRequestException("someuri", HttpStatusCode::ServerError, false))); + + bool exceptionHandlerCalled = false; + bool successCalled = false; + + try { + GetRequest() + .Then([&successCalled]() { successCalled = true; }) + .Catch([&exceptionHandlerCalled](const ApiRequestException&) { exceptionHandlerCalled = true; }) + .Await(); + } catch (...) { + } + + EXPECT_TRUE(exceptionHandlerCalled); + EXPECT_FALSE(successCalled); + } + + TEST_F(ApiRequestTest, ItCallsDefaultExceptionHandler) + { + ON_CALL(performer, Perform) + .WillByDefault(testing::Throw(ApiRequestException("someuri", HttpStatusCode::ServerError, false))); + + bool successCalled = false; + + GetRequest().Then([&successCalled]() { successCalled = true; }).Await(); + EXPECT_FALSE(successCalled); + } + + TEST_F(ApiRequestTest, ItCallsExceptionHandlerAsynchronously) + { + ON_CALL(performer, Perform) + .WillByDefault(testing::Throw(ApiRequestException("someuri", HttpStatusCode::ServerError, false))); + + bool exceptionHandlerCalled = false; + bool successCalled = false; + + GetRequest() + .Then([&successCalled]() { successCalled = true; }) + .Catch([&exceptionHandlerCalled](const ApiRequestException&) { exceptionHandlerCalled = true; }); + + auto testStart = std::chrono::system_clock::now(); + while (true) { + if (std::chrono::system_clock::now() > testStart + std::chrono::seconds(5)) { + throw std::exception("Test time exceeded"); + } + + if (exceptionHandlerCalled) { + break; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } + + EXPECT_TRUE(exceptionHandlerCalled); + EXPECT_FALSE(successCalled); + } +} // namespace UKControllerPluginUtilsTest::Api diff --git a/test/utils/api/ApiSettingsTest.cpp b/test/utils/api/ApiSettingsTest.cpp new file mode 100644 index 000000000..62c254224 --- /dev/null +++ b/test/utils/api/ApiSettingsTest.cpp @@ -0,0 +1,38 @@ +#include "api/ApiSettings.h" + +using UKControllerPluginUtils::Api::ApiSettings; + +namespace UKControllerPluginUtilsTest::Api { + class ApiSettingsTest : public testing::Test + { + public: + ApiSettingsTest() : settings("url", "key") + { + } + ApiSettings settings; + }; + + TEST_F(ApiSettingsTest, ItHasAUrl) + { + EXPECT_EQ("url", settings.Url()); + } + + TEST_F(ApiSettingsTest, ItHasAKey) + { + EXPECT_EQ("key", settings.Key()); + } + + TEST_F(ApiSettingsTest, ItCanSetItsUrl) + { + settings.Url("url2"); + EXPECT_EQ("url2", settings.Url()); + EXPECT_EQ("key", settings.Key()); + } + + TEST_F(ApiSettingsTest, ItCanSetItsKey) + { + settings.Key("key2"); + EXPECT_EQ("url", settings.Url()); + EXPECT_EQ("key2", settings.Key()); + } +} // namespace UKControllerPluginUtilsTest::Api diff --git a/test/utils/api/ApiUrlBuilderTest.cpp b/test/utils/api/ApiUrlBuilderTest.cpp new file mode 100644 index 000000000..f0779800e --- /dev/null +++ b/test/utils/api/ApiUrlBuilderTest.cpp @@ -0,0 +1,50 @@ +#include "api/ApiRequestData.h" +#include "api/ApiUrlBuilder.h" +#include "api/ApiSettings.h" + +using UKControllerPluginUtils::Api::ApiRequestData; +using UKControllerPluginUtils::Api::ApiSettings; +using UKControllerPluginUtils::Api::ApiUrlBuilder; +using UKControllerPluginUtils::Http::HttpMethod; + +namespace UKControllerPluginUtilsTest::Api { + class ApiUrlBuilderTest : public testing::Test + { + }; + + TEST_F(ApiUrlBuilderTest, ItBuildsUrl) + { + auto settings = ApiSettings("https://ukcp.vatsim.uk", "key"); + auto requestData = ApiRequestData("test", HttpMethod::Get()); + auto builder = ApiUrlBuilder(settings); + + EXPECT_EQ("https://ukcp.vatsim.uk/test", builder.BuildUrl(requestData)); + } + + TEST_F(ApiUrlBuilderTest, ItBuildsUrlRightTrimmingTheBaseUrl) + { + auto settings = ApiSettings("https://ukcp.vatsim.uk/", "key"); + auto requestData = ApiRequestData("test", HttpMethod::Get()); + auto builder = ApiUrlBuilder(settings); + + EXPECT_EQ("https://ukcp.vatsim.uk/test", builder.BuildUrl(requestData)); + } + + TEST_F(ApiUrlBuilderTest, ItBuildsUrlLeftTrimmingTheUri) + { + auto settings = ApiSettings("https://ukcp.vatsim.uk", "key"); + auto requestData = ApiRequestData("/test", HttpMethod::Get()); + auto builder = ApiUrlBuilder(settings); + + EXPECT_EQ("https://ukcp.vatsim.uk/test", builder.BuildUrl(requestData)); + } + + TEST_F(ApiUrlBuilderTest, ItBuildsUrlRightTrimmingTheUri) + { + auto settings = ApiSettings("https://ukcp.vatsim.uk", "key"); + auto requestData = ApiRequestData("test/", HttpMethod::Get()); + auto builder = ApiUrlBuilder(settings); + + EXPECT_EQ("https://ukcp.vatsim.uk/test", builder.BuildUrl(requestData)); + } +} // namespace UKControllerPluginUtilsTest::Api diff --git a/test/utils/api/ConfigApiSettingsProviderTest.cpp b/test/utils/api/ConfigApiSettingsProviderTest.cpp new file mode 100644 index 000000000..77762dae9 --- /dev/null +++ b/test/utils/api/ConfigApiSettingsProviderTest.cpp @@ -0,0 +1,106 @@ +#include "api/ApiSettings.h" +#include "api/ConfigApiSettingsProvider.h" + +using UKControllerPluginTest::Setting::MockSettingRepository; +using UKControllerPluginTest::Windows::MockWinApi; +using UKControllerPluginUtils::Api::ConfigApiSettingsProvider; + +namespace UKControllerPluginUtilsTest::Api { + class ConfigApiSettingsProviderTest : public testing::Test + { + public: + ConfigApiSettingsProviderTest() : provider(settings, windows) + { + } + + testing::NiceMock settings; + testing::NiceMock windows; + ConfigApiSettingsProvider provider; + }; + + TEST_F(ConfigApiSettingsProviderTest, ItReturnsSettings) + { + ON_CALL(settings, GetSetting("api-url", "https://ukcp.vatsim.uk")) + .WillByDefault(testing::Return("https://ukcp2.vatsim.uk")); + + ON_CALL(settings, GetSetting("api-key", "")).WillByDefault(testing::Return("key")); + + auto& apiSettings = provider.Get(); + EXPECT_EQ("https://ukcp2.vatsim.uk", apiSettings.Url()); + EXPECT_EQ("key", apiSettings.Key()); + } + + TEST_F(ConfigApiSettingsProviderTest, ItReturnsSameSettingsEachTime) + { + ON_CALL(settings, GetSetting("api-url", "https://ukcp.vatsim.uk")) + .WillByDefault(testing::Return("https://ukcp2.vatsim.uk")); + + ON_CALL(settings, GetSetting("api-key", "")).WillByDefault(testing::Return("key")); + + EXPECT_EQ(&provider.Get(), &provider.Get()); + } + + TEST_F(ConfigApiSettingsProviderTest, ItHasSettingsIfAKeyIsPresent) + { + ON_CALL(settings, GetSetting("api-url", "https://ukcp.vatsim.uk")) + .WillByDefault(testing::Return("https://ukcp2.vatsim.uk")); + + ON_CALL(settings, GetSetting("api-key", "")).WillByDefault(testing::Return("key")); + + EXPECT_TRUE(provider.Has()); + } + + TEST_F(ConfigApiSettingsProviderTest, ItDoesntHaveSettingsIfAKeyIsNotPresent) + { + ON_CALL(settings, GetSetting("api-url", "https://ukcp.vatsim.uk")) + .WillByDefault(testing::Return("https://ukcp2.vatsim.uk")); + + ON_CALL(settings, GetSetting("api-key", "")).WillByDefault(testing::Return("")); + + EXPECT_FALSE(provider.Has()); + } + + TEST_F(ConfigApiSettingsProviderTest, ItDoesntReloadSettingsIfUserDoesntSelectFile) + { + ON_CALL(settings, GetSetting("api-url", "https://ukcp.vatsim.uk")) + .WillByDefault(testing::Return("https://ukcp.vatsim.uk")); + + ON_CALL(settings, GetSetting("api-key", "")).WillByDefault(testing::Return("key")); + + ON_CALL(windows, FileOpenDialog(std::wstring(L"Select API Settings File"), 1, testing::_)) + .WillByDefault(testing::Return(L"")); + + EXPECT_CALL(windows, WriteToFile).Times(0); + + EXPECT_FALSE(provider.Reload()); + auto& apiSettings = provider.Get(); + EXPECT_EQ("https://ukcp.vatsim.uk", apiSettings.Url()); + EXPECT_EQ("key", apiSettings.Key()); + } + + TEST_F(ConfigApiSettingsProviderTest, ItReloadsSettings) + { + ON_CALL(settings, GetSetting("api-url", "https://ukcp.vatsim.uk")) + .WillByDefault(testing::Return("https://ukcp2.vatsim.uk")); + + ON_CALL(settings, GetSetting("api-key", "")).WillByDefault(testing::Return("key2")); + + ON_CALL(windows, FileOpenDialog(std::wstring(L"Select API Settings File"), 1, testing::_)) + .WillByDefault(testing::Return(L"foo/bar.json")); + + EXPECT_CALL(windows, ReadFromFileMock(std::wstring(L"foo/bar.json"), false)) + .Times(1) + .WillOnce(testing::Return("foo")); + + EXPECT_CALL(windows, WriteToFile(std::wstring(L"settings/api-settings.json"), "foo", true, false)).Times(1); + + EXPECT_CALL(settings, ReloadSetting("api-key")).Times(1); + + EXPECT_CALL(settings, ReloadSetting("api-url")).Times(1); + + EXPECT_TRUE(provider.Reload()); + auto& apiSettings = provider.Get(); + EXPECT_EQ("https://ukcp2.vatsim.uk", apiSettings.Url()); + EXPECT_EQ("key2", apiSettings.Key()); + } +} // namespace UKControllerPluginUtilsTest::Api diff --git a/test/utils/api/CurlApiRequestPerformerFactoryTest.cpp b/test/utils/api/CurlApiRequestPerformerFactoryTest.cpp new file mode 100644 index 000000000..71235b502 --- /dev/null +++ b/test/utils/api/CurlApiRequestPerformerFactoryTest.cpp @@ -0,0 +1,30 @@ +#include "api/ApiSettings.h" +#include "api/CurlApiRequestPerformerFactory.h" + +using UKControllerPluginTest::Curl::MockCurlApi; +using UKControllerPluginUtils::Api::ApiSettings; +using UKControllerPluginUtils::Api::CurlApiRequestPerformerFactory; + +namespace UKControllerPluginUtilsTest::Api { + class CurlApiRequestPerformerFactoryTest : public testing::Test + { + public: + CurlApiRequestPerformerFactoryTest() + : settings("url", "key"), factory(std::make_unique>()) + { + } + + ApiSettings settings; + CurlApiRequestPerformerFactory factory; + }; + + TEST_F(CurlApiRequestPerformerFactoryTest, ItReturnsPerformer) + { + EXPECT_NE(nullptr, &factory.Make(settings)); + } + + TEST_F(CurlApiRequestPerformerFactoryTest, ItReturnsTheSamePerformer) + { + EXPECT_EQ(&factory.Make(settings), &factory.Make(settings)); + } +} // namespace UKControllerPluginUtilsTest::Api diff --git a/test/utils/api/CurlApiRequestPerformerTest.cpp b/test/utils/api/CurlApiRequestPerformerTest.cpp new file mode 100644 index 000000000..e1112491e --- /dev/null +++ b/test/utils/api/CurlApiRequestPerformerTest.cpp @@ -0,0 +1,181 @@ +#include "api/ApiCurlRequestFactory.h" +#include "api/ApiHeaderApplicator.h" +#include "api/ApiRequestData.h" +#include "api/ApiSettings.h" +#include "api/ApiUrlBuilder.h" +#include "api/ApiRequestException.h" +#include "api/CurlApiRequestPerformer.h" +#include "curl/CurlRequest.h" +#include "curl/CurlResponse.h" + +using UKControllerPlugin::Curl::CurlRequest; +using UKControllerPlugin::Curl::CurlResponse; +using UKControllerPluginTest::Curl::MockCurlApi; +using UKControllerPluginUtils::Api::ApiCurlRequestFactory; +using UKControllerPluginUtils::Api::ApiHeaderApplicator; +using UKControllerPluginUtils::Api::ApiRequestData; +using UKControllerPluginUtils::Api::ApiRequestException; +using UKControllerPluginUtils::Api::ApiSettings; +using UKControllerPluginUtils::Api::ApiUrlBuilder; +using UKControllerPluginUtils::Api::CurlApiRequestPerformer; +using UKControllerPluginUtils::Http::HttpMethod; +using UKControllerPluginUtils::Http::HttpStatusCode; + +namespace UKControllerPluginUtilsTest::Api { + class CurlApiRequestPerformerTest : public testing::Test + { + public: + CurlApiRequestPerformerTest() + : requestData("test", HttpMethod::Get()), settings("https://ukcp.vatsim.uk", "key"), builder(settings), + headerApplicator(settings), requestFactory(builder, headerApplicator), performer(curl, requestFactory) + { + } + + [[nodiscard]] static auto GetRequest() -> CurlRequest + { + CurlRequest expectedRequest("https://ukcp.vatsim.uk/test", CurlRequest::METHOD_GET); + expectedRequest.AddHeader("Authorization", "Bearer key"); + expectedRequest.AddHeader("Accept", "application/json"); + expectedRequest.AddHeader("Content-Type", "application/json"); + return expectedRequest; + } + + [[nodiscard]] static auto GetResponseJson() -> std::string + { + return nlohmann::json{{"foo", "bar"}}.dump(); + } + + void TestSuccessStatus(HttpStatusCode statusCode) + { + EXPECT_CALL(curl, MakeCurlRequest(GetRequest())) + .Times(1) + .WillOnce(testing::Return(CurlResponse(GetResponseJson(), false, static_cast(statusCode)))); + + auto response = performer.Perform(requestData); + EXPECT_EQ(statusCode, response.StatusCode()); + EXPECT_EQ(nlohmann::json({{"foo", "bar"}}), response.Data()); + } + + void TestFailureStatus(HttpStatusCode statusCode) + { + EXPECT_CALL(curl, MakeCurlRequest(GetRequest())) + .Times(1) + .WillOnce(testing::Return(CurlResponse(GetResponseJson(), false, static_cast(statusCode)))); + + try { + performer.Perform(requestData); + } catch (ApiRequestException& exception) { + EXPECT_EQ("test", exception.Uri()); + EXPECT_EQ(statusCode, exception.StatusCode()); + EXPECT_FALSE(exception.InvalidJson()); + return; + } + + GTEST_FAIL(); + } + + testing::NiceMock curl; + ApiRequestData requestData; + ApiSettings settings; + ApiUrlBuilder builder; + ApiHeaderApplicator headerApplicator; + ApiCurlRequestFactory requestFactory; + CurlApiRequestPerformer performer; + }; + + TEST_F(CurlApiRequestPerformerTest, ItSucceedsOnOkStatus) + { + TestSuccessStatus(HttpStatusCode::Ok); + } + + TEST_F(CurlApiRequestPerformerTest, ItSucceedsOnCreatedStatus) + { + TestSuccessStatus(HttpStatusCode::Created); + } + + TEST_F(CurlApiRequestPerformerTest, ItSucceedsOnNoContentStatus) + { + TestSuccessStatus(HttpStatusCode::NoContent); + } + + TEST_F(CurlApiRequestPerformerTest, ItFailsOnBadRequestStatus) + { + TestFailureStatus(HttpStatusCode::BadRequest); + } + + TEST_F(CurlApiRequestPerformerTest, ItFailsOnUnauthorisedStatus) + { + TestFailureStatus(HttpStatusCode::Unauthorised); + } + + TEST_F(CurlApiRequestPerformerTest, ItFailsOnForbiddenStatus) + { + TestFailureStatus(HttpStatusCode::Forbidden); + } + + TEST_F(CurlApiRequestPerformerTest, ItFailsOnNotFoundStatus) + { + TestFailureStatus(HttpStatusCode::NotFound); + } + + TEST_F(CurlApiRequestPerformerTest, ItFailsOnMethodNotAllowedStatus) + { + TestFailureStatus(HttpStatusCode::MethodNotAllowed); + } + + TEST_F(CurlApiRequestPerformerTest, ItFailsOnUnprocessableStatus) + { + TestFailureStatus(HttpStatusCode::Unprocessable); + } + + TEST_F(CurlApiRequestPerformerTest, ItFailsOnServerErrorStatus) + { + TestFailureStatus(HttpStatusCode::ServerError); + } + + TEST_F(CurlApiRequestPerformerTest, ItFailsOnBadGatewayStatus) + { + TestFailureStatus(HttpStatusCode::BadGateway); + } + + TEST_F(CurlApiRequestPerformerTest, ItFailsOnUnknownStatus) + { + TestFailureStatus(static_cast(999L)); + } + + TEST_F(CurlApiRequestPerformerTest, ItFailsOnCurlError) + { + EXPECT_CALL(curl, MakeCurlRequest(GetRequest())) + .Times(1) + .WillOnce(testing::Return(CurlResponse(GetResponseJson(), true, 500L))); + + try { + performer.Perform(requestData); + } catch (ApiRequestException& exception) { + EXPECT_EQ("test", exception.Uri()); + EXPECT_EQ(HttpStatusCode::ServerError, exception.StatusCode()); + EXPECT_FALSE(exception.InvalidJson()); + return; + } + + GTEST_FAIL(); + } + + TEST_F(CurlApiRequestPerformerTest, ItFailsOnBadJson) + { + EXPECT_CALL(curl, MakeCurlRequest(GetRequest())) + .Times(1) + .WillOnce(testing::Return(CurlResponse("{]", false, 200L))); + + try { + performer.Perform(requestData); + } catch (ApiRequestException& exception) { + EXPECT_EQ("test", exception.Uri()); + EXPECT_EQ(HttpStatusCode::Ok, exception.StatusCode()); + EXPECT_TRUE(exception.InvalidJson()); + return; + } + + GTEST_FAIL(); + } +} // namespace UKControllerPluginUtilsTest::Api diff --git a/test/utils/api/ResponseTest.cpp b/test/utils/api/ResponseTest.cpp new file mode 100644 index 000000000..092c68ed7 --- /dev/null +++ b/test/utils/api/ResponseTest.cpp @@ -0,0 +1,26 @@ +#include "api/Response.h" + +using UKControllerPluginUtils::Api::Response; +using UKControllerPluginUtils::Http::HttpStatusCode; + +namespace UKControllerPluginUtilsTest::Api { + class ResponseTest : public testing::Test + { + public: + ResponseTest() : response(HttpStatusCode::Ok, {{"foo", "bar"}}) + { + } + + Response response; + }; + + TEST_F(ResponseTest, ItHasAStatusCode) + { + EXPECT_EQ(HttpStatusCode::Ok, response.StatusCode()); + } + + TEST_F(ResponseTest, ItHasData) + { + EXPECT_EQ(nlohmann::json({{"foo", "bar"}}), response.Data()); + } +} // namespace UKControllerPluginUtilsTest::Api diff --git a/test/utils/curl/CurlRequestTest.cpp b/test/utils/curl/CurlRequestTest.cpp index c5146c684..81a4e0a1a 100644 --- a/test/utils/curl/CurlRequestTest.cpp +++ b/test/utils/curl/CurlRequestTest.cpp @@ -1,10 +1,10 @@ -#include "pch/pch.h" #include "curl/CurlRequest.h" +#include "http/HttpMethod.h" using UKControllerPlugin::Curl::CurlRequest; +using UKControllerPluginUtils::Http::HttpMethod; -namespace UKControllerPluginUtilsTest -{ +namespace UKControllerPluginUtilsTest { namespace Curl { TEST(CurlRequestTest, TestConstructorSetsUri) @@ -19,6 +19,12 @@ namespace UKControllerPluginUtilsTest EXPECT_EQ(0, strcmp(request.GetMethod(), CurlRequest::METHOD_GET.c_str())); } + TEST(CurlRequestTest, TestConstructorSetsMethodFromHttpMethodEnum) + { + CurlRequest request("http://test.com/abc", HttpMethod::Get()); + EXPECT_EQ(0, strcmp(request.GetMethod(), CurlRequest::METHOD_GET.c_str())); + } + TEST(CurlRequestTest, TestConstructorSetsMaxRequestTimeIfNotSpecified) { CurlRequest request("http://test.com/abc", CurlRequest::METHOD_GET); @@ -146,5 +152,5 @@ namespace UKControllerPluginUtilsTest request2.AddHeader("Test1", "Test4"); EXPECT_FALSE(request1 == request2); } - } // namespace Curl + } // namespace Curl } // namespace UKControllerPluginUtilsTest diff --git a/test/utils/http/HttpMethodTest.cpp b/test/utils/http/HttpMethodTest.cpp new file mode 100644 index 000000000..922098192 --- /dev/null +++ b/test/utils/http/HttpMethodTest.cpp @@ -0,0 +1,44 @@ +#include "http/HttpMethod.h" + +using UKControllerPluginUtils::Http::HttpMethod; + +namespace UKControllerPluginUtilsTest::Http { + class HttpMethodTest : public testing::Test + { + }; + + TEST_F(HttpMethodTest, ItHasAGetMethod) + { + EXPECT_EQ("GET", static_cast(HttpMethod::Get())); + } + + TEST_F(HttpMethodTest, ItHasAPostMethod) + { + EXPECT_EQ("POST", static_cast(HttpMethod::Post())); + } + + TEST_F(HttpMethodTest, ItHasAPutMethod) + { + EXPECT_EQ("PUT", static_cast(HttpMethod::Put())); + } + + TEST_F(HttpMethodTest, ItHasAPatchMethod) + { + EXPECT_EQ("PATCH", static_cast(HttpMethod::Patch())); + } + + TEST_F(HttpMethodTest, ItHasADeleteMethod) + { + EXPECT_EQ("DELETE", static_cast(HttpMethod::Delete())); + } + + TEST_F(HttpMethodTest, EqualityIsTrueIfMethodsSame) + { + EXPECT_TRUE(HttpMethod::Delete() == HttpMethod::Delete()); + } + + TEST_F(HttpMethodTest, EqualityIsFalseIfMethodsDifferent) + { + EXPECT_FALSE(HttpMethod::Patch() == HttpMethod::Delete()); + } +} // namespace UKControllerPluginUtilsTest::Http diff --git a/test/utils/http/HttpStatusCodeTest.cpp b/test/utils/http/HttpStatusCodeTest.cpp new file mode 100644 index 000000000..6c77b5108 --- /dev/null +++ b/test/utils/http/HttpStatusCodeTest.cpp @@ -0,0 +1,97 @@ +#include "http/HttpStatusCode.h" + +using UKControllerPluginUtils::Http::HttpStatusCode; +using UKControllerPluginUtils::Http::IsAuthenticationError; +using UKControllerPluginUtils::Http::IsServerError; +using UKControllerPluginUtils::Http::IsSuccessful; + +namespace UKControllerPluginUtilsTest::Http { + class HttpStatusCodeTest : public testing::Test + { + }; + + TEST_F(HttpStatusCodeTest, IsEqualToUnsignedIntIfMatchesBackedValue) + { + EXPECT_TRUE(200L == HttpStatusCode::Ok); + } + + TEST_F(HttpStatusCodeTest, IsNotEqualToUnsignedIntIfDoesntMatcheBackedValue) + { + EXPECT_FALSE(400L == HttpStatusCode::Ok); + } + + TEST_F(HttpStatusCodeTest, ItIsAnAuthenticationErrorIfNotAuthorised) + { + EXPECT_TRUE(IsAuthenticationError(HttpStatusCode::Unauthorised)); + } + + TEST_F(HttpStatusCodeTest, ItIsAnAuthenticationErrorIfForbidden) + { + EXPECT_TRUE(IsAuthenticationError(HttpStatusCode::Forbidden)); + } + + TEST_F(HttpStatusCodeTest, ItIsNotAnAuthenticationErrorIfOk) + { + EXPECT_FALSE(IsAuthenticationError(HttpStatusCode::Ok)); + } + + TEST_F(HttpStatusCodeTest, ItIsAServerErrorIfServerError) + { + EXPECT_TRUE(IsServerError(HttpStatusCode::ServerError)); + } + + TEST_F(HttpStatusCodeTest, ItIsAServerErrorIfBadGateway) + { + EXPECT_TRUE(IsServerError(HttpStatusCode::BadGateway)); + } + + TEST_F(HttpStatusCodeTest, ItIsAServerErrorIfUnknown) + { + EXPECT_TRUE(IsServerError(HttpStatusCode::Unknown)); + } + + TEST_F(HttpStatusCodeTest, ItIsNotAnServerErrorIfOk) + { + EXPECT_FALSE(IsServerError(HttpStatusCode::Ok)); + } + + TEST_F(HttpStatusCodeTest, ItIsSuccessfulIfOk) + { + EXPECT_TRUE(IsSuccessful(HttpStatusCode::Ok)); + } + + TEST_F(HttpStatusCodeTest, ItIsSuccessfulIfCreated) + { + EXPECT_TRUE(IsSuccessful(HttpStatusCode::Created)); + } + + TEST_F(HttpStatusCodeTest, ItIsSuccessfulIfNoContent) + { + EXPECT_TRUE(IsSuccessful(HttpStatusCode::NoContent)); + } + + TEST_F(HttpStatusCodeTest, ItIsNotSuccessfulIfUnauthorised) + { + EXPECT_FALSE(IsSuccessful(HttpStatusCode::Unauthorised)); + } + + TEST_F(HttpStatusCodeTest, ItIsNotSuccessfulIfForbidden) + { + EXPECT_FALSE(IsSuccessful(HttpStatusCode::Forbidden)); + } + + TEST_F(HttpStatusCodeTest, ItIsNotSuccessfulIfUnprocessable) + { + EXPECT_FALSE(IsSuccessful(HttpStatusCode::Unprocessable)); + } + + TEST_F(HttpStatusCodeTest, ItIsNotSuccessfulIfServerError) + { + EXPECT_FALSE(IsSuccessful(HttpStatusCode::ServerError)); + } + + TEST_F(HttpStatusCodeTest, ItIsNotSuccessfulIfUnknown) + { + EXPECT_FALSE(IsSuccessful(HttpStatusCode::Unknown)); + } +} // namespace UKControllerPluginUtilsTest::Http diff --git a/test/utils/mock/MockSettingProvider.h b/test/utils/mock/MockSettingProvider.h new file mode 100644 index 000000000..970dc9bb6 --- /dev/null +++ b/test/utils/mock/MockSettingProvider.h @@ -0,0 +1,14 @@ +#include "setting/SettingProviderInterface.h" + +using UKControllerPlugin::Setting::SettingProviderInterface; + +namespace UKControllerPluginTest::Setting { + class MockSettingProvider : public SettingProviderInterface + { + public: + MOCK_METHOD(std::string, Get, (const std::string&), (override)); + MOCK_METHOD(void, Save, (const std::string&, const std::string&), (override)); + MOCK_METHOD(const std::set&, Provides, (), (override)); + MOCK_METHOD(void, Reload, (), (override)); + }; +} // namespace UKControllerPluginTest::Setting diff --git a/test/utils/pch/pch.h b/test/utils/pch/pch.h index 9b10dbceb..b4e76fcc4 100644 --- a/test/utils/pch/pch.h +++ b/test/utils/pch/pch.h @@ -12,7 +12,12 @@ #include "gmock/gmock.h" #pragma warning(pop) +#include "mock/MockSettingProvider.h" #include "../../testingutils/mock/MockApiInterface.h" +#include "../../testingutils/mock/MockApiRequestPerformer.h" +#include "../../testingutils/mock/MockApiRequestPerformerFactory.h" +#include "../../testingutils/mock/MockApiSettingsProvider.h" #include "../../testingutils/mock/MockCurlApi.h" #include "../../testingutils/mock/MockDialogProvider.h" +#include "../../testingutils/mock/MockSettingRepository.h" #include "../../testingutils/mock/MockWinApi.h" diff --git a/test/utils/setting/JsonFileSettingProviderTest.cpp b/test/utils/setting/JsonFileSettingProviderTest.cpp new file mode 100644 index 000000000..5558736f1 --- /dev/null +++ b/test/utils/setting/JsonFileSettingProviderTest.cpp @@ -0,0 +1,120 @@ +#include "setting/JsonFileSettingProvider.h" + +using UKControllerPlugin::Setting::JsonFileSettingProvider; + +namespace UKControllerPluginTest::Setting { + class JsonFileSettingProviderTest : public testing::Test + { + public: + JsonFileSettingProviderTest() + { + ON_CALL(windows, FileExists(std::wstring(L"settings/setting-file.json"))) + .WillByDefault(testing::Return(true)); + + nlohmann::json settings{ + {"setting1", "value1"}, + {"setting2", "value2"}, + }; + + ON_CALL(windows, ReadFromFileMock(std::wstring(L"settings/setting-file.json"), true)) + .WillByDefault(testing::Return(settings.dump())); + } + + [[nodiscard]] auto GetProvider() -> JsonFileSettingProvider + { + return {L"setting-file.json", std::set{"setting1", "setting2"}, windows}; + } + + testing::NiceMock windows; + }; + + TEST_F(JsonFileSettingProviderTest, ItReturnsSettingValues) + { + auto provider = GetProvider(); + EXPECT_EQ("value1", provider.Get("setting1")); + EXPECT_EQ("value2", provider.Get("setting2")); + } + + TEST_F(JsonFileSettingProviderTest, ItReturnsEmptyNoSetting) + { + EXPECT_TRUE(GetProvider().Get("setting3").empty()); + } + + TEST_F(JsonFileSettingProviderTest, ItProvidesSettingValues) + { + EXPECT_EQ(std::set({"setting1", "setting2"}), GetProvider().Provides()); + } + + TEST_F(JsonFileSettingProviderTest, ItWritesSettings) + { + nlohmann::json updatedSettings{{"setting2", "value2a"}, {"setting1", "value1"}}; + + EXPECT_CALL( + windows, WriteToFile(std::wstring(L"settings/setting-file.json"), updatedSettings.dump(), true, false)) + .Times(1); + + auto provider = GetProvider(); + provider.Save("setting2", "value2a"); + EXPECT_EQ("value2a", provider.Get("setting2")); + } + + TEST_F(JsonFileSettingProviderTest, ItReloadsSettingValues) + { + auto provider = GetProvider(); + EXPECT_EQ("value1", provider.Get("setting1")); + EXPECT_EQ("value2", provider.Get("setting2")); + + nlohmann::json settings{ + {"setting1", "value1a"}, + {"setting2", "value2a"}, + }; + + ON_CALL(windows, ReadFromFileMock(std::wstring(L"settings/setting-file.json"), true)) + .WillByDefault(testing::Return(settings.dump())); + + provider.Reload(); + EXPECT_EQ("value1a", provider.Get("setting1")); + EXPECT_EQ("value2a", provider.Get("setting2")); + } + + TEST_F(JsonFileSettingProviderTest, ItLoadsNoSettingsIfSettingFileDoesNotExist) + { + ON_CALL(windows, FileExists(std::wstring(L"settings/setting-file.json"))).WillByDefault(testing::Return(false)); + auto provider = GetProvider(); + EXPECT_TRUE(provider.Get("setting1").empty()); + EXPECT_TRUE(provider.Get("setting2").empty()); + } + + TEST_F(JsonFileSettingProviderTest, ItLoadsNoSettingsIfFileIsNotValidJson) + { + ON_CALL(windows, ReadFromFileMock(std::wstring(L"settings/setting-file.json"), true)) + .WillByDefault(testing::Return("{]")); + auto provider = GetProvider(); + EXPECT_TRUE(provider.Get("setting1").empty()); + EXPECT_TRUE(provider.Get("setting2").empty()); + } + + TEST_F(JsonFileSettingProviderTest, ItLoadsNoSettingsIfJsonIsNotObject) + { + ON_CALL(windows, ReadFromFileMock(std::wstring(L"settings/setting-file.json"), true)) + .WillByDefault(testing::Return("[]")); + auto provider = GetProvider(); + EXPECT_TRUE(provider.Get("setting1").empty()); + EXPECT_TRUE(provider.Get("setting2").empty()); + } + + TEST_F(JsonFileSettingProviderTest, ItLoadsNoSettingsIfKeysNotStrings) + { + nlohmann::json settings{ + {"setting1", 1}, + {"setting2", 2}, + }; + + ON_CALL(windows, ReadFromFileMock(std::wstring(L"settings/setting-file.json"), true)) + .WillByDefault(testing::Return(settings.dump())); + + auto provider = GetProvider(); + EXPECT_TRUE(provider.Get("setting1").empty()); + EXPECT_TRUE(provider.Get("setting2").empty()); + } +} // namespace UKControllerPluginTest::Setting diff --git a/test/utils/setting/SettingRepositoryFactoryTest.cpp b/test/utils/setting/SettingRepositoryFactoryTest.cpp index d2f128307..6a1bcc7f3 100644 --- a/test/utils/setting/SettingRepositoryFactoryTest.cpp +++ b/test/utils/setting/SettingRepositoryFactoryTest.cpp @@ -9,27 +9,8 @@ using UKControllerPluginTest::Windows::MockWinApi; namespace UKControllerPluginUtilsTest::Setting { - TEST(SettingRepositoryFactory, CreateLoadsApiConfigSettings) + TEST(SettingRepositoryFactory, CreateReturnsARepository) { - StrictMock mockWinApi; - - EXPECT_CALL(mockWinApi, FileExists(std::wstring(L"settings/api-settings.json"))) - .Times(1) - .WillOnce(Return(true)); - - EXPECT_CALL(mockWinApi, ReadFromFileMock(std::wstring(L"settings/api-settings.json"), true)) - .Times(1) - .WillOnce(Return("{\"api-key\": \"testkey\"}")); - - EXPECT_CALL(mockWinApi, FileExists(std::wstring(L"settings/release-channel.json"))) - .Times(1) - .WillOnce(Return(true)); - - EXPECT_CALL(mockWinApi, ReadFromFileMock(std::wstring(L"settings/release-channel.json"), true)) - .Times(1) - .WillOnce(Return("{\"release_channel\": \"beta\"}")); - - std::unique_ptr repo = SettingRepositoryFactory::Create(mockWinApi); - EXPECT_TRUE("beta" == repo->GetSetting("release_channel")); + EXPECT_EQ(0, SettingRepositoryFactory::Create()->CountSettings()); } } // namespace UKControllerPluginUtilsTest::Setting diff --git a/test/utils/setting/SettingRepositoryTest.cpp b/test/utils/setting/SettingRepositoryTest.cpp index 0af55a5f2..0e1692835 100644 --- a/test/utils/setting/SettingRepositoryTest.cpp +++ b/test/utils/setting/SettingRepositoryTest.cpp @@ -1,275 +1,140 @@ #include "setting/SettingRepository.h" -#include "setting/SettingValue.h" -using ::testing::Return; -using ::testing::StrictMock; -using ::testing::Throw; using UKControllerPlugin::Setting::SettingRepository; -using UKControllerPlugin::Setting::SettingValue; -using UKControllerPluginTest::Windows::MockWinApi; +using UKControllerPluginTest::Setting::MockSettingProvider; namespace UKControllerPluginUtilsTest::Setting { - TEST(SettingRepository, AddSettingValueAddsSetting) + class SettingRepositoryTest : public testing::Test { - StrictMock winApiMock; - SettingRepository repo(winApiMock); - - EXPECT_CALL( - winApiMock, WriteToFile(std::wstring(L"settings/baz"), nlohmann::json{{"foo", "bar"}}.dump(4), true, false)) - .Times(1); - - repo.AddSettingValue({"foo", "bar", "baz"}); - EXPECT_EQ(1, repo.SettingsCount()); + public: + SettingRepositoryTest() + : provider1Settings({"setting1", "setting2"}), provider2Settings({"setting3"}), + mockProvider1(std::make_shared>()), + mockProvider2(std::make_shared>()) + { + ON_CALL(*mockProvider1, Provides).WillByDefault(testing::ReturnRef(provider1Settings)); + + ON_CALL(*mockProvider2, Provides).WillByDefault(testing::ReturnRef(provider2Settings)); + } + + std::set provider1Settings; + std::set provider2Settings; + std::shared_ptr> mockProvider1; + std::shared_ptr> mockProvider2; + SettingRepository repository; + }; + + TEST_F(SettingRepositoryTest, ItStartsWithNoSettings) + { + EXPECT_EQ(0, repository.CountSettings()); } - TEST(SettingRepository, AddSettingValueDoesntOverwrite) + TEST_F(SettingRepositoryTest, ItAddsSettingProviders) { - StrictMock winApiMock; - SettingRepository repo(winApiMock); - - EXPECT_CALL( - winApiMock, WriteToFile(std::wstring(L"settings/baz"), nlohmann::json{{"foo", "bar"}}.dump(4), true, false)) - .Times(1); - - EXPECT_CALL( - winApiMock, - WriteToFile(std::wstring(L"settings/barrie"), nlohmann::json{{"foo", "bob"}}.dump(4), true, false)) - .Times(0); - - repo.AddSettingValue({"foo", "bar", "baz"}); - repo.AddSettingValue({"foo", "bob", "barrie"}); - - EXPECT_EQ("bar", repo.GetSetting("foo")); - EXPECT_EQ(1, repo.SettingsCount()); + repository.AddProvider(mockProvider1); + repository.AddProvider(mockProvider2); + EXPECT_EQ(3, repository.CountSettings()); + EXPECT_TRUE(repository.HasSetting("setting1")); + EXPECT_TRUE(repository.HasSetting("setting2")); + EXPECT_TRUE(repository.HasSetting("setting3")); } - TEST(SettingRepository, AddSettingsFromJsonFileStopsIfFileDoesntExist) + TEST_F(SettingRepositoryTest, ItDoesntAddDuplicateProviders) { - StrictMock winApiMock; - - EXPECT_CALL(winApiMock, FileExists(std::wstring(L"settings/test/test.json"))).Times(1).WillOnce(Return(false)); - - SettingRepository repo(winApiMock); - repo.AddSettingsFromJsonFile("test/test.json"); - EXPECT_EQ(0, repo.SettingsCount()); + repository.AddProvider(mockProvider1); + repository.AddProvider(mockProvider1); + repository.AddProvider(mockProvider1); + repository.AddProvider(mockProvider2); + repository.AddProvider(mockProvider2); + repository.AddProvider(mockProvider2); + EXPECT_EQ(3, repository.CountSettings()); + EXPECT_TRUE(repository.HasSetting("setting1")); + EXPECT_TRUE(repository.HasSetting("setting2")); + EXPECT_TRUE(repository.HasSetting("setting3")); } - TEST(SettingRepository, AddSettingsFromJsonFileStopsIfFileReadError) + TEST_F(SettingRepositoryTest, ItReturnsDefaultValueIfNoProviderForSetting) { - StrictMock winApiMock; - - EXPECT_CALL(winApiMock, FileExists(std::wstring(L"settings/test/test.json"))).Times(1).WillOnce(Return(true)); + repository.AddProvider(mockProvider1); + repository.AddProvider(mockProvider2); - EXPECT_CALL(winApiMock, ReadFromFileMock(std::wstring(L"settings/test/test.json"), true)) - .Times(1) - .WillOnce(Throw(std::ifstream::failure("test"))); - - SettingRepository repo(winApiMock); - repo.AddSettingsFromJsonFile("test/test.json"); - EXPECT_EQ(0, repo.SettingsCount()); + EXPECT_EQ("abc", repository.GetSetting("test1", "abc")); } - TEST(SettingRepository, AddSettingsFromJsonFileStopsIfInvalidJson) + TEST_F(SettingRepositoryTest, ItReturnsEmptyValueIfNoProviderForSettingAndNoDefaultProvided) { - StrictMock winApiMock; - - EXPECT_CALL(winApiMock, FileExists(std::wstring(L"settings/test/test.json"))).Times(1).WillOnce(Return(true)); + repository.AddProvider(mockProvider1); + repository.AddProvider(mockProvider2); - EXPECT_CALL(winApiMock, ReadFromFileMock(std::wstring(L"settings/test/test.json"), true)) - .Times(1) - .WillOnce(Return("{{}")); - - SettingRepository repo(winApiMock); - repo.AddSettingsFromJsonFile("test/test.json"); - EXPECT_EQ(0, repo.SettingsCount()); + EXPECT_EQ("", repository.GetSetting("test1")); } - TEST(SettingRepository, AddSettingsFromJsonFileStopsIfNotJsonObject) + TEST_F(SettingRepositoryTest, ItReturnsDefaultValueIfProviderReturnsEmpty) { - StrictMock winApiMock; - - EXPECT_CALL(winApiMock, FileExists(std::wstring(L"settings/test/test.json"))).Times(1).WillOnce(Return(true)); + EXPECT_CALL(*mockProvider1, Get("setting1")).Times(1).WillOnce(testing::Return("")); - EXPECT_CALL(winApiMock, ReadFromFileMock(std::wstring(L"settings/test/test.json"), true)) - .Times(1) - .WillOnce(Return("abcd")); + repository.AddProvider(mockProvider1); + repository.AddProvider(mockProvider2); - SettingRepository repo(winApiMock); - repo.AddSettingsFromJsonFile("test/test.json"); - EXPECT_EQ(0, repo.SettingsCount()); + EXPECT_EQ("abc", repository.GetSetting("setting1", "abc")); } - TEST(SettingRepository, AddSettingsFromJsonFileAddsSettings) + TEST_F(SettingRepositoryTest, ItReturnsValueFromProvider) { - StrictMock winApiMock; - - EXPECT_CALL(winApiMock, FileExists(std::wstring(L"settings/test/test.json"))).Times(1).WillOnce(Return(true)); + EXPECT_CALL(*mockProvider1, Get("setting1")).Times(1).WillOnce(testing::Return("123")); - EXPECT_CALL(winApiMock, ReadFromFileMock(std::wstring(L"settings/test/test.json"), true)) - .Times(1) - .WillOnce(Return("{\"test1\": \"testValue1\", \"test2\": \"testValue2\"}")); + repository.AddProvider(mockProvider1); + repository.AddProvider(mockProvider2); - SettingRepository repo(winApiMock); - repo.AddSettingsFromJsonFile("test/test.json"); - EXPECT_EQ(2, repo.SettingsCount()); - EXPECT_TRUE(repo.HasSetting("test1")); - EXPECT_TRUE(repo.HasSetting("test2")); - EXPECT_TRUE(repo.GetSetting("test1") == "testValue1"); - EXPECT_TRUE(repo.GetSetting("test2") == "testValue2"); + EXPECT_EQ("123", repository.GetSetting("setting1")); } - TEST(SettingRepository, AddSettingsFromJsonFileIgnoresDuplicateSettings) + TEST_F(SettingRepositoryTest, ItDoesntUpdateSettingIfNoProvider) { - StrictMock winApiMock; + EXPECT_CALL(*mockProvider1, Save(testing::_, testing::_)).Times(0); - EXPECT_CALL(winApiMock, FileExists(std::wstring(L"settings/test/test1.json"))).Times(1).WillOnce(Return(true)); + EXPECT_CALL(*mockProvider2, Save(testing::_, testing::_)).Times(0); - EXPECT_CALL(winApiMock, FileExists(std::wstring(L"settings/test/test2.json"))).Times(1).WillOnce(Return(true)); + repository.AddProvider(mockProvider1); + repository.AddProvider(mockProvider2); - EXPECT_CALL(winApiMock, ReadFromFileMock(std::wstring(L"settings/test/test1.json"), true)) - .Times(1) - .WillOnce(Return("{\"test1\": \"testValue1\"}")); - - EXPECT_CALL(winApiMock, ReadFromFileMock(std::wstring(L"settings/test/test2.json"), true)) - .Times(1) - .WillOnce(Return("{\"test1\": \"testValue2\"}")); - - SettingRepository repo(winApiMock); - repo.AddSettingsFromJsonFile("test/test1.json"); - repo.AddSettingsFromJsonFile("test/test2.json"); - EXPECT_TRUE(repo.HasSetting("test1")); - EXPECT_TRUE(repo.GetSetting("test1") == "testValue1"); + repository.UpdateSetting("setting4", "abc"); } - TEST(SettingRepository, GetSettingReturnsEmptyStringByDefaultIfNotSet) + TEST_F(SettingRepositoryTest, ItUpdatesSetting) { - StrictMock winApiMock; + EXPECT_CALL(*mockProvider1, Save(testing::_, testing::_)).Times(0); - SettingRepository repo(winApiMock); - EXPECT_TRUE("" == repo.GetSetting("test")); - } + EXPECT_CALL(*mockProvider2, Save("setting3", "abc")).Times(1); - TEST(SettingRepository, GetSettingReturnsDefaultvalueIfProvided) - { - StrictMock winApiMock; + repository.AddProvider(mockProvider1); + repository.AddProvider(mockProvider2); - SettingRepository repo(winApiMock); - EXPECT_TRUE("test2" == repo.GetSetting("test", "test2")); + repository.UpdateSetting("setting3", "abc"); } - TEST(SettingRepository, UpdateSettingChangesSettingValue) + TEST_F(SettingRepositoryTest, ItDoesntReloadSettingIfNoProvider) { - StrictMock winApiMock; - - EXPECT_CALL(winApiMock, FileExists(std::wstring(L"settings/test/test.json"))).Times(1).WillOnce(Return(true)); - - EXPECT_CALL(winApiMock, ReadFromFileMock(std::wstring(L"settings/test/test.json"), true)) - .Times(1) - .WillOnce(Return("{\"test1\": \"testValue1\", \"test2\": \"testValue2\"}")); - - EXPECT_CALL( - winApiMock, - WriteToFile( - std::wstring(L"settings/test/test.json"), - nlohmann::json{{"test1", "notTestValue1"}, {"test2", "testValue2"}}.dump(4), - true, - false)) - .Times(1); - - SettingRepository repo(winApiMock); - repo.AddSettingsFromJsonFile("test/test.json"); - repo.UpdateSetting("test1", "notTestValue1"); - EXPECT_TRUE(repo.HasSetting("test1")); - EXPECT_TRUE(repo.GetSetting("test1") == "notTestValue1"); - } + EXPECT_CALL(*mockProvider1, Reload()).Times(0); - TEST(SettingRepository, UpdateSettingDoesNothingIfSettingDoesNotExist) - { - StrictMock winApiMock; - SettingRepository repo(winApiMock); - repo.UpdateSetting("test1", "notTestValue1"); - EXPECT_FALSE(repo.HasSetting("test2")); - } + EXPECT_CALL(*mockProvider2, Reload()).Times(0); - TEST(SettingRepository, WriteAllSettingsToFileHandlesSingleFile) - { - StrictMock winApiMock; - - EXPECT_CALL(winApiMock, FileExists(std::wstring(L"settings/test/test.json"))).Times(1).WillOnce(Return(true)); - - EXPECT_CALL(winApiMock, ReadFromFileMock(std::wstring(L"settings/test/test.json"), true)) - .Times(1) - .WillOnce(Return("{\"test1\": \"testValue1\", \"test2\": \"testValue2\"}")); - - EXPECT_CALL( - winApiMock, - WriteToFile( - std::wstring(L"settings/test/test.json"), - "{\n \"test1\": \"testValue1\",\n \"test2\": \"testValue2\"\n}", - true, - false)) - .Times(1); - - SettingRepository repo(winApiMock); - repo.AddSettingsFromJsonFile("test/test.json"); - repo.WriteAllSettingsToFile(); - } + repository.AddProvider(mockProvider1); + repository.AddProvider(mockProvider2); - TEST(SettingRepository, WriteAllSettingsToFileHandlesUpdatedSettings) - { - StrictMock winApiMock; - - EXPECT_CALL(winApiMock, FileExists(std::wstring(L"settings/test/test.json"))).Times(1).WillOnce(Return(true)); - - EXPECT_CALL(winApiMock, ReadFromFileMock(std::wstring(L"settings/test/test.json"), true)) - .Times(1) - .WillOnce(Return("{\"test1\": \"testValue1\", \"test2\": \"testValue2\"}")); - - EXPECT_CALL( - winApiMock, - WriteToFile( - std::wstring(L"settings/test/test.json"), - "{\n \"test1\": \"notTestValue1\",\n \"test2\": \"testValue2\"\n}", - true, - false)) - .Times(2); - - SettingRepository repo(winApiMock); - repo.AddSettingsFromJsonFile("test/test.json"); - repo.UpdateSetting("test1", "notTestValue1"); - repo.WriteAllSettingsToFile(); + repository.ReloadSetting("setting4"); } - TEST(SettingRepository, WriteAllSettingsToFileHandlesMultipleFiles) + TEST_F(SettingRepositoryTest, ItReloadsSetting) { - StrictMock winApiMock; - - EXPECT_CALL(winApiMock, FileExists(std::wstring(L"settings/test/test1.json"))).Times(1).WillOnce(Return(true)); - - EXPECT_CALL(winApiMock, FileExists(std::wstring(L"settings/test/test2.json"))).Times(1).WillOnce(Return(true)); - - EXPECT_CALL(winApiMock, ReadFromFileMock(std::wstring(L"settings/test/test1.json"), true)) - .Times(1) - .WillOnce(Return("{\"test1\": \"testValue1\"}")); - - EXPECT_CALL(winApiMock, ReadFromFileMock(std::wstring(L"settings/test/test2.json"), true)) - .Times(1) - .WillOnce(Return("{\"test2\": \"testValue2\"}")); + EXPECT_CALL(*mockProvider1, Reload()).Times(0); - EXPECT_CALL( - winApiMock, - WriteToFile(std::wstring(L"settings/test/test1.json"), "{\n \"test1\": \"testValue1\"\n}", true, false)) - .Times(1); + EXPECT_CALL(*mockProvider2, Reload()).Times(1); - EXPECT_CALL( - winApiMock, - WriteToFile(std::wstring(L"settings/test/test2.json"), "{\n \"test2\": \"testValue2\"\n}", true, false)) - .Times(1); + repository.AddProvider(mockProvider1); + repository.AddProvider(mockProvider2); - SettingRepository repo(winApiMock); - repo.AddSettingsFromJsonFile("test/test1.json"); - repo.AddSettingsFromJsonFile("test/test2.json"); - repo.WriteAllSettingsToFile(); + repository.ReloadSetting("setting3"); } } // namespace UKControllerPluginUtilsTest::Setting diff --git a/test/utils/string/StringTrimFunctionTest.cpp b/test/utils/string/StringTrimFunctionTest.cpp new file mode 100644 index 000000000..600ac5f41 --- /dev/null +++ b/test/utils/string/StringTrimFunctionTest.cpp @@ -0,0 +1,71 @@ +#include "string/StringTrimFunctions.h" + +using UKControllerPluginUtils::String::ltrim; +using UKControllerPluginUtils::String::rtrim; +using UKControllerPluginUtils::String::trim; + +namespace UKControllerPluginUtilsTest::String { + class StringTrimFunctionsTest : public testing::Test + { + }; + + TEST_F(StringTrimFunctionsTest, TestLeftTrimRemovesCharactersFromLeftOfString) + { + EXPECT_EQ("abc", ltrim("xabc", "x")); + } + + TEST_F(StringTrimFunctionsTest, TestLeftTrimRemovesMultipleCharactersFromLeftOfString) + { + EXPECT_EQ("abc", ltrim("xyzabc", "xyz")); + } + + TEST_F(StringTrimFunctionsTest, TestLeftTrimDoesntRemoveFromMiddleOfString) + { + EXPECT_EQ("axbxc", ltrim("axbxc", "x")); + } + + TEST_F(StringTrimFunctionsTest, TestLeftTrimHasDefaultCharacters) + { + EXPECT_EQ("abc", ltrim("\n\r\f\v\tabc")); + } + + TEST_F(StringTrimFunctionsTest, TestRightTrimRemovesCharactersFromRightOfString) + { + EXPECT_EQ("abc", rtrim("abcx", "x")); + } + + TEST_F(StringTrimFunctionsTest, TestRightTrimRemovesMultipleCharactersFromRightOfString) + { + EXPECT_EQ("abc", rtrim("abcxyz", "xyz")); + } + + TEST_F(StringTrimFunctionsTest, TestRightTrimDoesntRemoveFromMiddleOfString) + { + EXPECT_EQ("axbxc", rtrim("axbxc", "x")); + } + + TEST_F(StringTrimFunctionsTest, TestRightTrimHasDefaultCharacters) + { + EXPECT_EQ("abc", rtrim("abc\n\r\f\v\t")); + } + + TEST_F(StringTrimFunctionsTest, TestTrimRemovesCharactersFromBothSidesOfString) + { + EXPECT_EQ("abc", trim("xabcx", "x")); + } + + TEST_F(StringTrimFunctionsTest, TestTrimRemovesMultipleCharactersFromBothSidesOfString) + { + EXPECT_EQ("abc", trim("xyzabcxyz", "xyz")); + } + + TEST_F(StringTrimFunctionsTest, TestTrimDoesntRemoveFromMiddleOfString) + { + EXPECT_EQ("axbxc", trim("axbxc", "x")); + } + + TEST_F(StringTrimFunctionsTest, TestTrimHasDefaultCharacters) + { + EXPECT_EQ("abc", trim("\n\r\f\v\tabc\n\r\f\v\t")); + } +} // namespace UKControllerPluginUtilsTest::String diff --git a/third_party/continuable b/third_party/continuable new file mode 160000 index 000000000..ed8310e34 --- /dev/null +++ b/third_party/continuable @@ -0,0 +1 @@ +Subproject commit ed8310e345b3c930c0810bc27f3e979a479da9b6 diff --git a/third_party/function2 b/third_party/function2 new file mode 160000 index 000000000..02ca99831 --- /dev/null +++ b/third_party/function2 @@ -0,0 +1 @@ +Subproject commit 02ca99831de59c7c3a4b834789260253cace0ced