SFSClientImplTests.cpp (16100B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 4 #include "../../util/SFSExceptionMatcher.h" 5 #include "../../util/TestHelper.h" 6 #include "SFSClientImpl.h" 7 #include "TestOverride.h" 8 #include "connection/Connection.h" 9 #include "connection/ConnectionManager.h" 10 #include "connection/CurlConnection.h" 11 #include "connection/CurlConnectionManager.h" 12 #include "connection/mock/MockConnectionManager.h" 13 14 #include <catch2/catch_test_macros.hpp> 15 #include <nlohmann/json.hpp> 16 17 #define TEST(...) TEST_CASE("[SFSClientImplTests] " __VA_ARGS__) 18 19 using namespace SFS; 20 using namespace SFS::details; 21 using namespace SFS::test; 22 using json = nlohmann::json; 23 24 namespace 25 { 26 class MockCurlConnection : public CurlConnection 27 { 28 public: 29 MockCurlConnection(const ReportingHandler& handler, 30 Result::Code& responseCode, 31 std::string& getResponse, 32 std::string& postResponse, 33 bool& expectEmptyPostBody) 34 : CurlConnection({}, handler) 35 , m_responseCode(responseCode) 36 , m_getResponse(getResponse) 37 , m_postResponse(postResponse) 38 , m_expectEmptyPostBody(expectEmptyPostBody) 39 { 40 } 41 42 std::string Get(const std::string&) override 43 { 44 if (m_responseCode == Result::Success) 45 { 46 INFO("MockCurlConnection::Get() called, response: " << m_getResponse); 47 return m_getResponse; 48 } 49 throw SFSException(m_responseCode); 50 } 51 52 std::string Post(const std::string&, const std::string& data) override 53 { 54 if (m_responseCode == Result::Success) 55 { 56 INFO("MockCurlConnection::Post() called, response: " << m_postResponse); 57 if (m_expectEmptyPostBody) 58 { 59 REQUIRE(data.empty()); 60 } 61 else 62 { 63 REQUIRE(!data.empty()); 64 } 65 return m_postResponse; 66 } 67 throw SFSException(m_responseCode); 68 } 69 70 private: 71 Result::Code& m_responseCode; 72 std::string& m_getResponse; 73 std::string& m_postResponse; 74 bool& m_expectEmptyPostBody; 75 }; 76 77 void CheckProduct(const VersionEntity& entity, std::string_view ns, std::string_view name, std::string_view version) 78 { 79 REQUIRE(entity.GetContentType() == ContentType::Generic); 80 REQUIRE(entity.contentId.nameSpace == ns); 81 REQUIRE(entity.contentId.name == name); 82 REQUIRE(entity.contentId.version == version); 83 } 84 85 void CheckDownloadInfo(const FileEntities& files, const std::string& name) 86 { 87 REQUIRE(files.size() == 2); 88 REQUIRE(files[0]->fileId == (name + ".json")); 89 REQUIRE(files[0]->url == ("http://localhost/1.json")); 90 REQUIRE(files[1]->fileId == (name + ".bin")); 91 REQUIRE(files[1]->url == ("http://localhost/2.bin")); 92 } 93 } // namespace 94 95 TEST("Testing class SFSClientImpl()") 96 { 97 const std::string ns = "testNameSpace"; 98 SFSClientImpl<CurlConnectionManager> sfsClient({"testAccountId", "testInstanceId", ns, LogCallbackToTest}); 99 100 Result::Code responseCode = Result::Success; 101 std::string getResponse; 102 std::string postResponse; 103 bool expectEmptyPostBody = true; 104 std::unique_ptr<Connection> connection = std::make_unique<MockCurlConnection>(sfsClient.GetReportingHandler(), 105 responseCode, 106 getResponse, 107 postResponse, 108 expectEmptyPostBody); 109 110 const std::string productName = "productName"; 111 const std::string expectedVersion = "0.0.0.2"; 112 113 SECTION("Testing SFSClientImpl::GetLatestVersion()") 114 { 115 expectEmptyPostBody = false; 116 std::unique_ptr<VersionEntity> entity; 117 118 SECTION("Expected response") 119 { 120 const json latestVersionResponse = { 121 {"ContentId", {{"Namespace", ns}, {"Name", productName}, {"Version", expectedVersion}}}}; 122 postResponse = latestVersionResponse.dump(); 123 SECTION("No attributes") 124 { 125 REQUIRE_NOTHROW(entity = sfsClient.GetLatestVersion({productName, {}}, *connection)); 126 REQUIRE(entity); 127 CheckProduct(*entity, ns, productName, expectedVersion); 128 } 129 130 SECTION("With attributes") 131 { 132 const TargetingAttributes attributes{{"attr1", "value"}}; 133 REQUIRE_NOTHROW(entity = sfsClient.GetLatestVersion({productName, attributes}, *connection)); 134 REQUIRE(entity); 135 CheckProduct(*entity, ns, productName, expectedVersion); 136 } 137 138 SECTION("Failing") 139 { 140 responseCode = Result::HttpNotFound; 141 REQUIRE_THROWS_CODE(entity = sfsClient.GetLatestVersion({"badName", {}}, *connection), HttpNotFound); 142 REQUIRE(!entity); 143 144 const TargetingAttributes attributes{{"attr1", "value"}}; 145 REQUIRE_THROWS_CODE(entity = sfsClient.GetLatestVersion({"badName", attributes}, *connection), 146 HttpNotFound); 147 REQUIRE(!entity); 148 } 149 } 150 151 SECTION("Unexpected response") 152 { 153 SECTION("Wrong ns") 154 { 155 const json latestVersionResponse = { 156 {"ContentId", {{"Namespace", "wrong"}, {"Name", productName}, {"Version", expectedVersion}}}}; 157 postResponse = latestVersionResponse.dump(); 158 } 159 160 SECTION("Wrong name") 161 { 162 const json latestVersionResponse = { 163 {"ContentId", {{"Namespace", ns}, {"Name", "wrong"}, {"Version", expectedVersion}}}}; 164 postResponse = latestVersionResponse.dump(); 165 } 166 167 REQUIRE_THROWS_CODE_MSG(entity = sfsClient.GetLatestVersion({productName, {}}, *connection), 168 ServiceInvalidResponse, 169 "Response does not match the requested product"); 170 REQUIRE(!entity); 171 } 172 } 173 174 SECTION("Testing SFSClientImpl::GetLatestVersionBatch()") 175 { 176 expectEmptyPostBody = false; 177 json latestVersionResponse = json::array(); 178 latestVersionResponse.push_back( 179 {{"ContentId", {{"Namespace", ns}, {"Name", productName}, {"Version", expectedVersion}}}}); 180 postResponse = latestVersionResponse.dump(); 181 VersionEntities entities; 182 SECTION("No attributes") 183 { 184 REQUIRE_NOTHROW(entities = sfsClient.GetLatestVersionBatch({{productName, {}}}, *connection)); 185 REQUIRE(!entities.empty()); 186 CheckProduct(*entities[0], ns, productName, expectedVersion); 187 } 188 189 SECTION("With attributes") 190 { 191 const TargetingAttributes attributes{{"attr1", "value"}}; 192 REQUIRE_NOTHROW(entities = sfsClient.GetLatestVersionBatch({{productName, attributes}}, *connection)); 193 REQUIRE(!entities.empty()); 194 CheckProduct(*entities[0], ns, productName, expectedVersion); 195 } 196 197 SECTION("Failing") 198 { 199 responseCode = Result::HttpNotFound; 200 REQUIRE_THROWS_CODE(entities = sfsClient.GetLatestVersionBatch({{"badName", {}}}, *connection), 201 HttpNotFound); 202 203 const TargetingAttributes attributes{{"attr1", "value"}}; 204 REQUIRE_THROWS_CODE(entities = sfsClient.GetLatestVersionBatch({{"badName", attributes}}, *connection), 205 HttpNotFound); 206 } 207 } 208 209 SECTION("Testing SFSClientImpl::GetSpecificVersion()") 210 { 211 json specificVersionResponse; 212 specificVersionResponse["ContentId"] = {{"Namespace", ns}, {"Name", productName}, {"Version", expectedVersion}}; 213 specificVersionResponse["Files"] = json::array({productName + ".json", productName + ".bin"}); 214 getResponse = specificVersionResponse.dump(); 215 std::unique_ptr<VersionEntity> entity; 216 SECTION("Getting version") 217 { 218 REQUIRE_NOTHROW(entity = sfsClient.GetSpecificVersion(productName, expectedVersion, *connection)); 219 REQUIRE(entity); 220 CheckProduct(*entity, ns, productName, expectedVersion); 221 } 222 223 SECTION("Failing") 224 { 225 responseCode = Result::HttpNotFound; 226 REQUIRE_THROWS_CODE(entity = sfsClient.GetSpecificVersion(productName, expectedVersion, *connection), 227 HttpNotFound); 228 } 229 } 230 231 SECTION("Testing SFSClientImpl::GetDownloadInfo()") 232 { 233 expectEmptyPostBody = true; 234 json downloadInfoResponse; 235 downloadInfoResponse = json::array(); 236 downloadInfoResponse.push_back({{"Url", "http://localhost/1.json"}, 237 {"FileId", productName + ".json"}, 238 {"SizeInBytes", 100}, 239 {"Hashes", {{"Sha1", "123"}, {"Sha256", "456"}}}}); 240 downloadInfoResponse[0]["DeliveryOptimization"] = {{"CatalogId", "789"}}; 241 downloadInfoResponse[0]["DeliveryOptimization"]["Properties"] = { 242 {"IntegrityCheckInfo", {{"PiecesHashFileUrl", "http://localhost/1.json"}, {"HashOfHashes", "abc"}}}}; 243 244 downloadInfoResponse.push_back({{"Url", "http://localhost/2.bin"}, 245 {"FileId", productName + ".bin"}, 246 {"SizeInBytes", 200}, 247 {"Hashes", {{"Sha1", "421"}, {"Sha256", "132"}}}}); 248 downloadInfoResponse[1]["DeliveryOptimization"] = downloadInfoResponse[0]["DeliveryOptimization"]; 249 postResponse = downloadInfoResponse.dump(); 250 251 FileEntities files; 252 SECTION("Getting version") 253 { 254 REQUIRE_NOTHROW(files = sfsClient.GetDownloadInfo(productName, expectedVersion, *connection)); 255 REQUIRE(!files.empty()); 256 CheckDownloadInfo(files, productName); 257 } 258 259 SECTION("Failing") 260 { 261 responseCode = Result::HttpNotFound; 262 REQUIRE_THROWS_CODE(files = sfsClient.GetDownloadInfo(productName, expectedVersion, *connection), 263 HttpNotFound); 264 REQUIRE(files.empty()); 265 } 266 } 267 } 268 269 TEST("Testing ClientConfig validation") 270 { 271 SECTION("accountId must not be empty") 272 { 273 const std::string expectedErrorMsg = "ClientConfig::accountId must not be empty"; 274 for (size_t i = 0; i <= 10; ++i) 275 { 276 ClientConfig config; 277 config.accountId = std::string(i, 'a'); 278 if (i >= 1) 279 { 280 REQUIRE_NOTHROW(SFSClientImpl<CurlConnectionManager>(std::move(config))); 281 } 282 else 283 { 284 REQUIRE_THROWS_CODE_MSG(SFSClientImpl<CurlConnectionManager>(std::move(config)), 285 InvalidArg, 286 expectedErrorMsg); 287 } 288 } 289 } 290 291 SECTION("instanceId must not be empty") 292 { 293 const std::string expectedErrorMsg = "ClientConfig::instanceId must not be empty"; 294 for (size_t i = 0; i <= 10; ++i) 295 { 296 ClientConfig config; 297 config.accountId = "testAccountId"; 298 config.instanceId = std::string(i, 'a'); 299 if (i >= 1) 300 { 301 REQUIRE_NOTHROW(SFSClientImpl<CurlConnectionManager>(std::move(config))); 302 } 303 else 304 { 305 REQUIRE_THROWS_CODE_MSG(SFSClientImpl<CurlConnectionManager>(std::move(config)), 306 InvalidArg, 307 expectedErrorMsg); 308 } 309 } 310 } 311 312 SECTION("NameSpace must not be empty") 313 { 314 const std::string expectedErrorMsg = "ClientConfig::nameSpace must not be empty"; 315 for (size_t i = 0; i <= 10; ++i) 316 { 317 ClientConfig config; 318 config.accountId = "testAccountId"; 319 config.nameSpace = std::string(i, 'a'); 320 if (i >= 1) 321 { 322 REQUIRE_NOTHROW(SFSClientImpl<CurlConnectionManager>(std::move(config))); 323 } 324 else 325 { 326 REQUIRE_THROWS_CODE_MSG(SFSClientImpl<CurlConnectionManager>(std::move(config)), 327 InvalidArg, 328 expectedErrorMsg); 329 } 330 } 331 } 332 } 333 334 TEST("Testing SFSClientImpl::SetCustomBaseUrl()") 335 { 336 ClientConfig config; 337 config.accountId = "testAccountId"; 338 SFSClientImpl<MockConnectionManager> sfsClient(std::move(config)); 339 340 REQUIRE(sfsClient.MakeUrlBuilder().GetUrl() == "https://testAccountId.api.cdp.microsoft.com/"); 341 342 sfsClient.SetCustomBaseUrl("customUrl"); 343 REQUIRE_THROWS_CODE_MSG(sfsClient.MakeUrlBuilder().GetUrl(), 344 ConnectionUrlSetupFailed, 345 "Curl URL error: Bad scheme"); 346 347 sfsClient.SetCustomBaseUrl("http://customUrl.com/"); 348 REQUIRE(sfsClient.MakeUrlBuilder().GetUrl() == "http://customUrl.com/"); 349 } 350 351 TEST("Testing test override SFS_TEST_OVERRIDE_BASE_URL") 352 { 353 ClientConfig config; 354 config.accountId = "testAccountId"; 355 SFSClientImpl<MockConnectionManager> sfsClient(std::move(config)); 356 357 REQUIRE(sfsClient.MakeUrlBuilder().GetUrl() == "https://testAccountId.api.cdp.microsoft.com/"); 358 359 { 360 INFO("Can override the base url with the test key"); 361 ScopedTestOverride override(TestOverride::BaseUrl, "http://override.com"); 362 if (AreTestOverridesAllowed()) 363 { 364 REQUIRE(sfsClient.MakeUrlBuilder().GetUrl() == "http://override.com/"); 365 } 366 else 367 { 368 REQUIRE(sfsClient.MakeUrlBuilder().GetUrl() == "https://testAccountId.api.cdp.microsoft.com/"); 369 } 370 } 371 372 if (AreTestOverridesAllowed()) 373 { 374 INFO("Fails if the override is not a valid URL"); 375 ScopedTestOverride override(TestOverride::BaseUrl, "override"); 376 { 377 REQUIRE_THROWS_CODE_MSG(sfsClient.MakeUrlBuilder().GetUrl(), 378 ConnectionUrlSetupFailed, 379 "Curl URL error: Bad scheme"); 380 } 381 } 382 383 INFO("Override is unset after ScopedEnv goes out of scope"); 384 REQUIRE(sfsClient.MakeUrlBuilder().GetUrl() == "https://testAccountId.api.cdp.microsoft.com/"); 385 386 sfsClient.SetCustomBaseUrl("http://customUrl.com"); 387 REQUIRE(sfsClient.MakeUrlBuilder().GetUrl() == "http://customUrl.com/"); 388 389 { 390 INFO("Can also override a custom base base url with the test key"); 391 ScopedTestOverride override(TestOverride::BaseUrl, "http://override.com"); 392 if (AreTestOverridesAllowed()) 393 { 394 REQUIRE(sfsClient.MakeUrlBuilder().GetUrl() == "http://override.com/"); 395 } 396 else 397 { 398 REQUIRE(sfsClient.MakeUrlBuilder().GetUrl() == "http://customUrl.com/"); 399 } 400 } 401 402 REQUIRE(sfsClient.MakeUrlBuilder().GetUrl() == "http://customUrl.com/"); 403 } 404 405 TEST("Testing passing a logging callback to constructor of SFSClientImpl") 406 { 407 SFSClientImpl<MockConnectionManager> sfsClient( 408 {"testAccountId", "testInstanceId", "testNameSpace", [](const LogData&) {}}); 409 SFSClientImpl<MockConnectionManager> sfsClient2({"testAccountId", "testInstanceId", "testNameSpace", nullptr}); 410 }