winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

MockWebServer.cpp (25565B)


      1 // Copyright (c) Microsoft Corporation.
      2 // Licensed under the MIT License.
      3 
      4 #include "MockWebServer.h"
      5 
      6 #include "../util/TestHelper.h"
      7 #include "ErrorHandling.h"
      8 #include "ServerCommon.h"
      9 #include "Util.h"
     10 #include "connection/HttpHeader.h"
     11 
     12 #ifdef _WIN32
     13 #define WIN32_LEAN_AND_MEAN
     14 #include <windows.h>
     15 #endif
     16 
     17 #include <nlohmann/json.hpp>
     18 
     19 #include <chrono>
     20 #include <mutex>
     21 #include <optional>
     22 #include <thread>
     23 #include <unordered_map>
     24 
     25 using namespace SFS;
     26 using namespace SFS::details;
     27 using namespace SFS::details::util;
     28 using namespace SFS::test;
     29 using namespace SFS::test::details;
     30 using json = nlohmann::json;
     31 
     32 namespace
     33 {
     34 
     35 struct App
     36 {
     37     std::string version;
     38     std::vector<MockPrerequisite> prerequisites;
     39 };
     40 
     41 struct AppCmp
     42 {
     43     bool operator()(App a, App b) const
     44     {
     45         return a.version < b.version;
     46     }
     47 };
     48 
     49 json GenerateContentIdJsonObject(const std::string& name, const std::string& latestVersion, const std::string& ns)
     50 {
     51     // {
     52     //   "ContentId": {
     53     //     "Namespace": <ns>,
     54     //     "Name": <name>,
     55     //     "Version": <version>
     56     //   }
     57     // }
     58 
     59     return {{"ContentId", {{"Namespace", ns}, {"Name", name}, {"Version", latestVersion}}}};
     60 }
     61 
     62 json GenerateGetAppVersionJsonObject(const std::string& name, const App& app, const std::string& ns)
     63 {
     64     // {
     65     //   "ContentId": {
     66     //     "Namespace": <ns>,
     67     //     "Name": <name>,
     68     //     "Version": <version>
     69     //   },
     70     //   "UpdateId": "<id>",
     71     //   "Prerequisites": [
     72     //     {
     73     //       "Namespace": "<ns>",
     74     //       "Name": "<name>",
     75     //       "Version": "<version>"
     76     //     }
     77     //   ]
     78     // }
     79 
     80     json prereqs = json::array();
     81     for (const auto& prereq : app.prerequisites)
     82     {
     83         prereqs.push_back({{"Namespace", ns}, {"Name", prereq.name}, {"Version", prereq.version}});
     84     }
     85     return {{"ContentId", {{"Namespace", ns}, {"Name", name}, {"Version", app.version}}},
     86             {"UpdateId", "123"},
     87             {"Prerequisites", prereqs}};
     88 }
     89 
     90 json GeneratePostDownloadInfo(const std::string& name)
     91 {
     92     // [
     93     //   {
     94     //     "Url": <url>,
     95     //     "FileId": <fileid>,
     96     //     "SizeInBytes": <size>,
     97     //     "Hashes": {
     98     //       "Sha1": <sha1>,
     99     //       "Sha256": <sha2>
    100     //     },
    101     //     "DeliveryOptimization": {
    102     //       "CatalogId": <catalogid>,
    103     //       "Properties": {
    104     //         "IntegrityCheckInfo": {
    105     //           "PiecesHashFileUrl": <url>,
    106     //           "HashOfHashes": <hash>
    107     //         }
    108     //       }
    109     //     }
    110     //   },
    111     //   ...
    112     // ]
    113 
    114     // Generating DeliveryOptimizationData to simulate the server response, but it's not being parsed by the Client
    115 
    116     json response;
    117     response = json::array();
    118     response.push_back({{"Url", "http://localhost/1.json"},
    119                         {"FileId", name + ".json"},
    120                         {"SizeInBytes", 100},
    121                         {"Hashes", {{"Sha1", "123"}, {"Sha256", "456"}}}});
    122     response[0]["DeliveryOptimization"] = {{"CatalogId", "789"}};
    123     response[0]["DeliveryOptimization"]["Properties"] = {
    124         {"IntegrityCheckInfo", {{"PiecesHashFileUrl", "http://localhost/1.json"}, {"HashOfHashes", "abc"}}}};
    125 
    126     response.push_back({{"Url", "http://localhost/2.bin"},
    127                         {"FileId", name + ".bin"},
    128                         {"SizeInBytes", 200},
    129                         {"Hashes", {{"Sha1", "421"}, {"Sha256", "132"}}}});
    130     response[1]["DeliveryOptimization"] = {{"CatalogId", "14"}};
    131     response[1]["DeliveryOptimization"]["Properties"] = {
    132         {"IntegrityCheckInfo", {{"PiecesHashFileUrl", "http://localhost/2.bin"}, {"HashOfHashes", "abcd"}}}};
    133     return response;
    134 }
    135 
    136 json GeneratePostAppDownloadInfo(const std::string& name)
    137 {
    138     // [
    139     //   {
    140     //     "Url": <url>,
    141     //     "FileId": <fileid>,
    142     //     "SizeInBytes": <size>,
    143     //     "Hashes": {
    144     //       "Sha1": <sha1>,
    145     //       "Sha256": <sha2>
    146     //     },
    147     //     "DeliveryOptimization": {
    148     //       "CatalogId": <catalogid>,
    149     //       "Properties": {
    150     //         "IntegrityCheckInfo": {
    151     //           "PiecesHashFileUrl": <url>,
    152     //           "HashOfHashes": <hash>
    153     //         }
    154     //       }
    155     //     },
    156     //     "ApplicabilityDetails": {
    157     //       "Architectures": [
    158     //         "<arch>"
    159     //       ],
    160     //       "PlatformApplicabilityForPackage": [
    161     //         "<app>"
    162     //       ]
    163     //     },
    164     //     "FileMoniker": "<moniker>"
    165     //   }
    166     // ]
    167 
    168     // Generating DeliveryOptimizationData to simulate the server response, but it's not being parsed by the Client
    169 
    170     json response;
    171     response = json::array();
    172     response.push_back({{"Url", "http://localhost/1.json"},
    173                         {"FileId", name + ".json"},
    174                         {"SizeInBytes", 100},
    175                         {"Hashes", {{"Sha1", "123"}, {"Sha256", "456"}}}});
    176     response[0]["DeliveryOptimization"] = {{"CatalogId", "789"}};
    177     response[0]["DeliveryOptimization"]["Properties"] = {
    178         {"IntegrityCheckInfo", {{"PiecesHashFileUrl", "http://localhost/1.json"}, {"HashOfHashes", "abc"}}}};
    179     response[0]["ApplicabilityDetails"] = {{"Architectures", {"x86"}},
    180                                            {"PlatformApplicabilityForPackage", {"Windows"}}};
    181     response[0]["FileMoniker"] = "1.json";
    182 
    183     response.push_back({{"Url", "http://localhost/2.bin"},
    184                         {"FileId", name + ".bin"},
    185                         {"SizeInBytes", 200},
    186                         {"Hashes", {{"Sha1", "421"}, {"Sha256", "132"}}}});
    187     response[1]["DeliveryOptimization"] = {{"CatalogId", "14"}};
    188     response[1]["DeliveryOptimization"]["Properties"] = {
    189         {"IntegrityCheckInfo", {{"PiecesHashFileUrl", "http://localhost/2.bin"}, {"HashOfHashes", "abcd"}}}};
    190     response[1]["ApplicabilityDetails"] = {{"Architectures", {"amd64"}},
    191                                            {"PlatformApplicabilityForPackage", {"Linux"}}};
    192     response[1]["FileMoniker"] = "2.bin";
    193 
    194     return response;
    195 }
    196 
    197 void CheckApiVersion(const httplib::Request& req, std::string_view apiVersion)
    198 {
    199     if (util::AreNotEqualI(req.path_params.at("apiVersion"), apiVersion))
    200     {
    201         throw StatusCodeException(httplib::StatusCode::NotFound_404);
    202     }
    203 }
    204 } // namespace
    205 
    206 namespace SFS::test::details
    207 {
    208 class MockWebServerImpl : public BaseServerImpl
    209 {
    210   public:
    211     MockWebServerImpl() = default;
    212     ~MockWebServerImpl() = default;
    213 
    214     MockWebServerImpl(const MockWebServerImpl&) = delete;
    215     MockWebServerImpl& operator=(const MockWebServerImpl&) = delete;
    216 
    217     void RegisterProduct(std::string&& name, std::string&& version);
    218     void RegisterAppProduct(std::string&& name, std::string&& version, std::vector<MockPrerequisite>&& prerequisites);
    219     void RegisterExpectedRequestHeader(std::string&& header, std::string&& value);
    220     void SetForcedHttpErrors(std::queue<HttpCode> forcedErrors);
    221     void SetResponseHeaders(std::unordered_map<HttpCode, HeaderMap> headersByCode);
    222 
    223   private:
    224     void ConfigureRequestHandlers() override;
    225     std::string GetLogIdentifier() override;
    226 
    227     void ConfigurePostLatestVersion();
    228     void ConfigurePostLatestVersionBatch();
    229     void ConfigureGetSpecificVersion();
    230     void ConfigurePostDownloadInfo();
    231 
    232     void RunHttpCallback(const httplib::Request& req,
    233                          httplib::Response& res,
    234                          const std::string& methodName,
    235                          const std::string& apiVersion,
    236                          const std::function<void(const httplib::Request, httplib::Response&)>& callback);
    237     void CheckRequestHeaders(const httplib::Request& req);
    238 
    239     using VersionList = std::set<std::string>;
    240     std::unordered_map<std::string, VersionList> m_products;
    241 
    242     using AppList = std::set<App, AppCmp>;
    243     std::unordered_map<std::string, AppList> m_appProducts;
    244 
    245     std::unordered_map<std::string, std::string> m_expectedRequestHeaders;
    246     std::queue<HttpCode> m_forcedHttpErrors;
    247     std::unordered_map<HttpCode, HeaderMap> m_headersByCode;
    248 };
    249 } // namespace SFS::test::details
    250 
    251 MockWebServer::MockWebServer()
    252 {
    253     m_impl = std::make_unique<MockWebServerImpl>();
    254     m_impl->Start();
    255 }
    256 
    257 MockWebServer::~MockWebServer()
    258 {
    259     const auto ret = Stop();
    260     if (!ret)
    261     {
    262         TEST_UNSCOPED_INFO("Failed to stop: " + std::string(ToString(ret.GetCode())));
    263     }
    264 }
    265 
    266 Result MockWebServer::Stop()
    267 {
    268     return m_impl->Stop();
    269 }
    270 
    271 std::string MockWebServer::GetBaseUrl() const
    272 {
    273     return m_impl->GetUrl();
    274 }
    275 
    276 void MockWebServer::RegisterProduct(std::string name, std::string version)
    277 {
    278     m_impl->RegisterProduct(std::move(name), std::move(version));
    279 }
    280 
    281 void MockWebServer::RegisterAppProduct(std::string name,
    282                                        std::string version,
    283                                        std::vector<MockPrerequisite> prerequisites)
    284 {
    285     m_impl->RegisterAppProduct(std::move(name), std::move(version), std::move(prerequisites));
    286 }
    287 
    288 void MockWebServer::RegisterExpectedRequestHeader(HttpHeader header, std::string value)
    289 {
    290     std::string headerName = ToString(header);
    291     m_impl->RegisterExpectedRequestHeader(std::move(headerName), std::move(value));
    292 }
    293 
    294 void MockWebServer::SetForcedHttpErrors(std::queue<HttpCode> forcedErrors)
    295 {
    296     m_impl->SetForcedHttpErrors(std::move(forcedErrors));
    297 }
    298 
    299 void MockWebServer::SetResponseHeaders(std::unordered_map<HttpCode, HeaderMap> headersByCode)
    300 {
    301     m_impl->SetResponseHeaders(std::move(headersByCode));
    302 }
    303 
    304 void MockWebServerImpl::ConfigureRequestHandlers()
    305 {
    306     ConfigurePostLatestVersion();
    307     ConfigurePostLatestVersionBatch();
    308     ConfigureGetSpecificVersion();
    309     ConfigurePostDownloadInfo();
    310 }
    311 
    312 std::string MockWebServerImpl::GetLogIdentifier()
    313 {
    314     return "MockWebServer";
    315 }
    316 
    317 void MockWebServerImpl::ConfigurePostLatestVersion()
    318 {
    319     // Path: /api/<apiVersion:v2>/contents/<instanceId>/namespaces/<ns>/names/<name>/versions/latest?action=select
    320     const std::string pattern = "/api/:apiVersion/contents/:instanceId/namespaces/:ns/names/:name/versions/latest";
    321     m_server.Post(pattern, [&](const httplib::Request& req, httplib::Response& res) {
    322         RunHttpCallback(req, res, "PostLatestVersion", "v2", [&](const httplib::Request& req, httplib::Response& res) {
    323             // TODO: Ignoring instanceId for now
    324 
    325             if (!req.has_param("action") || util::AreNotEqualI(req.get_param_value("action"), "select"))
    326             {
    327                 // TODO: SFS might throw a different error when the query string is unexpected
    328                 throw StatusCodeException(httplib::StatusCode::NotFound_404);
    329             }
    330 
    331             // Checking body has expected format, but won't use it for the response
    332             {
    333                 if (req.body.empty())
    334                 {
    335                     throw StatusCodeException(httplib::StatusCode::BadRequest_400);
    336                 }
    337 
    338                 json body;
    339                 try
    340                 {
    341                     body = json::parse(req.body);
    342                 }
    343                 catch (const json::parse_error& ex)
    344                 {
    345                     BUFFER_LOG("JSON parse error: " + std::string(ex.what()));
    346                     throw StatusCodeException(httplib::StatusCode::BadRequest_400);
    347                 }
    348 
    349                 // The GetLatestVersion API expects an object as a body, with a "TargetingAttributes" object element.
    350                 if (!body.is_object() || !body.contains("TargetingAttributes") ||
    351                     !body["TargetingAttributes"].is_object())
    352                 {
    353                     throw StatusCodeException(httplib::StatusCode::BadRequest_400);
    354                 }
    355             }
    356 
    357             const std::string ns = req.path_params.at("ns");
    358 
    359             json response;
    360             const std::string& name = req.path_params.at("name");
    361             if (auto it = m_products.find(name); it != m_products.end())
    362             {
    363                 const auto& versions = it->second;
    364                 if (versions.empty())
    365                 {
    366                     throw StatusCodeException(httplib::StatusCode::InternalServerError_500);
    367                 }
    368 
    369                 const auto& latestVersion = *versions.rbegin();
    370                 response = GenerateContentIdJsonObject(name, latestVersion, ns);
    371             }
    372             else if (auto appIt = m_appProducts.find(name); appIt != m_appProducts.end())
    373             {
    374                 const auto& appList = appIt->second;
    375                 if (appList.empty())
    376                 {
    377                     throw StatusCodeException(httplib::StatusCode::InternalServerError_500);
    378                 }
    379 
    380                 const auto& latestApp = *appList.rbegin();
    381                 response = GenerateGetAppVersionJsonObject(name, latestApp, ns);
    382             }
    383             else
    384             {
    385                 throw StatusCodeException(httplib::StatusCode::NotFound_404);
    386             }
    387 
    388             res.set_content(response.dump(), "application/json");
    389         });
    390     });
    391 }
    392 
    393 void MockWebServerImpl::ConfigurePostLatestVersionBatch()
    394 {
    395     // Path: /api/<apiVersion:v2>/contents/<instanceId>/namespaces/<ns>/names?action=BatchUpdates
    396     const std::string pattern = "/api/:apiVersion/contents/:instanceId/namespaces/:ns/names";
    397     m_server.Post(pattern, [&](const httplib::Request& req, httplib::Response& res) {
    398         RunHttpCallback(
    399             req,
    400             res,
    401             "PostLatestVersionBatch",
    402             "v2",
    403             [&](const httplib::Request& req, httplib::Response& res) {
    404                 // TODO: Ignoring instanceId for now
    405 
    406                 if (!req.has_param("action") || util::AreNotEqualI(req.get_param_value("action"), "BatchUpdates"))
    407                 {
    408                     // TODO: SFS might throw a different error when the query string is unexpected
    409                     throw StatusCodeException(httplib::StatusCode::NotFound_404);
    410                 }
    411 
    412                 if (req.body.empty())
    413                 {
    414                     throw StatusCodeException(httplib::StatusCode::BadRequest_400);
    415                 }
    416 
    417                 json body;
    418                 try
    419                 {
    420                     body = json::parse(req.body);
    421                 }
    422                 catch (const json::parse_error& ex)
    423                 {
    424                     BUFFER_LOG("JSON parse error: " + std::string(ex.what()));
    425                     throw StatusCodeException(httplib::StatusCode::BadRequest_400);
    426                 }
    427 
    428                 // The BatchUpdates API returns an array of objects, each with a "Product" key.
    429                 // If repeated, the same product is only returned once.
    430                 // TODO: We are ignoring the TargetingAttributes for now.
    431                 if (!body.is_array())
    432                 {
    433                     throw StatusCodeException(httplib::StatusCode::BadRequest_400);
    434                 }
    435 
    436                 // Iterate over the array and collect the unique products
    437                 std::unordered_map<std::string, json> requestedProducts;
    438                 for (const auto& productRequest : body)
    439                 {
    440                     if (!productRequest.is_object() || !productRequest.contains("Product") ||
    441                         !productRequest["Product"].is_string() || !productRequest.contains("TargetingAttributes"))
    442                     {
    443                         throw StatusCodeException(httplib::StatusCode::BadRequest_400);
    444                     }
    445                     if (requestedProducts.count(productRequest["Product"]))
    446                     {
    447                         continue;
    448                     }
    449                     requestedProducts.emplace(productRequest["Product"], productRequest["TargetingAttributes"]);
    450                 }
    451 
    452                 // If at least one product exists, we will return a 200 OK with that. Non-existing products are ignored.
    453                 // Otherwise, a 404 is sent.
    454                 json response = json::array();
    455                 for (const auto& [name, _] : requestedProducts)
    456                 {
    457                     auto it = m_products.find(name);
    458                     if (it == m_products.end())
    459                     {
    460                         continue;
    461                     }
    462 
    463                     const VersionList& versions = it->second;
    464                     if (versions.empty())
    465                     {
    466                         throw StatusCodeException(httplib::StatusCode::InternalServerError_500);
    467                     }
    468 
    469                     const std::string ns = req.path_params.at("ns");
    470                     const auto& latestVersion = *versions.rbegin();
    471 
    472                     response.push_back(GenerateContentIdJsonObject(name, latestVersion, ns));
    473                 }
    474 
    475                 if (response.empty())
    476                 {
    477                     throw StatusCodeException(httplib::StatusCode::NotFound_404);
    478                 }
    479 
    480                 res.set_content(response.dump(), "application/json");
    481             });
    482     });
    483 }
    484 
    485 void MockWebServerImpl::ConfigureGetSpecificVersion()
    486 {
    487     // Path: /api/<apiVersion:v2>/contents/<instanceId>/namespaces/<ns>/names/<name>/versions/<version>
    488     const std::string pattern = "/api/:apiVersion/contents/:instanceId/namespaces/:ns/names/:name/versions/:version";
    489     m_server.Get(pattern, [&](const httplib::Request& req, httplib::Response& res) {
    490         RunHttpCallback(req, res, "GetSpecificVersion", "v2", [&](const httplib::Request& req, httplib::Response& res) {
    491             // TODO: Ignoring instanceId for now
    492 
    493             const std::string& name = req.path_params.at("name");
    494             auto it = m_products.find(name);
    495             if (it == m_products.end())
    496             {
    497                 throw StatusCodeException(httplib::StatusCode::NotFound_404);
    498             }
    499 
    500             const VersionList& versions = it->second;
    501             if (versions.empty())
    502             {
    503                 throw StatusCodeException(httplib::StatusCode::InternalServerError_500);
    504             }
    505 
    506             // TODO: Are apps suppported?
    507 
    508             const std::string& version = req.path_params.at("version");
    509             if (version.empty() || !versions.count(version))
    510             {
    511                 throw StatusCodeException(httplib::StatusCode::NotFound_404);
    512             }
    513 
    514             const std::string ns = req.path_params.at("ns");
    515 
    516             res.set_content(GenerateContentIdJsonObject(name, version, ns).dump(), "application/json");
    517         });
    518     });
    519 }
    520 
    521 void MockWebServerImpl::ConfigurePostDownloadInfo()
    522 {
    523     // Path:
    524     // /api/<apiVersion:v2>/contents/<instanceId>/namespaces/<ns>/names/<name>/versions/<version>/files?action=GenerateDownloadInfo
    525     const std::string pattern =
    526         "/api/:apiVersion/contents/:instanceId/namespaces/:ns/names/:name/versions/:version/files";
    527     m_server.Post(pattern, [&](const httplib::Request& req, httplib::Response& res) {
    528         RunHttpCallback(req, res, "PostDownloadInfo", "v2", [&](const httplib::Request& req, httplib::Response& res) {
    529             // TODO: Ignoring instanceId and ns for now
    530 
    531             if (!req.has_param("action") || util::AreNotEqualI(req.get_param_value("action"), "GenerateDownloadInfo"))
    532             {
    533                 // TODO: SFS might throw a different error when the query string is unexpected
    534                 throw StatusCodeException(httplib::StatusCode::NotFound_404);
    535             }
    536 
    537             const std::string& version = req.path_params.at("version");
    538             if (version.empty())
    539             {
    540                 throw StatusCodeException(httplib::StatusCode::NotFound_404);
    541             }
    542 
    543             json response;
    544             const std::string& name = req.path_params.at("name");
    545             if (auto it = m_products.find(name); it != m_products.end())
    546             {
    547                 const auto& versions = it->second;
    548                 if (versions.empty())
    549                 {
    550                     throw StatusCodeException(httplib::StatusCode::InternalServerError_500);
    551                 }
    552 
    553                 if (!versions.count(version))
    554                 {
    555                     throw StatusCodeException(httplib::StatusCode::NotFound_404);
    556                 }
    557 
    558                 // Response is a dummy, doesn't use the version above
    559                 response = GeneratePostDownloadInfo(name);
    560             }
    561             else if (auto appIt = m_appProducts.find(name); appIt != m_appProducts.end())
    562             {
    563                 const auto& appList = appIt->second;
    564                 if (appList.empty())
    565                 {
    566                     throw StatusCodeException(httplib::StatusCode::InternalServerError_500);
    567                 }
    568 
    569                 auto app = std::find_if(appList.begin(), appList.end(), [&](const App& app) {
    570                     return app.version == version;
    571                 });
    572                 if (app == appList.end())
    573                 {
    574                     throw StatusCodeException(httplib::StatusCode::NotFound_404);
    575                 }
    576 
    577                 // Response is a dummy, doesn't use the version above
    578                 response = GeneratePostAppDownloadInfo(name);
    579             }
    580             else
    581             {
    582                 throw StatusCodeException(httplib::StatusCode::NotFound_404);
    583             }
    584 
    585             res.set_content(response.dump(), "application/json");
    586         });
    587     });
    588 }
    589 
    590 void MockWebServerImpl::RunHttpCallback(const httplib::Request& req,
    591                                         httplib::Response& res,
    592                                         const std::string& methodName,
    593                                         const std::string& apiVersion,
    594                                         const std::function<void(const httplib::Request, httplib::Response&)>& callback)
    595 {
    596     if (m_forcedHttpErrors.size() > 0)
    597     {
    598         res.status = m_forcedHttpErrors.front();
    599         m_forcedHttpErrors.pop();
    600 
    601         BUFFER_LOG("Forcing HTTP error: " + std::to_string(res.status));
    602     }
    603     else
    604     {
    605         try
    606         {
    607             BUFFER_LOG("Matched " + methodName);
    608             CheckApiVersion(req, apiVersion);
    609             CheckRequestHeaders(req);
    610             callback(req, res);
    611             res.status = httplib::StatusCode::OK_200;
    612         }
    613         catch (const StatusCodeException& ex)
    614         {
    615             res.status = ex.GetStatusCode();
    616         }
    617         catch (const std::exception&)
    618         {
    619             res.status = httplib::StatusCode::InternalServerError_500;
    620         }
    621         catch (...)
    622         {
    623             res.status = httplib::StatusCode::InternalServerError_500;
    624         }
    625     }
    626 
    627     if (m_headersByCode.count(res.status) > 0)
    628     {
    629         BUFFER_LOG("HTTP code " + std::to_string(res.status) + " has response headers to be sent");
    630         for (const auto& header : m_headersByCode[res.status])
    631         {
    632             BUFFER_LOG("Adding header [" + header.first + "] with value [" + header.second + "]");
    633             res.set_header(header.first, header.second);
    634         }
    635     }
    636 }
    637 
    638 void MockWebServerImpl::CheckRequestHeaders(const httplib::Request& req)
    639 {
    640     for (const auto& header : m_expectedRequestHeaders)
    641     {
    642         std::optional<std::string> errorMessage;
    643         if (!req.has_header(header.first))
    644         {
    645             errorMessage = "Expected header [" + header.first + "] not found";
    646         }
    647         else if (util::AreNotEqualI(req.get_header_value(header.first), header.second))
    648         {
    649             errorMessage = "Header [" + header.first + "] with value [" + req.get_header_value(header.first) +
    650                            "] does not match the expected value [" + header.second + "]";
    651         }
    652 
    653         if (errorMessage)
    654         {
    655             BUFFER_LOG(*errorMessage);
    656             throw std::runtime_error(errorMessage->c_str());
    657         }
    658     }
    659 }
    660 
    661 void MockWebServerImpl::RegisterProduct(std::string&& name, std::string&& version)
    662 {
    663     m_products[std::move(name)].emplace(std::move(version));
    664 }
    665 
    666 void MockWebServerImpl::RegisterAppProduct(std::string&& name,
    667                                            std::string&& version,
    668                                            std::vector<MockPrerequisite>&& prerequisites)
    669 {
    670     for (const auto& prereq : prerequisites)
    671     {
    672         m_appProducts[prereq.name].emplace(App{prereq.version, {}});
    673     }
    674     m_appProducts[std::move(name)].emplace(App{std::move(version), std::move(prerequisites)});
    675 }
    676 
    677 void MockWebServerImpl::RegisterExpectedRequestHeader(std::string&& header, std::string&& value)
    678 {
    679     if (auto it = m_expectedRequestHeaders.find(header); it != m_expectedRequestHeaders.end())
    680     {
    681         it->second = std::move(value);
    682         return;
    683     }
    684     else
    685     {
    686         m_expectedRequestHeaders.emplace(std::move(header), std::move(value));
    687     }
    688 }
    689 
    690 void MockWebServerImpl::SetForcedHttpErrors(std::queue<int> forcedErrors)
    691 {
    692     m_forcedHttpErrors = std::move(forcedErrors);
    693 }
    694 
    695 void MockWebServerImpl::SetResponseHeaders(std::unordered_map<HttpCode, HeaderMap> headersByCode)
    696 {
    697     m_headersByCode = std::move(headersByCode);
    698 }