PackageCollection.cpp (21762B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #include "pch.h" 4 #include "TestCommon.h" 5 6 #include <winget/Regex.h> 7 #include <PackageCollection.h> 8 9 // Duplicating here because a change to these values in the product *REALLY* needs to be thought through. 10 using namespace std::string_literals; 11 using namespace std::string_view_literals; 12 13 using namespace AppInstaller::CLI; 14 using namespace AppInstaller::Repository; 15 using namespace AppInstaller::Utility; 16 17 const std::string s_PackagesJson_Schema = "$schema"; 18 const std::string s_PackagesJson_SchemaUri_v2_0 = "https://aka.ms/winget-packages.schema.2.0.json"; 19 const std::string s_PackagesJson_WinGetVersion = "WinGetVersion"; 20 const std::string s_PackagesJson_CreationDate = "CreationDate"; 21 22 const std::string s_PackagesJson_Sources = "Sources"; 23 const std::string s_PackagesJson_Source_Details = "SourceDetails"; 24 const std::string s_PackagesJson_Source_Name = "Name"; 25 const std::string s_PackagesJson_Source_Identifier = "Identifier"; 26 const std::string s_PackagesJson_Source_Argument = "Argument"; 27 const std::string s_PackagesJson_Source_Type = "Type"; 28 29 const std::string s_PackagesJson_Packages = "Packages"; 30 const std::string s_PackagesJson_Package_PackageIdentifier = "PackageIdentifier"; 31 const std::string s_PackagesJson_Package_Version = "Version"; 32 const std::string s_PackagesJson_Package_Channel = "Channel"; 33 34 namespace 35 { 36 37 Json::Value ParseJsonString(const std::string& jsonString) 38 { 39 Json::Value root; 40 std::stringstream{ jsonString } >> root; 41 return root; 42 } 43 44 void ValidateJsonStringProperty(const Json::Value& node, const std::string& propertyName, std::string_view expectedValue, bool allowMissing = false) 45 { 46 if (allowMissing && expectedValue.empty() && !node.isMember(propertyName)) 47 { 48 return; 49 } 50 51 REQUIRE(node.isMember(propertyName)); 52 REQUIRE(node[propertyName].isString()); 53 if (!expectedValue.empty()) 54 { 55 REQUIRE(node[propertyName].asString() == expectedValue); 56 } 57 } 58 59 const Json::Value& GetAndValidateJsonProperty(const Json::Value& node, const std::string& propertyName, Json::ValueType valueType) 60 { 61 REQUIRE(node.isMember(propertyName)); 62 REQUIRE(node[propertyName].type() == valueType); 63 return node[propertyName]; 64 } 65 66 void ValidateJsonWithCollection(const Json::Value& root, const PackageCollection& collection) 67 { 68 ValidateJsonStringProperty(root, s_PackagesJson_Schema, s_PackagesJson_SchemaUri_v2_0); 69 ValidateJsonStringProperty(root, s_PackagesJson_WinGetVersion, collection.ClientVersion); 70 // valijson now validates the date-time format, just check the string property exists. 71 ValidateJsonStringProperty(root, s_PackagesJson_CreationDate, ""); 72 73 const auto& jsonSources = GetAndValidateJsonProperty(root, s_PackagesJson_Sources, Json::ValueType::arrayValue); 74 REQUIRE(jsonSources.size() == collection.Sources.size()); 75 76 // Expect the order to be the same. Not really needed, but it makes things easier. 77 auto jsonSourceItr = jsonSources.begin(); 78 auto sourceItr = collection.Sources.begin(); 79 for (; jsonSourceItr != jsonSources.end(); ++jsonSourceItr, ++sourceItr) 80 { 81 REQUIRE(jsonSourceItr->isObject()); 82 const auto& jsonSourceDetails = GetAndValidateJsonProperty(*jsonSourceItr, s_PackagesJson_Source_Details, Json::ValueType::objectValue); 83 ValidateJsonStringProperty(jsonSourceDetails, s_PackagesJson_Source_Name, sourceItr->Details.Name); 84 ValidateJsonStringProperty(jsonSourceDetails, s_PackagesJson_Source_Argument, sourceItr->Details.Arg); 85 ValidateJsonStringProperty(jsonSourceDetails, s_PackagesJson_Source_Type, sourceItr->Details.Type); 86 ValidateJsonStringProperty(jsonSourceDetails, s_PackagesJson_Source_Identifier, sourceItr->Details.Identifier); 87 88 const auto& jsonPackages = GetAndValidateJsonProperty(*jsonSourceItr, s_PackagesJson_Packages, Json::ValueType::arrayValue); 89 REQUIRE(jsonPackages.size() == sourceItr->Packages.size()); 90 91 auto jsonPackageItr = jsonPackages.begin(); 92 auto packageItr = sourceItr->Packages.begin(); 93 for (; jsonPackageItr != jsonPackages.end(); ++jsonPackageItr, ++packageItr) 94 { 95 REQUIRE(jsonPackageItr->isObject()); 96 ValidateJsonStringProperty(*jsonPackageItr, s_PackagesJson_Package_PackageIdentifier, packageItr->Id); 97 ValidateJsonStringProperty(*jsonPackageItr, s_PackagesJson_Package_Version, packageItr->VersionAndChannel.GetVersion().ToString(), true); 98 ValidateJsonStringProperty(*jsonPackageItr, s_PackagesJson_Package_Channel, packageItr->VersionAndChannel.GetChannel().ToString(), true); 99 } 100 } 101 } 102 103 void ValidateEqualCollections(const PackageCollection& first, const PackageCollection& second) 104 { 105 REQUIRE(first.ClientVersion == second.ClientVersion); 106 REQUIRE(first.Sources.size() == second.Sources.size()); 107 108 auto firstSourceItr = first.Sources.begin(); 109 auto secondSourceItr = second.Sources.begin(); 110 for (; firstSourceItr != first.Sources.end(); ++firstSourceItr, ++secondSourceItr) 111 { 112 REQUIRE(firstSourceItr->Details.Name == secondSourceItr->Details.Name); 113 REQUIRE(firstSourceItr->Details.Arg == secondSourceItr->Details.Arg); 114 REQUIRE(firstSourceItr->Details.Type == secondSourceItr->Details.Type); 115 REQUIRE(firstSourceItr->Details.Identifier == secondSourceItr->Details.Identifier); 116 117 REQUIRE(firstSourceItr->Packages.size() == secondSourceItr->Packages.size()); 118 auto firstPackageItr = firstSourceItr->Packages.begin(); 119 auto secondPackageItr = secondSourceItr->Packages.begin(); 120 for (; firstPackageItr != firstSourceItr->Packages.end(); ++firstPackageItr, ++secondPackageItr) 121 { 122 REQUIRE(firstPackageItr->Id == secondPackageItr->Id); 123 REQUIRE(firstPackageItr->VersionAndChannel.ToString() == secondPackageItr->VersionAndChannel.ToString()); 124 } 125 } 126 } 127 } 128 129 TEST_CASE("PackageCollection_Write_SingleSource", "[PackageCollection]") 130 { 131 PackageCollection::Source source; 132 source.Details.Name = "TestSource"; 133 source.Details.Arg = "https://aka.ms/winget"; 134 source.Details.Type = "Microsoft.PreIndexed.Package"; 135 source.Details.Identifier = "TestSourceId"; 136 137 source.Packages.emplace_back(LocIndString{ "test.package1"sv }, Version{ "1.0.1" }, Channel{ "" }); 138 source.Packages.emplace_back(LocIndString{ "test.package2"sv }, Version{ "2" }, Channel{ "Public" }); 139 140 PackageCollection pc 141 { 142 "0.1.2.3", 143 std::vector<PackageCollection::Source>{ source } 144 }; 145 146 ValidateJsonWithCollection(PackagesJson::CreateJson(pc), pc); 147 } 148 149 TEST_CASE("PackageCollection_Write_MultipleSources", "[PackageCollection]") 150 { 151 PackageCollection::Source source1; 152 source1.Details.Name = "TestSource"; 153 source1.Details.Arg = "https://aka.ms/winget"; 154 source1.Details.Type = "Microsoft.PreIndexed.Package"; 155 source1.Details.Identifier = "TestSourceId"; 156 source1.Packages.emplace_back(LocIndString{ "test.package1"sv }, Version{ "1.0.1" }, Channel{ "" }); 157 158 PackageCollection::Source source2; 159 source2.Details.Name = "TestSource2"; 160 source2.Details.Arg = "https://aka.ms/winget"; 161 source2.Details.Type = "*Test"; 162 source2.Details.Identifier = "SecondId"; 163 source2.Packages.emplace_back(LocIndString{ "test.package2"sv }, Version{ "2.1.0" }, Channel{ "Beta" }); 164 165 PackageCollection pc 166 { 167 "1.0.0.0", 168 std::vector<PackageCollection::Source>{ source1, source2 } 169 }; 170 171 ValidateJsonWithCollection(PackagesJson::CreateJson(pc), pc); 172 } 173 174 TEST_CASE("PackageCollection_Read_SingleSource_1_0", "[PackageCollection]") 175 { 176 auto json = ParseJsonString(R"( 177 { 178 "$schema": "https://aka.ms/winget-packages.schema.1.0.json", 179 "CreationDate": "2021-01-01T12:00:00.000-00:00", 180 "Sources": [ 181 { 182 "Packages": [ 183 { 184 "Id": "test.WithVersion", 185 "Version": "0.1", 186 "Channel": "Preview" 187 }, 188 { 189 "Id": "test.NoVersion" 190 } 191 ], 192 "SourceDetails": { 193 "Argument": "https://aka.ms/winget", 194 "Identifier": "TestSourceId", 195 "Name": "TestSource", 196 "Type": "Microsoft.PreIndexed.Package" 197 } 198 } 199 ], 200 "WinGetVersion": "1.0.0" 201 })"); 202 203 auto parseResult = PackagesJson::TryParseJson(json); 204 REQUIRE(parseResult.Result == PackagesJson::ParseResult::Type::Success); 205 REQUIRE(parseResult.Errors.empty()); 206 207 PackageCollection::Source source; 208 source.Details.Name = "TestSource"; 209 source.Details.Arg = "https://aka.ms/winget"; 210 source.Details.Type = "Microsoft.PreIndexed.Package"; 211 source.Details.Identifier = "TestSourceId"; 212 213 source.Packages.emplace_back(LocIndString{ "test.WithVersion"sv }, Version{ "0.1" }, Channel{ "Preview" }); 214 source.Packages.emplace_back(LocIndString{ "test.NoVersion"sv }, Version{ "" }, Channel{ "" }); 215 216 PackageCollection expected 217 { 218 "1.0.0", 219 std::vector<PackageCollection::Source>{ source } 220 }; 221 222 ValidateEqualCollections(parseResult.Packages, expected); 223 } 224 225 TEST_CASE("PackageCollection_Read_SingleSource_2_0", "[PackageCollection]") 226 { 227 auto json = ParseJsonString(R"( 228 { 229 "$schema": "https://aka.ms/winget-packages.schema.2.0.json", 230 "CreationDate": "2021-01-01T12:00:00.000-00:00", 231 "Sources": [ 232 { 233 "Packages": [ 234 { 235 "PackageIdentifier": "test.WithVersion", 236 "Version": "0.1", 237 "Channel": "Preview" 238 }, 239 { 240 "PackageIdentifier": "test.NoVersion" 241 } 242 ], 243 "SourceDetails": { 244 "Argument": "https://aka.ms/winget", 245 "Identifier": "TestSourceId", 246 "Name": "TestSource", 247 "Type": "Microsoft.PreIndexed.Package" 248 } 249 } 250 ] 251 })"); 252 253 auto parseResult = PackagesJson::TryParseJson(json); 254 REQUIRE(parseResult.Result == PackagesJson::ParseResult::Type::Success); 255 REQUIRE(parseResult.Errors.empty()); 256 257 PackageCollection::Source source; 258 source.Details.Name = "TestSource"; 259 source.Details.Arg = "https://aka.ms/winget"; 260 source.Details.Type = "Microsoft.PreIndexed.Package"; 261 source.Details.Identifier = "TestSourceId"; 262 263 source.Packages.emplace_back(LocIndString{ "test.WithVersion"sv }, Version{ "0.1" }, Channel{ "Preview" }); 264 source.Packages.emplace_back(LocIndString{ "test.NoVersion"sv }, Version{ "" }, Channel{ "" }); 265 266 PackageCollection expected 267 { 268 "", 269 std::vector<PackageCollection::Source>{ source } 270 }; 271 272 ValidateEqualCollections(parseResult.Packages, expected); 273 } 274 275 TEST_CASE("PackageCollection_Read_MultipleSources_1_0", "[PackageCollection]") 276 { 277 auto json = ParseJsonString(R"( 278 { 279 "$schema": "https://aka.ms/winget-packages.schema.1.0.json", 280 "CreationDate": "2021-01-01T12:00:00.000-00:00", 281 "WinGetVersion": "1.0.0", 282 "Sources": [ 283 { 284 "SourceDetails": { 285 "Argument": "//firstSource", 286 "Identifier": "Id1", 287 "Name": "First", 288 "Type": "Microsoft.PreIndexed.Package" 289 }, 290 "Packages": [ 291 { 292 "Id": "test" 293 } 294 ] 295 }, 296 { 297 "SourceDetails": { 298 "Argument": "//secondSource", 299 "Identifier": "Id2", 300 "Name": "Second", 301 "Type": "*TestSource" 302 }, 303 "Packages": [ 304 { 305 "Id": "test2", 306 "Version": "1.0" 307 } 308 ] 309 } 310 ] 311 })"); 312 313 314 auto parseResult = PackagesJson::TryParseJson(json); 315 REQUIRE(parseResult.Result == PackagesJson::ParseResult::Type::Success); 316 REQUIRE(parseResult.Errors.empty()); 317 318 PackageCollection::Source source1; 319 source1.Details.Name = "First"; 320 source1.Details.Arg = "//firstSource"; 321 source1.Details.Type = "Microsoft.PreIndexed.Package"; 322 source1.Details.Identifier = "Id1"; 323 source1.Packages.emplace_back(LocIndString{ "test"sv }, Version{ "" }, Channel{ "" }); 324 325 PackageCollection::Source source2; 326 source2.Details.Name = "Second"; 327 source2.Details.Arg = "//secondSource"; 328 source2.Details.Type = "*TestSource"; 329 source2.Details.Identifier = "Id2"; 330 source2.Packages.emplace_back(LocIndString{ "test2"sv }, Version{ "1.0" }, Channel{ "" }); 331 332 PackageCollection expected 333 { 334 "1.0.0", 335 std::vector<PackageCollection::Source>{ source1, source2 } 336 }; 337 338 ValidateEqualCollections(parseResult.Packages, expected); 339 } 340 341 TEST_CASE("PackageCollection_Read_MultipleSources_2_0", "[PackageCollection]") 342 { 343 auto json = ParseJsonString(R"( 344 { 345 "$schema": "https://aka.ms/winget-packages.schema.2.0.json", 346 "CreationDate": "2021-01-01T12:00:00.000-00:00", 347 "WinGetVersion": "1.0.0", 348 "Sources": [ 349 { 350 "SourceDetails": { 351 "Argument": "//firstSource", 352 "Identifier": "Id1", 353 "Name": "First", 354 "Type": "Microsoft.PreIndexed.Package" 355 }, 356 "Packages": [ 357 { 358 "PackageIdentifier": "test" 359 } 360 ] 361 }, 362 { 363 "SourceDetails": { 364 "Argument": "//secondSource", 365 "Identifier": "Id2", 366 "Name": "Second", 367 "Type": "*TestSource" 368 }, 369 "Packages": [ 370 { 371 "PackageIdentifier": "test2", 372 "Version": "1.0" 373 } 374 ] 375 } 376 ] 377 })"); 378 379 380 auto parseResult = PackagesJson::TryParseJson(json); 381 REQUIRE(parseResult.Result == PackagesJson::ParseResult::Type::Success); 382 REQUIRE(parseResult.Errors.empty()); 383 384 PackageCollection::Source source1; 385 source1.Details.Name = "First"; 386 source1.Details.Arg = "//firstSource"; 387 source1.Details.Type = "Microsoft.PreIndexed.Package"; 388 source1.Details.Identifier = "Id1"; 389 source1.Packages.emplace_back(LocIndString{ "test"sv }, Version{ "" }, Channel{ "" }); 390 391 PackageCollection::Source source2; 392 source2.Details.Name = "Second"; 393 source2.Details.Arg = "//secondSource"; 394 source2.Details.Type = "*TestSource"; 395 source2.Details.Identifier = "Id2"; 396 source2.Packages.emplace_back(LocIndString{ "test2"sv }, Version{ "1.0" }, Channel{ "" }); 397 398 PackageCollection expected 399 { 400 "1.0.0", 401 std::vector<PackageCollection::Source>{ source1, source2 } 402 }; 403 404 ValidateEqualCollections(parseResult.Packages, expected); 405 } 406 407 TEST_CASE("PackageCollection_Read_RepeatedSource", "[PackageCollection]") 408 { 409 auto json = ParseJsonString(R"( 410 { 411 "$schema": "https://aka.ms/winget-packages.schema.1.0.json", 412 "CreationDate": "2021-01-01T12:00:00.000-00:00", 413 "WinGetVersion": "1.0.0", 414 "Sources": [ 415 { 416 "SourceDetails": { 417 "Argument": "//firstSource", 418 "Identifier": "Id1", 419 "Name": "First", 420 "Type": "Microsoft.PreIndexed.Package" 421 }, 422 "Packages": [ 423 { 424 "Id": "test" 425 } 426 ] 427 }, 428 { 429 "SourceDetails": { 430 "Argument": "//secondSource", 431 "Identifier": "Id2", 432 "Name": "Second", 433 "Type": "*TestSource" 434 }, 435 "Packages": [ 436 { 437 "Id": "test2", 438 "Version": "1.0" 439 } 440 ] 441 }, 442 { 443 "SourceDetails": { 444 "Argument": "//secondSource", 445 "Identifier": "Id2", 446 "Name": "Second", 447 "Type": "*TestSource" 448 }, 449 "Packages": [ 450 { 451 "Id": "test3", 452 "Version": "1.2" 453 } 454 ] 455 } 456 ] 457 })"); 458 459 460 auto parseResult = PackagesJson::TryParseJson(json); 461 REQUIRE(parseResult.Result == PackagesJson::ParseResult::Type::Success); 462 REQUIRE(parseResult.Errors.empty()); 463 464 PackageCollection::Source source1; 465 source1.Details.Name = "First"; 466 source1.Details.Arg = "//firstSource"; 467 source1.Details.Type = "Microsoft.PreIndexed.Package"; 468 source1.Details.Identifier = "Id1"; 469 source1.Packages.emplace_back(LocIndString{ "test"sv }, Version{ "" }, Channel{ "" }); 470 471 PackageCollection::Source source2; 472 source2.Details.Name = "Second"; 473 source2.Details.Arg = "//secondSource"; 474 source2.Details.Type = "*TestSource"; 475 source2.Details.Identifier = "Id2"; 476 source2.Packages.emplace_back(LocIndString{ "test2"sv }, Version{ "1.0" }, Channel{ "" }); 477 source2.Packages.emplace_back(LocIndString{ "test3"sv }, Version{ "1.2" }, Channel{ "" }); 478 479 PackageCollection expected 480 { 481 "1.0.0", 482 std::vector<PackageCollection::Source>{ source1, source2 } 483 }; 484 485 ValidateEqualCollections(parseResult.Packages, expected); 486 } 487 488 TEST_CASE("PackageCollection_Read_MissingSchema", "[PackageCollection]") 489 { 490 auto json = ParseJsonString(R"( 491 { 492 "CreationDate": "2021-01-01T12:00:00.000-00:00", 493 "Sources": [ 494 { 495 "Packages": [ 496 { 497 "Id": "test.test" 498 } 499 ], 500 "SourceDetails": { 501 "Argument": "https://aka.ms/winget", 502 "Identifier": "TestSourceId", 503 "Name": "TestSource", 504 "Type": "Microsoft.PreIndexed.Package" 505 } 506 } 507 ], 508 "WinGetVersion": "1.0.0" 509 })"); 510 511 auto parseResult = PackagesJson::TryParseJson(json); 512 REQUIRE(parseResult.Result == PackagesJson::ParseResult::Type::MissingSchema); 513 514 json = ParseJsonString("\"Not even a JSON object\""); 515 516 parseResult = PackagesJson::TryParseJson(json); 517 REQUIRE(parseResult.Result == PackagesJson::ParseResult::Type::MissingSchema); 518 } 519 520 TEST_CASE("PackageCollection_Read_WrongSchema", "[PackageCollection]") 521 { 522 auto json = ParseJsonString(R"( 523 { 524 "$schema": "https://aka.ms/winget-settings.schema.json", 525 "CreationDate": "2021-01-01T12:00:00.000-00:00", 526 "Sources": [ 527 { 528 "Packages": [ 529 { 530 "Id": "test.test" 531 } 532 ], 533 "SourceDetails": { 534 "Argument": "https://aka.ms/winget", 535 "Identifier": "TestSourceId", 536 "Name": "TestSource", 537 "Type": "Microsoft.PreIndexed.Package" 538 } 539 } 540 ], 541 "WinGetVersion": "1.0.0" 542 })"); 543 544 auto parseResult = PackagesJson::TryParseJson(json); 545 REQUIRE(parseResult.Result == PackagesJson::ParseResult::Type::UnrecognizedSchema); 546 } 547 548 TEST_CASE("PackageCollection_Read_SchemaValidationFail", "[PackageCollection]") 549 { 550 auto json = ParseJsonString(R"( 551 { 552 "$schema": "https://aka.ms/winget-packages.schema.1.0.json", 553 "CreationDate": "2021-01-01T12:00:00.000-00:00", 554 "NotSources": [ 555 { 556 "Packages": [ 557 { 558 "Id": "test.test" 559 } 560 ], 561 "SourceDetails": { 562 "Argument": "https://aka.ms/winget", 563 "Identifier": "TestSourceId", 564 "Name": "TestSource", 565 "Type": "Microsoft.PreIndexed.Package" 566 } 567 } 568 ], 569 "WinGetVersion": "1.0.0" 570 })"); 571 572 auto parseResult = PackagesJson::TryParseJson(json); 573 INFO(parseResult.Errors); 574 575 REQUIRE(parseResult.Result == PackagesJson::ParseResult::Type::SchemaValidationFailed); 576 REQUIRE(parseResult.Errors.find("Missing required property 'Sources'.") != std::string::npos); 577 } 578 579 TEST_CASE("PackageCollection_Read_SchemaValidationFail_Id", "[PackageCollection]") 580 { 581 auto json = ParseJsonString(R"( 582 { 583 "$schema": "https://aka.ms/winget-packages.schema.1.0.json", 584 "CreationDate": "2021-01-01T12:00:00.000-00:00", 585 "Sources": [ 586 { 587 "Packages": [ 588 { 589 "NotId": "test.test" 590 } 591 ], 592 "SourceDetails": { 593 "Argument": "https://aka.ms/winget", 594 "Identifier": "TestSourceId", 595 "Name": "TestSource", 596 "Type": "Microsoft.PreIndexed.Package" 597 } 598 } 599 ], 600 "WinGetVersion": "1.0.0" 601 })"); 602 603 auto parseResult = PackagesJson::TryParseJson(json); 604 INFO(parseResult.Errors); 605 606 REQUIRE(parseResult.Result == PackagesJson::ParseResult::Type::SchemaValidationFailed); 607 REQUIRE(parseResult.Errors.find("Missing required property 'Id'.") != std::string::npos); 608 } 609 610 TEST_CASE("PackageCollection_Read_BadTimeStamp", "[PackageCollection]") 611 { 612 auto json = ParseJsonString(R"( 613 { 614 "$schema": "https://aka.ms/winget-packages.schema.1.0.json", 615 "CreationDate": "2021- 1- 1 12:00:00.000", 616 "Sources": [ 617 { 618 "Packages": [ 619 { 620 "Id": "test" 621 } 622 ], 623 "SourceDetails": { 624 "Argument": "https://aka.ms/winget", 625 "Identifier": "TestSourceId", 626 "Name": "TestSource", 627 "Type": "Microsoft.PreIndexed.Package" 628 } 629 } 630 ], 631 "WinGetVersion": "1.0.0" 632 })"); 633 634 auto parseResult = PackagesJson::TryParseJson(json); 635 INFO(parseResult.Errors); 636 637 REQUIRE(parseResult.Result == PackagesJson::ParseResult::Type::SchemaValidationFailed); 638 REQUIRE_FALSE(parseResult.Errors.empty()); 639 }