From e5cf4427b8cde0d94d02247ab09721520b79a34e Mon Sep 17 00:00:00 2001 From: Elshiekh Ahmed Date: Wed, 24 Jun 2026 09:51:26 -0700 Subject: [PATCH 01/11] Add WireMock.Net to integration test project (Task 2/6 Phase 0) Add the WireMock.Net package reference and lock-file entries; it provides the in-process HTTP mock server used to build the Ingestion and XFUS fixtures in later phases. The package is saved to the private feed and is publicly readable, so CI restores it without authentication. --- .../PackageUploader.IntegrationTest.csproj | 1 + .../packages.lock.json | 1043 +++++++++++++++++ 2 files changed, 1044 insertions(+) diff --git a/src/PackageUploader.IntegrationTest/PackageUploader.IntegrationTest.csproj b/src/PackageUploader.IntegrationTest/PackageUploader.IntegrationTest.csproj index 85e2569e..37972d8d 100644 --- a/src/PackageUploader.IntegrationTest/PackageUploader.IntegrationTest.csproj +++ b/src/PackageUploader.IntegrationTest/PackageUploader.IntegrationTest.csproj @@ -16,6 +16,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/PackageUploader.IntegrationTest/packages.lock.json b/src/PackageUploader.IntegrationTest/packages.lock.json index 2bf55d70..1559dc72 100644 --- a/src/PackageUploader.IntegrationTest/packages.lock.json +++ b/src/PackageUploader.IntegrationTest/packages.lock.json @@ -58,6 +58,25 @@ "MSTest.Analyzers": "4.2.2" } }, + "WireMock.Net": { + "type": "Direct", + "requested": "[2.11.0, )", + "resolved": "2.11.0", + "contentHash": "gQgO9LPslgAPr0cHwHIx2OP3UYZX0Ex11UdAhFyiY2cHBjpxtw9A6GOcF79B56vMFOhYFBHWUgl6fcSFLin0yg==", + "dependencies": { + "WireMock.Net.GraphQL": "2.11.0", + "WireMock.Net.Matchers.SystemTextJsonPath": "2.11.0", + "WireMock.Net.MimePart": "2.11.0", + "WireMock.Net.Minimal": "2.11.0", + "WireMock.Net.OpenTelemetry": "2.11.0", + "WireMock.Net.ProtoBuf": "2.11.0" + } + }, + "AnyOf": { + "type": "Transitive", + "resolved": "0.5.0.1", + "contentHash": "WDQw5Qos3mhCumSCgKD70TM1dmqBAuJFGv1cFtNTwTaDLZR7kGy33M5C+L0vZV/bNRNwyi5ABvRGPWHL17rkNw==" + }, "Azure.Core": { "type": "Transitive", "resolved": "1.54.0", @@ -106,16 +125,699 @@ "System.Diagnostics.EventLog": "6.0.0" } }, + "Fare": { + "type": "Transitive", + "resolved": "2.2.1", + "contentHash": "21XZo/yuXK1k0EUhdLnjgRD4n0HQYmPFchV6uaORcRc65rasZ1vdm2dmJXPBKZiIBztRRYRmmg/B76W721VWkA==" + }, + "GraphQL": { + "type": "Transitive", + "resolved": "8.5.0", + "contentHash": "BZkfH7GVacTZEkyqa4XN9mW12UA/0XYrpEkkrJnNBf0Pqw8CZWQfItLaWgG2C1Ju9YHH1UUG+LmIpo8iMP6pKA==", + "dependencies": { + "GraphQL-Parser": "9.5.0", + "GraphQL.Analyzers": "8.5.0" + } + }, + "GraphQL-Parser": { + "type": "Transitive", + "resolved": "9.5.0", + "contentHash": "5XWJGKHdVi8pyD4P0EglmJmlXEGs0HzvGlEBf3+/Ve1jLYBBKIOkKvY0Ej17b9Kn1bbBxkrmghqbmsMbkLL1nQ==" + }, + "GraphQL.Analyzers": { + "type": "Transitive", + "resolved": "8.5.0", + "contentHash": "jwfvZD5agmw9J8iZEe6BUfKAY+/lC7EqDQg+6JRwXaQ6G/MCLy8jyBc2SHF/2JdAtrkygc1bVyCUP5mR2PHzVA==" + }, + "GraphQL.NewtonsoftJson": { + "type": "Transitive", + "resolved": "8.5.0", + "contentHash": "tAeUoUhJih5fdZRCV0ue3G/gsu8YBiyNZkgLVFyk0wTk8vJGLTBDJaP5o5LVo1edVnk0bR+0/PaNXAEJxkVrTw==", + "dependencies": { + "GraphQL": "[8.5.0, 9.0.0)", + "Newtonsoft.Json": "13.0.3" + } + }, + "Handlebars.Net": { + "type": "Transitive", + "resolved": "2.1.6", + "contentHash": "WsYWCEXsIM6hEOSOSRHtIYLjC8BnbT5MVmqhNKRqUI7qiv0t8x3nJiBTEv0ZZfvUAMAFnadGIzSsS/U2anVG1Q==" + }, + "Handlebars.Net.Helpers": { + "type": "Transitive", + "resolved": "2.5.5", + "contentHash": "MZ0/Nvy3XdEy/igZD4fJy5HiUKcKUA170Sq+2RmJFsGiWSqlroXzp8TQqJTDCi1aBtETfQOVdBG0YNvuDs9+uQ==", + "dependencies": { + "Handlebars.Net.Helpers.Core": "2.5.5" + } + }, + "Handlebars.Net.Helpers.Core": { + "type": "Transitive", + "resolved": "2.5.5", + "contentHash": "vLTL6UrLUPPiWDCKig8FLhSU+i9J4n/8RfrhadvnvxqziyK0ArxKMT2gLqQ+X/8vJaRcI9zvD5HxA8KjWbq3Dw==", + "dependencies": { + "Handlebars.Net": "2.1.6", + "Stef.Validation": "0.1.1" + } + }, + "Handlebars.Net.Helpers.Humanizer": { + "type": "Transitive", + "resolved": "2.5.5", + "contentHash": "A7TmfLtv7x8HiVckXBmKmOAsO5GKxjSOjxymXS70upqzLLH8BjrhFl+QIGFCdVIWQRx3+yNjGcsz/JXNwt9YZg==", + "dependencies": { + "Handlebars.Net.Helpers": "2.5.5", + "Handlebars.Net.Helpers.Core": "2.5.5", + "Humanizer": "[2.14.1, 4.0.0)" + } + }, + "Handlebars.Net.Helpers.Json": { + "type": "Transitive", + "resolved": "2.5.5", + "contentHash": "iRBo/ik0M8M6ezJt4QzZm5KQptEdeh6bVtnDbieuxh5YPTUsPMFvtoq0gg426PwrahE+5rXoFZmIM11Oy5GwTg==", + "dependencies": { + "Handlebars.Net.Helpers": "2.5.5", + "Handlebars.Net.Helpers.Core": "2.5.5", + "Newtonsoft.Json": "13.0.3" + } + }, + "Handlebars.Net.Helpers.Random": { + "type": "Transitive", + "resolved": "2.5.5", + "contentHash": "zKcfFDN4QxgEjk4Em9yz/PQu0mBpIgEaqjhacg2Fl6M0oSsF7VBVflae2WRM9MtiVeRTwLkVwcy7TvJ6iqFuVQ==", + "dependencies": { + "Handlebars.Net.Helpers": "2.5.5", + "Handlebars.Net.Helpers.Core": "2.5.5", + "RandomDataGenerator.Net": "1.0.19" + } + }, + "Handlebars.Net.Helpers.Xeger": { + "type": "Transitive", + "resolved": "2.5.5", + "contentHash": "J+w9KalIuYlTKMeIv8eoisdoMEz44elri0UOLtfTAuDbADwnBBsJGp4kAQI107+hBcqery9OCRCXm8fvH4eCxQ==", + "dependencies": { + "Fare": "2.2.1", + "Handlebars.Net.Helpers": "2.5.5", + "Handlebars.Net.Helpers.Core": "2.5.5" + } + }, + "Handlebars.Net.Helpers.XPath": { + "type": "Transitive", + "resolved": "2.5.5", + "contentHash": "uUGzjR5w5YCv+BdWQ4RpWAho0tUG0zfAKG5v+abXS6+E+fjbfSshOg7LyoWTVcGTWO0PouukhSMUFaumB2K4tg==", + "dependencies": { + "Handlebars.Net.Helpers": "2.5.5", + "Handlebars.Net.Helpers.Core": "2.5.5", + "XPath2.Extensions": "1.1.5" + } + }, + "Handlebars.Net.Helpers.Xslt": { + "type": "Transitive", + "resolved": "2.5.5", + "contentHash": "bOaX47avO4Uja6jTZcBAgS5KjL/2ZaewCpB0Oy7cVegctPyxiiRx/T44XGSt0133hHry9f5nJVsjFKNLrYq0Pg==", + "dependencies": { + "Handlebars.Net.Helpers": "2.5.5", + "Handlebars.Net.Helpers.Core": "2.5.5" + } + }, + "Humanizer": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "/FUTD3cEceAAmJSCPN9+J+VhGwmL/C12jvwlyM1DFXShEMsBzvLzLqSrJ2rb+k/W2znKw7JyflZgZpyE+tI7lA==", + "dependencies": { + "Humanizer.Core.af": "2.14.1", + "Humanizer.Core.ar": "2.14.1", + "Humanizer.Core.az": "2.14.1", + "Humanizer.Core.bg": "2.14.1", + "Humanizer.Core.bn-BD": "2.14.1", + "Humanizer.Core.cs": "2.14.1", + "Humanizer.Core.da": "2.14.1", + "Humanizer.Core.de": "2.14.1", + "Humanizer.Core.el": "2.14.1", + "Humanizer.Core.es": "2.14.1", + "Humanizer.Core.fa": "2.14.1", + "Humanizer.Core.fi-FI": "2.14.1", + "Humanizer.Core.fr": "2.14.1", + "Humanizer.Core.fr-BE": "2.14.1", + "Humanizer.Core.he": "2.14.1", + "Humanizer.Core.hr": "2.14.1", + "Humanizer.Core.hu": "2.14.1", + "Humanizer.Core.hy": "2.14.1", + "Humanizer.Core.id": "2.14.1", + "Humanizer.Core.is": "2.14.1", + "Humanizer.Core.it": "2.14.1", + "Humanizer.Core.ja": "2.14.1", + "Humanizer.Core.ko-KR": "2.14.1", + "Humanizer.Core.ku": "2.14.1", + "Humanizer.Core.lv": "2.14.1", + "Humanizer.Core.ms-MY": "2.14.1", + "Humanizer.Core.mt": "2.14.1", + "Humanizer.Core.nb": "2.14.1", + "Humanizer.Core.nb-NO": "2.14.1", + "Humanizer.Core.nl": "2.14.1", + "Humanizer.Core.pl": "2.14.1", + "Humanizer.Core.pt": "2.14.1", + "Humanizer.Core.ro": "2.14.1", + "Humanizer.Core.ru": "2.14.1", + "Humanizer.Core.sk": "2.14.1", + "Humanizer.Core.sl": "2.14.1", + "Humanizer.Core.sr": "2.14.1", + "Humanizer.Core.sr-Latn": "2.14.1", + "Humanizer.Core.sv": "2.14.1", + "Humanizer.Core.th-TH": "2.14.1", + "Humanizer.Core.tr": "2.14.1", + "Humanizer.Core.uk": "2.14.1", + "Humanizer.Core.uz-Cyrl-UZ": "2.14.1", + "Humanizer.Core.uz-Latn-UZ": "2.14.1", + "Humanizer.Core.vi": "2.14.1", + "Humanizer.Core.zh-CN": "2.14.1", + "Humanizer.Core.zh-Hans": "2.14.1", + "Humanizer.Core.zh-Hant": "2.14.1" + } + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" + }, + "Humanizer.Core.af": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "BoQHyu5le+xxKOw+/AUM7CLXneM/Bh3++0qh1u0+D95n6f9eGt9kNc8LcAHLIOwId7Sd5hiAaaav0Nimj3peNw==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.ar": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "3d1V10LDtmqg5bZjWkA/EkmGFeSfNBcyCH+TiHcHP+HGQQmRq3eBaLcLnOJbVQVn3Z6Ak8GOte4RX4kVCxQlFA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.az": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "8Z/tp9PdHr/K2Stve2Qs/7uqWPWLUK9D8sOZDNzyv42e20bSoJkHFn7SFoxhmaoVLJwku2jp6P7HuwrfkrP18Q==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.bg": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "S+hIEHicrOcbV2TBtyoPp1AVIGsBzlarOGThhQYCnP6QzEYo/5imtok6LMmhZeTnBFoKhM8yJqRfvJ5yqVQKSQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.bn-BD": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "U3bfj90tnUDRKlL1ZFlzhCHoVgpTcqUlTQxjvGCaFKb+734TTu3nkHUWVZltA1E/swTvimo/aXLtkxnLFrc0EQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.cs": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "jWrQkiCTy3L2u1T86cFkgijX6k7hoB0pdcFMWYaSZnm6rvG/XJE40tfhYyKhYYgIc1x9P2GO5AC7xXvFnFdqMQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.da": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "5o0rJyE/2wWUUphC79rgYDnif/21MKTTx9LIzRVz9cjCIVFrJ2bDyR2gapvI9D6fjoyvD1NAfkN18SHBsO8S9g==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.de": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "9JD/p+rqjb8f5RdZ3aEJqbjMYkbk4VFii2QDnnOdNo6ywEfg/A5YeOQ55CaBJmy7KvV4tOK4+qHJnX/tg3Z54A==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.el": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "Xmv6sTL5mqjOWGGpqY7bvbfK5RngaUHSa8fYDGSLyxY9mGdNbDcasnRnMOvi0SxJS9gAqBCn21Xi90n2SHZbFA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.es": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "e//OIAeMB7pjBV1HqqI4pM2Bcw3Jwgpyz9G5Fi4c+RJvhqFwztoWxW57PzTnNJE2lbhGGLQZihFZjsbTUsbczA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.fa": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "nzDOj1x0NgjXMjsQxrET21t1FbdoRYujzbmZoR8u8ou5CBWY1UNca0j6n/PEJR/iUbt4IxstpszRy41wL/BrpA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.fi-FI": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "Vnxxx4LUhp3AzowYi6lZLAA9Lh8UqkdwRh4IE2qDXiVpbo08rSbokATaEzFS+o+/jCNZBmoyyyph3vgmcSzhhQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.fr": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "2p4g0BYNzFS3u9SOIDByp2VClYKO0K1ecDV4BkB9EYdEPWfFODYnF+8CH8LpUrpxL2TuWo2fiFx/4Jcmrnkbpg==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.fr-BE": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "o6R3SerxCRn5Ij8nCihDNMGXlaJ/1AqefteAssgmU2qXYlSAGdhxmnrQAXZUDlE4YWt/XQ6VkNLtH7oMqsSPFQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.he": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "FPsAhy7Iw6hb+ZitLgYC26xNcgGAHXb0V823yFAzcyoL5ozM+DCJtYfDPYiOpsJhEZmKFTM9No0jUn1M89WGvg==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.hr": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "chnaD89yOlST142AMkAKLuzRcV5df3yyhDyRU5rypDiqrq2HN8y1UR3h1IicEAEtXLoOEQyjSAkAQ6QuXkn7aw==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.hu": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "hAfnaoF9LTGU/CmFdbnvugN4tIs8ppevVMe3e5bD24+tuKsggMc5hYta9aiydI8JH9JnuVmxvNI4DJee1tK05A==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.hy": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "sVIKxOiSBUb4gStRHo9XwwAg9w7TNvAXbjy176gyTtaTiZkcjr9aCPziUlYAF07oNz6SdwdC2mwJBGgvZ0Sl2g==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.id": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "4Zl3GTvk3a49Ia/WDNQ97eCupjjQRs2iCIZEQdmkiqyaLWttfb+cYXDMGthP42nufUL0SRsvBctN67oSpnXtsg==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.is": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "R67A9j/nNgcWzU7gZy1AJ07ABSLvogRbqOWvfRDn4q6hNdbg/mjGjZBp4qCTPnB2mHQQTCKo3oeCUayBCNIBCw==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.it": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "jYxGeN4XIKHVND02FZ+Woir3CUTyBhLsqxu9iqR/9BISArkMf1Px6i5pRZnvq4fc5Zn1qw71GKKoCaHDJBsLFw==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.ja": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "TM3ablFNoYx4cYJybmRgpDioHpiKSD7q0QtMrmpsqwtiiEsdW5zz/q4PolwAczFnvrKpN6nBXdjnPPKVet93ng==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.ko-KR": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "CtvwvK941k/U0r8PGdEuBEMdW6jv/rBiA9tUhakC7Zd2rA/HCnDcbr1DiNZ+/tRshnhzxy/qwmpY8h4qcAYCtQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.ku": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "vHmzXcVMe+LNrF9txpdHzpG7XJX65SiN9GQd/Zkt6gsGIIEeECHrkwCN5Jnlkddw2M/b0HS4SNxdR1GrSn7uCA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.lv": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "E1/KUVnYBS1bdOTMNDD7LV/jdoZv/fbWTLPtvwdMtSdqLyRTllv6PGM9xVQoFDYlpvVGtEl/09glCojPHw8ffA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.ms-MY": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "vX8oq9HnYmAF7bek4aGgGFJficHDRTLgp/EOiPv9mBZq0i4SA96qVMYSjJ2YTaxs7Eljqit7pfpE2nmBhY5Fnw==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.mt": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "pEgTBzUI9hzemF7xrIZigl44LidTUhNu4x/P6M9sAwZjkUF0mMkbpxKkaasOql7lLafKrnszs0xFfaxQyzeuZQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.nb": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "mbs3m6JJq53ssLqVPxNfqSdTxAcZN3njlG8yhJVx83XVedpTe1ECK9aCa8FKVOXv93Gl+yRHF82Hw9T9LWv2hw==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.nb-NO": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "AsJxrrVYmIMbKDGe8W6Z6//wKv9dhWH7RsTcEHSr4tQt/80pcNvLi0hgD3fqfTtg0tWKtgch2cLf4prorEV+5A==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.nl": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "24b0OUdzJxfoqiHPCtYnR5Y4l/s4Oh7KW7uDp+qX25NMAHLCGog2eRfA7p2kRJp8LvnynwwQxm2p534V9m55wQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.pl": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "17mJNYaBssENVZyQHduiq+bvdXS0nhZJGEXtPKoMhKv3GD//WO0mEfd9wjEBsWCSmWI7bjRqhCidxzN+YtJmsg==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.pt": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "8HB8qavcVp2la1GJX6t+G9nDYtylPKzyhxr9LAooIei9MnQvNsjEiIE4QvHoeDZ4weuQ9CsPg1c211XUMVEZ4A==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.ro": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "psXNOcA6R8fSHoQYhpBTtTTYiOk8OBoN3PKCEDgsJKIyeY5xuK81IBdGi77qGZMu/OwBRQjQCBMtPJb0f4O1+A==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.ru": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "zm245xUWrajSN2t9H7BTf84/2APbUkKlUJpcdgsvTdAysr1ag9fi1APu6JEok39RRBXDfNRVZHawQ/U8X0pSvQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.sk": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "Ncw24Vf3ioRnbU4MsMFHafkyYi8JOnTqvK741GftlQvAbULBoTz2+e7JByOaasqeSi0KfTXeegJO+5Wk1c0Mbw==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.sl": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "l8sUy4ciAIbVThWNL0atzTS2HWtv8qJrsGWNlqrEKmPwA4SdKolSqnTes9V89fyZTc2Q43jK8fgzVE2C7t009A==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.sr": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "rnNvhpkOrWEymy7R/MiFv7uef8YO5HuXDyvojZ7JpijHWA5dXuVXooCOiA/3E93fYa3pxDuG2OQe4M/olXbQ7w==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.sr-Latn": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "nuy/ykpk974F8ItoQMS00kJPr2dFNjOSjgzCwfysbu7+gjqHmbLcYs7G4kshLwdA4AsVncxp99LYeJgoh1JF5g==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.sv": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "E53+tpAG0RCp+cSSI7TfBPC+NnsEqUuoSV0sU+rWRXWr9MbRWx1+Zj02XMojqjGzHjjOrBFBBio6m74seFl0AA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.th-TH": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "eSevlJtvs1r4vQarNPfZ2kKDp/xMhuD00tVVzRXkSh1IAZbBJI/x2ydxUOwfK9bEwEp+YjvL1Djx2+kw7ziu7g==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.tr": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "rQ8N+o7yFcFqdbtu1mmbrXFi8TQ+uy+fVH9OPI0CI3Cu1om5hUU/GOMC3hXsTCI6d79y4XX+0HbnD7FT5khegA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.uk": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "2uEfujwXKNm6bdpukaLtEJD+04uUtQD65nSGCetA1fYNizItEaIBUboNfr3GzJxSMQotNwGVM3+nSn8jTd0VSg==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.uz-Cyrl-UZ": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "TD3ME2sprAvFqk9tkWrvSKx5XxEMlAn1sjk+cYClSWZlIMhQQ2Bp/w0VjX1Kc5oeKjxRAnR7vFcLUFLiZIDk9Q==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.uz-Latn-UZ": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "/kHAoF4g0GahnugZiEMpaHlxb+W6jCEbWIdsq9/I1k48ULOsl/J0pxZj93lXC3omGzVF1BTVIeAtv5fW06Phsg==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.vi": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "rsQNh9rmHMBtnsUUlJbShMsIMGflZtPmrMM6JNDw20nhsvqfrdcoDD8cMnLAbuSovtc3dP+swRmLQzKmXDTVPA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.zh-CN": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "uH2dWhrgugkCjDmduLdAFO9w1Mo0q07EuvM0QiIZCVm6FMCu/lGv2fpMu4GX+4HLZ6h5T2Pg9FIdDLCPN2a67w==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.zh-Hans": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "WH6IhJ8V1UBG7rZXQk3dZUoP2gsi8a0WkL8xL0sN6WGiv695s8nVcmab9tWz20ySQbuzp0UkSxUQFi5jJHIpOQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.zh-Hant": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "VIXB7HCUC34OoaGnO3HJVtSv2/wljPhjV7eKH4+TFPgQdJj2lvHNKY41Dtg0Bphu7X5UaXFR4zrYYyo+GNOjbA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "JmesPath.Net": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "sL1LeqBm+BWSKvgZN/T470IqkXcKQXmOYsRUZU18jDZeiIBmvUfIe9m3VhiII/jOK/6WmrQ+W8Pqwz3k28WX9g==", + "dependencies": { + "JmesPath.Net.Parser": "1.1.0", + "Newtonsoft.Json": "13.0.4" + } + }, + "JmesPath.Net.Parser": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "NLTE/dPy8lMcZO6E7SL5Jw3fay8Vesll7+hkeRVSRaVNg1RRyPBV3/u6CM7QNgtnzhvyFPDyxUHUuRdh0vzCSg==" + }, + "Json.More.Net": { + "type": "Transitive", + "resolved": "3.0.1", + "contentHash": "fRctF2J2SILYG6wqP21drmeEODmCVkVQ/b3MndDu2fT1swfySyUgq7ePCk+aENGlDcIm05fyfjh9vcuqDEfv3w==" + }, + "JsonConverter.Abstractions": { + "type": "Transitive", + "resolved": "0.13.0", + "contentHash": "Ci3nuKx3GgMDfW9JA4dJpU+hJV5G1ve72mploQP8ivSDpOmo2QbfVAQkxsVzb3UQJCgwxxM6rdE/fPXwM0yj0g==" + }, + "JsonConverter.Newtonsoft.Json": { + "type": "Transitive", + "resolved": "0.13.0", + "contentHash": "K6doeW12emLiJV4laUf58y3kkjng6/IARRtC7+20qIOBrP1pBxGRsjz2IfxCgSBkTML3q6FWe7O+/UE374lsaA==", + "dependencies": { + "JsonConverter.Abstractions": "0.13.0", + "Newtonsoft.Json": "13.0.4", + "Stef.Validation": "0.1.1" + } + }, + "JsonConverter.System.Text.Json": { + "type": "Transitive", + "resolved": "0.13.0", + "contentHash": "UtRbkZT16Z0OVZ9n/h60E0GlUDTul/DjpuuajdsCNvefsmTxf6WNB8Cq9Hjwu9pGjfqOaFOmaz8k54DaHBmc0g==", + "dependencies": { + "JsonConverter.Abstractions": "0.13.0", + "Stef.Validation": "0.1.1" + } + }, + "JsonPath.Net": { + "type": "Transitive", + "resolved": "3.0.2", + "contentHash": "Cmt2mvPYOLljjqSfM1xUYZYTPf8MPbwv2XpCpPxxq9u23/CGrz/fljgd1fJUNujd3+E1adNOyF1TLwppbyQwxg==", + "dependencies": { + "Json.More.Net": "3.0.1" + } + }, + "MetadataReferenceService.Abstractions": { + "type": "Transitive", + "resolved": "0.0.1", + "contentHash": "Sf5ip58vlqWkQIAULIOKFIIFuhtRd8lChsJRZdFo746NVApEp/qgxNf/zCLjbB/RA/8TQGXWrFPKpqjyeh3EMg==", + "dependencies": { + "Microsoft.CodeAnalysis.CSharp": "4.8.0", + "Stef.Validation": "0.1.1" + } + }, + "MetadataReferenceService.Default": { + "type": "Transitive", + "resolved": "0.0.1", + "contentHash": "ihrchqYobpQMA9tn0W+MGD3oe5onqCttbR3lQfEiVzwF0V9/DS+K4YtvsUPGDC9XIie2Xw3lugSSk97k+OUwnQ==", + "dependencies": { + "MetadataReferenceService.Abstractions": "0.0.1" + } + }, "Microsoft.ApplicationInsights": { "type": "Transitive", "resolved": "2.23.0", "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" }, + "Microsoft.AspNetCore.Http": { + "type": "Transitive", + "resolved": "2.3.9", + "contentHash": "+CcfWi1LoKYbcxt+3toO4xbBG+qSSMbPuuow+cbZKIrITXuu1geN1traamL4jG8QaHdHGm3M0eCh+EOgdMgNPA==", + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "2.3.0", + "Microsoft.AspNetCore.WebUtilities": "2.3.0", + "Microsoft.Extensions.ObjectPool": "8.0.11", + "Microsoft.Extensions.Options": "8.0.2", + "Microsoft.Net.Http.Headers": "2.3.8" + } + }, + "Microsoft.AspNetCore.Http.Abstractions": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "39r9PPrjA6s0blyFv5qarckjNkaHRA5B+3b53ybuGGNTXEj1/DStQJ4NWjFL6QTRQpL9zt7nDyKxZdJOlcnq+Q==", + "dependencies": { + "Microsoft.AspNetCore.Http.Features": "2.3.0" + } + }, + "Microsoft.AspNetCore.Http.Features": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "f10WUgcsKqrkmnz6gt8HeZ7kyKjYN30PO7cSic1lPtH7paPtnQqXPOveul/SIPI43PhRD4trttg4ywnrEmmJpA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.AspNetCore.WebUtilities": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "trbXdWzoAEUVd0PE2yTopkz4kjZaAIA7xUWekd5uBw+7xE8Do/YOVTeb9d9koPTlbtZT539aESJjSLSqD8eYrQ==", + "dependencies": { + "Microsoft.Net.Http.Headers": "2.3.0" + } + }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", "resolved": "10.0.3", "contentHash": "TV62UsrJZPX6gbt3c4WrtXh7bmaDIcMqf9uft1cc4L6gJXOU07hDGEh+bFQh/L2Az0R1WVOkiT66lFqS6G2NmA==" }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.3.4", + "contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.3.4" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "+3+qfdb/aaGD8PZRCrsdobbzGs1m9u119SkkJt8e/mk3xLJz/udLtS2T6nY27OTXxBBw10HzAbC8Z9w08VyP/g==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[4.8.0]" + } + }, "Microsoft.DiaSymReader": { "type": "Transitive", "resolved": "2.2.3", @@ -402,6 +1104,11 @@ "Microsoft.Extensions.Primitives": "10.0.7" } }, + "Microsoft.Extensions.ObjectPool": { + "type": "Transitive", + "resolved": "8.0.11", + "contentHash": "6ApKcHNJigXBfZa6XlDQ8feJpq7SG1ogZXg6M4FiNzgd6irs3LUAzo0Pfn4F2ZI9liGnH1XIBR/OtSbZmJAV5w==" + }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "10.0.7", @@ -459,6 +1166,56 @@ "resolved": "8.14.0", "contentHash": "iwbCpSjD3ehfTwBhtSNEtKPK0ICun6ov7Ibx6ISNA9bfwIyzI2Siwyi9eJFCJBwxowK9xcA1mj+jBWiigeqgcQ==" }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Transitive", + "resolved": "6.34.0", + "contentHash": "CZMom/ZoWcgjxLMxmCmcEkuoA0OA4swN1CGeMBQyxF/hEZgRbWK9EnWVJ9/oMUq3D1+OGJjnbN+W6gFq9kZcEg==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "6.34.0" + } + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "6.34.0", + "contentHash": "E0AbluNkI30/VKa96PxJhhFZDx/NGYIXFrRIRq1N5/V0TToaiuc3hM90QLFszT2BBQefnp/wjm12ilSudmt9bg==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "6.34.0" + } + }, + "Microsoft.IdentityModel.Protocols": { + "type": "Transitive", + "resolved": "6.34.0", + "contentHash": "xrqYK+V3FW+fMQ5oI7cwku2wj1RHz8qym3kh+rD+BTgCw1RmfFyWrLQ8/rVEqTl2nn4NcC0N+sHk0Q4qQ8dK9A==", + "dependencies": { + "Microsoft.IdentityModel.Logging": "6.34.0", + "Microsoft.IdentityModel.Tokens": "6.34.0" + } + }, + "Microsoft.IdentityModel.Protocols.OpenIdConnect": { + "type": "Transitive", + "resolved": "6.34.0", + "contentHash": "SN3eZtssgpfnTCUlKsTJn9/0UiSc/HsbGLFl5Xp8vXFLXBeweWiDu54jFngSirjtJd6lSw3GgZhK5LZvVXGGLQ==", + "dependencies": { + "Microsoft.IdentityModel.Protocols": "6.34.0", + "System.IdentityModel.Tokens.Jwt": "6.34.0" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Transitive", + "resolved": "6.34.0", + "contentHash": "PEPcGMqbEwEwbpQ6nTld9Nqq6V5BPZSOfk71qXZ7h7DuGuxa13bWvjImhJba5Ko88YvIuZuOBJWFZmjLfwbNXA==", + "dependencies": { + "Microsoft.IdentityModel.Logging": "6.34.0" + } + }, + "Microsoft.Net.Http.Headers": { + "type": "Transitive", + "resolved": "2.3.8", + "contentHash": "JO60u/VVUdaZfv4XQ//zgcH54y8rnxdpcvXnsDqWLKB4adDKaCiaozixDfQ/6H+PKYfkNV2CL8b8U+F9mciE3Q==", + "dependencies": { + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, "Microsoft.Testing.Extensions.Telemetry": { "type": "Transitive", "resolved": "2.2.2", @@ -510,6 +1267,91 @@ "resolved": "4.2.2", "contentHash": "0VUx09Q6MdPlTCG+xTqEoXIrjr32F1Ya5EI/hfQdRSczZh61AWWtCdGXRCe3DDfUUbPVvFBZTJcrlTT1Cv25Dg==" }, + "Namotion.Reflection": { + "type": "Transitive", + "resolved": "2.1.2", + "contentHash": "7tSHAzX8GWKy0qrW6OgQWD7kAZiqzhq+m1503qczuwuK6ZYhOGCQUxw+F3F4KkRM70aB6RMslsRVSCFeouIehw==" + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.4", + "contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A==" + }, + "NJsonSchema": { + "type": "Transitive", + "resolved": "10.9.0", + "contentHash": "IBPo6Srxn2MEcIFM3HdM4QImrJbsIeujENQyzHL2Pv6wLsKSYAyAEilecRqaLOhoy3snEiPLx7hhv7opbhOxKQ==", + "dependencies": { + "Namotion.Reflection": "2.1.2", + "Newtonsoft.Json": "9.0.1" + } + }, + "NJsonSchema.Extensions": { + "type": "Transitive", + "resolved": "0.2.0", + "contentHash": "zLHUfuCmnaaQbKxqvTALrxhXV6Pbdy4G3ZlAI+7oaXdJmSyQPpMHGcxDBmw0+qHziT7jVImxU1BjcidKJHeprg==", + "dependencies": { + "NJsonSchema": "10.9.0" + } + }, + "NSwag.Core": { + "type": "Transitive", + "resolved": "13.16.1", + "contentHash": "xiX+H3Bv6zxrqJExPepO5WQVutkDUMdlUA3NqQ8VguwsYwJlkV05eF8XvmbJn/yGJWUag7vLImuXAoj0/327Bg==", + "dependencies": { + "NJsonSchema": "10.7.2", + "Newtonsoft.Json": "9.0.1" + } + }, + "OpenTelemetry": { + "type": "Transitive", + "resolved": "1.15.3", + "contentHash": "N0i6WjPoHPbZyms1ugbDIFAJFuGlpeExJMU/+XSL0lQRUkg/D0utFkDoLXf8Z1km5B+xVZ2GyMXXiX8qdeNmPg==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Configuration": "10.0.0", + "OpenTelemetry.Api.ProviderBuilderExtensions": "1.15.3" + } + }, + "OpenTelemetry.Api": { + "type": "Transitive", + "resolved": "1.15.3", + "contentHash": "fX+fkCysfPut+qCcT3bKqyX4QN9Saf4CgX8HLOHywEVD+Xr7sULtfuypITpoDysjx8R59dn/3mWhgimMH8cm/g==" + }, + "OpenTelemetry.Api.ProviderBuilderExtensions": { + "type": "Transitive", + "resolved": "1.15.3", + "contentHash": "SYn0lqYDwLMWhv/zlNGsQcl2yX++yTumanX46bmOZE/ZDOd1WjPBO2kZaZgKLEZTZk48pavIFGJ6vOvxXgWVFQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "OpenTelemetry.Api": "1.15.3" + } + }, + "OpenTelemetry.Exporter.OpenTelemetryProtocol": { + "type": "Transitive", + "resolved": "1.15.3", + "contentHash": "FEXJepcseTGbATiCkUfP7ipoFEYYfl/0UmmUwi0KxCPg9PaUA8ab2P1LGopK+/HExasJ1ZutFhZrN6WvUIR23g==", + "dependencies": { + "OpenTelemetry": "1.15.3" + } + }, + "OpenTelemetry.Extensions.Hosting": { + "type": "Transitive", + "resolved": "1.15.3", + "contentHash": "u8n/W8yIlqv0BXZmvId1iVaeWXG42tGKdTkuLYg5g57Y/r9CeUNzqtrSHNdG5IoO8iPX79w3v+WsbAHgUQbfeg==", + "dependencies": { + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "OpenTelemetry": "1.15.3" + } + }, + "OpenTelemetry.Instrumentation.AspNetCore": { + "type": "Transitive", + "resolved": "1.15.2", + "contentHash": "2nPd7r0ug/gd6/CNFL6Rlu+RSQ9WYGSGHAYQ1ssbSqyzKJpqTunfx2I/1O0WB5k+L0cyXbG4XVZpoSoUc3M7wg==", + "dependencies": { + "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.3, 2.0.0)" + } + }, "Polly": { "type": "Transitive", "resolved": "7.2.4", @@ -528,6 +1370,65 @@ "Polly": "7.1.0" } }, + "protobuf-net": { + "type": "Transitive", + "resolved": "3.2.52", + "contentHash": "XbZurNU3B/VaL/5OJ0kshO+AWxsZroI1saKuLfZpDwH2ngb2K9bdF1nIW6elFOViZw7TQCmfVZapxrMKCDqecQ==", + "dependencies": { + "protobuf-net.Core": "3.2.52" + } + }, + "protobuf-net.Core": { + "type": "Transitive", + "resolved": "3.2.52", + "contentHash": "zOpGtUo2QTgbsiI0D0yCe8aUTgDPov6kqIu1CDHI6isqhYcAHdirRrdnfsQXmAUfAWx1LwVYGgC6xe6fNS4UAg==" + }, + "ProtoBufJsonConverter": { + "type": "Transitive", + "resolved": "0.11.0", + "contentHash": "lxvcZQlCtgYZpfm9hhAJVZ1jsPkb9g3fyaAOnQLyEu8MiAowEIdED6jzwfjqLqOhY4AahqnbfRvZ904Ud43X7w==", + "dependencies": { + "MetadataReferenceService.Default": "0.0.1", + "Microsoft.CodeAnalysis.CSharp": "4.8.0", + "Newtonsoft.Json": "13.0.3", + "Stef.Validation": "0.1.1", + "protobuf-net": "3.2.52" + } + }, + "RamlToOpenApiConverter.SourceOnly": { + "type": "Transitive", + "resolved": "0.11.0", + "contentHash": "uYuENc27+ly789oJITPXGDEjvq/gzXrIZ0STCaN2Gkg6rmYTLPkcQyqXuia6HFzMaNBDEI1+WT5V+G4mYYq6bQ==" + }, + "RandomDataGenerator.Net": { + "type": "Transitive", + "resolved": "1.0.19.1", + "contentHash": "OkAqBA69VbYYg+biX2DYWcucOI/yEivkdJ/XPqife/mQAC0r/NArcAU/EI3i/1oRFUUCjibV0b7qEjK+TYTqDw==", + "dependencies": { + "Fare": "2.2.1", + "Stef.Validation": "0.1.1" + } + }, + "Scriban.Signed": { + "type": "Transitive", + "resolved": "7.2.0", + "contentHash": "AGI7QURe/UR9lsfM53yylQngbe77PngBnpzZQ+QV3wcevP4cydyCT+YAMR4rntWEle7v++xUjmfmtXuAvp6Rtw==" + }, + "SharpYaml": { + "type": "Transitive", + "resolved": "2.1.3", + "contentHash": "qd6kDo2uZPG/AvQ3NS2CXr7fDtP+rjiNQ81No3mFGpn41DcG0FEklgnj/j5hVhgQ6YTgTYbvyzkdDnvT+RtuLQ==" + }, + "SimMetrics.Net": { + "type": "Transitive", + "resolved": "1.0.5", + "contentHash": "LaSDYOJDh2WncgRboqiWtk/Igqoim/LV7v808qBeWY/f36Ol5oEKguEYpKrWw5ap8KYP0SRXf7/v3zil9koY6Q==" + }, + "Stef.Validation": { + "type": "Transitive", + "resolved": "0.3.0", + "contentHash": "OfzmxQMK4eBzmobph43p1NsLTgVAC3XGTcvQS0odhsdL6uS7UsFzBMK6S9mAfIijZdLW4q98aZ3dtTOeTaQo6Q==" + }, "System.ClientModel": { "type": "Transitive", "resolved": "1.10.0", @@ -549,6 +1450,15 @@ "resolved": "10.0.7", "contentHash": "WbmDLeTPYhEzXhvYVioTVn/D1XX6bovyny9n5p8Zxtf03+eY385RB818teZm6n+fA63iZNvng0/Np4tLuhkMhQ==" }, + "System.IdentityModel.Tokens.Jwt": { + "type": "Transitive", + "resolved": "6.34.0", + "contentHash": "c0misfmFT3QxKY+a16PGlj+DtiUzoPaf26m2avyPZaLRc9vlIdLtmovfRY5MqN+y/SEoBSRXrgVaeZGPgFQQ6w==", + "dependencies": { + "Microsoft.IdentityModel.JsonWebTokens": "6.34.0", + "Microsoft.IdentityModel.Tokens": "6.34.0" + } + }, "System.Interactive.Async": { "type": "Transitive", "resolved": "7.0.1", @@ -577,6 +1487,139 @@ "resolved": "4.5.0", "contentHash": "wLBKzFnDCxP12VL9ANydSYhk59fC4cvOr9ypYQLPnAj48NQIhqnjdD2yhP8yEKyBJEjERWS9DisKL7rX5eU25Q==" }, + "TinyMapper.Signed": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "W5uc9QXp8PUgP3VQ1Qyt3vK8ptyjj38tJ7nEAtRKA6R/4e6+2gsgYrAmRg9fCK1hhe3E0yeAm5acC14qx2CINg==" + }, + "WireMock.Net.Abstractions": { + "type": "Transitive", + "resolved": "2.11.0", + "contentHash": "/iy5Jn1QHW1CDmyVj6e+/rhUQpuNHk5Y6Qy6MQSFv48R86YovqZ2Au0VwtL2nVvwbRY0TeMH3XkhARHcynRUPA==" + }, + "WireMock.Net.GraphQL": { + "type": "Transitive", + "resolved": "2.11.0", + "contentHash": "QVfnWZrjI9P9v3pOzfDByZ0IN4JmV91jo+H6jz5heJ2dWPFFmmqLpxIc2Y2ExuJGYQAJmvNYAMC3/2YSXK7+wQ==", + "dependencies": { + "GraphQL.NewtonsoftJson": "8.5.0", + "WireMock.Net.Shared": "2.11.0" + } + }, + "WireMock.Net.Matchers.SystemTextJsonPath": { + "type": "Transitive", + "resolved": "2.11.0", + "contentHash": "B8qofF1bdEgtwHRgswtOJkZxHmLmmf8RIH4Jk6KJSHn4QKwwMrtPdEOD632/03P8n93O2K2mfd8PDsovLQ7gcQ==", + "dependencies": { + "JsonPath.Net": "3.0.2", + "WireMock.Net.Shared": "2.11.0" + } + }, + "WireMock.Net.MimePart": { + "type": "Transitive", + "resolved": "2.11.0", + "contentHash": "+aCR6r3cx+ZvS0hhu5CUjNAstub5Sy1JmqFC7q3QQK82yazlH7OB4Dzh1rTJH+BXcxoWKC91nqXgt6xfD8Uv8g==", + "dependencies": { + "Stef.Validation": "0.3.0", + "WireMock.Net.Shared": "2.11.0" + } + }, + "WireMock.Net.Minimal": { + "type": "Transitive", + "resolved": "2.11.0", + "contentHash": "NmQrlaNiZRx535OFUJYCG3+HZRXEi0rom4mN+09qas6RVaZgnh+ChxnWFIdOS8rXoTRlpDmFHG1fdUVHHX4mhA==", + "dependencies": { + "JmesPath.Net": "1.1.0", + "Microsoft.IdentityModel.Protocols.OpenIdConnect": "6.34.0", + "NJsonSchema.Extensions": "0.2.0", + "NSwag.Core": "13.16.1", + "Scriban.Signed": "7.2.0", + "SimMetrics.Net": "1.0.5", + "TinyMapper.Signed": "4.0.0", + "WireMock.Net.OpenApiParser": "2.11.0", + "WireMock.Net.Shared": "2.11.0", + "WireMock.Org.Abstractions": "2.11.0" + } + }, + "WireMock.Net.OpenApiParser": { + "type": "Transitive", + "resolved": "2.11.0", + "contentHash": "b3awrEjgWG6iPCT2wO5Jdye6JaGlCeGoCrgtaeucP0GVYn/BmeNGqKU6BGpIsx7J0+W6YBj0QgpNQp7/nRgeaw==", + "dependencies": { + "Newtonsoft.Json": "13.0.4", + "RamlToOpenApiConverter.SourceOnly": "0.11.0", + "RandomDataGenerator.Net": "1.0.19.1", + "SharpYaml": "2.1.3", + "Stef.Validation": "0.3.0", + "WireMock.Net.Abstractions": "2.11.0", + "YamlDotNet": "16.3.0" + } + }, + "WireMock.Net.OpenTelemetry": { + "type": "Transitive", + "resolved": "2.11.0", + "contentHash": "T+ShaY7mI82RhQmCUrs5n5ZX67xlTyIfC1/gNiftca/wiaXCctj+b8gt7tr7z+LuKroTnMRQDuOFdHhBKVjUQQ==", + "dependencies": { + "OpenTelemetry.Exporter.OpenTelemetryProtocol": "1.15.3", + "OpenTelemetry.Extensions.Hosting": "1.15.3", + "OpenTelemetry.Instrumentation.AspNetCore": "1.15.2", + "WireMock.Net.Shared": "2.11.0" + } + }, + "WireMock.Net.ProtoBuf": { + "type": "Transitive", + "resolved": "2.11.0", + "contentHash": "cWJTxP4oG9PZoOQDTzRNbuJX8mY/MtmjMCiwnSx2h6Vm33HxHFngdmHdgK4ZjbZfnqq6gKIXPel+DvEExXa6lQ==", + "dependencies": { + "ProtoBufJsonConverter": "0.11.0", + "WireMock.Net.Shared": "2.11.0" + } + }, + "WireMock.Net.Shared": { + "type": "Transitive", + "resolved": "2.11.0", + "contentHash": "WvVfCUHP6noNmNdpCWuWeBok44QFjgwQITGoTiMd0v40fCB85SKJU/1uocP8ZgRABELBMbbLALsIuz31KdeNcA==", + "dependencies": { + "AnyOf": "0.5.0.1", + "Handlebars.Net.Helpers": "2.5.5", + "Handlebars.Net.Helpers.Humanizer": "2.5.5", + "Handlebars.Net.Helpers.Json": "2.5.5", + "Handlebars.Net.Helpers.Random": "2.5.5", + "Handlebars.Net.Helpers.XPath": "2.5.5", + "Handlebars.Net.Helpers.Xeger": "2.5.5", + "Handlebars.Net.Helpers.Xslt": "2.5.5", + "JsonConverter.Newtonsoft.Json": "0.13.0", + "JsonConverter.System.Text.Json": "0.13.0", + "Microsoft.AspNetCore.Http": "2.3.9", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", + "Stef.Validation": "0.3.0", + "WireMock.Net.Abstractions": "2.11.0" + } + }, + "WireMock.Org.Abstractions": { + "type": "Transitive", + "resolved": "2.11.0", + "contentHash": "ERlcOKHCgOblIaph2QZYpj9wrIwHRirwHFRqzfo4qDUVxH3QB8PoxuVEaL6LGFUM9gKBilrdvs4ulsxKiwjqwQ==" + }, + "XPath2": { + "type": "Transitive", + "resolved": "1.1.5", + "contentHash": "LQg7kZyAmmb+qvv5TiOuuijxN97rRbR05qbMkVIH+i+sx9CA2UNUKGNtdVxWEXOabS8BIwlXm6ox1OOTjvZ6jw==" + }, + "XPath2.Extensions": { + "type": "Transitive", + "resolved": "1.1.5", + "contentHash": "oEbdGUJsF25QL3Vj1GgSlT2xdbxnka5dKcjuA9CouWCV/l9ecSfypOv78B1+YUD8a8w47prLNw2i3ofLNcrbGA==", + "dependencies": { + "Newtonsoft.Json": "13.0.3", + "XPath2": "1.1.5" + } + }, + "YamlDotNet": { + "type": "Transitive", + "resolved": "16.3.0", + "contentHash": "SgMOdxbz8X65z8hraIs6hOEdnkH6hESTAIUa7viEngHOYaH+6q5XJmwr1+yb9vJpNQ19hCQY69xbFsLtXpobQA==" + }, "PackageUploader": { "type": "Project", "dependencies": { From 0c176dcb45e8a2418e1b044c94cefc775230048a Mon Sep 17 00:00:00 2001 From: Elshiekh Ahmed Date: Wed, 24 Jun 2026 10:16:15 -0700 Subject: [PATCH 02/11] Add WireMock.Net Ingestion API mock fixture (Task 2/6 Phase 1) Add IngestionMockServer, a reusable WireMock.Net-backed fake of the Partner Center Ingestion API with fluent stubs for the endpoints PackageUploader uses (GetProduct, GetBranches, CreatePackage, package processing poll, package configuration, CreateSubmission, submission poll) and configurable success/error/retry/polling scenarios. Polling and retry use WireMock stateful scenarios with self-looping terminal states. --- .../Mocks/IngestionMockServer.cs | 275 ++++++++++++++++++ .../Infrastructure/Mocks/ResponseScenario.cs | 20 ++ 2 files changed, 295 insertions(+) create mode 100644 src/PackageUploader.IntegrationTest/Infrastructure/Mocks/IngestionMockServer.cs create mode 100644 src/PackageUploader.IntegrationTest/Infrastructure/Mocks/ResponseScenario.cs diff --git a/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/IngestionMockServer.cs b/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/IngestionMockServer.cs new file mode 100644 index 00000000..56ca8507 --- /dev/null +++ b/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/IngestionMockServer.cs @@ -0,0 +1,275 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using WireMock.Server; + +namespace PackageUploader.IntegrationTest.Infrastructure.Mocks; + +/// +/// A reusable WireMock.Net-backed fake of the Partner Center Ingestion API. Starts a real HTTP +/// server on a random local port and exposes fluent stubs for the endpoints PackageUploader uses, +/// with configurable success / error / retry / polling scenarios. Dispose to stop the server. +/// +internal sealed class IngestionMockServer : IDisposable +{ + private readonly WireMockServer _server; + private readonly string _id = Guid.NewGuid().ToString("N"); + + public IngestionMockServer() => _server = WireMockServer.Start(); + + /// Base URL of the running mock server (e.g. http://localhost:12345). + public string Url => _server.Url!; + + /// The underlying server, for advanced stubbing or request-log assertions. + public WireMockServer Server => _server; + + // ---- GetProduct: GET /products/{id} ---- + + public IngestionMockServer StubGetProduct(string productId, ResponseScenario scenario = ResponseScenario.Success) + { + var request = Request.Create().WithPath($"/products/{productId}").UsingGet(); + if (scenario != ResponseScenario.Success) + { + _server.Given(request).RespondWith(ErrorResponse(scenario)); + return this; + } + + _server.Given(request).RespondWith(JsonResponse(new + { + resourceType = "AzureGameProduct", + name = $"Test Product {productId}", + id = productId, + externalIds = new[] { new { type = "StoreId", value = "9TESTBIGID000" } }, + isModularPublishing = true, + })); + return this; + } + + // ---- GetBranches: GET /products/{id}/branches/getByModule(module=Package) ---- + + public IngestionMockServer StubGetPackageBranches( + string productId, + params (string FriendlyName, string CurrentDraftInstanceId)[] branches) + { + var values = branches.Select(b => new + { + resourceType = "Branch", + friendlyName = b.FriendlyName, + type = "Main", + module = "Package", + currentDraftInstanceId = b.CurrentDraftInstanceId, + }); + + _server + .Given(Request.Create().WithPath("/products/" + productId + "/branches/getByModule*").UsingGet()) + .RespondWith(JsonResponse(new { value = values })); + return this; + } + + // ---- CreatePackageRequest: POST /products/{id}/packages ---- + + public IngestionMockServer StubCreatePackage(string productId, string packageId, string fileName = "test.xvc") + { + _server + .Given(Request.Create().WithPath($"/products/{productId}/packages").UsingPost()) + .RespondWith(JsonResponse(new + { + resourceType = "GamePackage", + id = packageId, + state = "PendingUpload", + fileName, + uploadInfo = new { }, + })); + return this; + } + + // ---- GetPackage processing poll: GET /products/{id}/packages/{packageId} ---- + // Returns each state in order across successive calls, then stays on the final state. + + public IngestionMockServer StubGetPackageProcessing( + string productId, + string packageId, + params string[] stateProgression) + { + var states = stateProgression.Length > 0 ? stateProgression : ["Processed"]; + var request = Request.Create().WithPath($"/products/{productId}/packages/{packageId}").UsingGet(); + var scenario = $"package-{productId}-{packageId}-{_id}"; + + for (int i = 0; i < states.Length; i++) + { + bool isLast = i == states.Length - 1; + var body = JsonResponse(new + { + resourceType = "GamePackage", + id = packageId, + state = states[i], + }); + + var builder = _server.Given(request).InScenario(scenario); + if (i > 0) + { + builder = builder.WhenStateIs(StateName(i)); + } + if (!isLast) + { + builder = builder.WillSetStateTo(StateName(i + 1)); + } + else if (i > 0) + { + builder = builder.WillSetStateTo(StateName(i)); + } + + builder.RespondWith(body); + } + + return this; + } + + // ---- GetPackageConfig: GET /products/{id}/packageConfigurations/getByInstanceID(...) and GET/PUT by id ---- + + public IngestionMockServer StubPackageConfiguration(string productId, string instanceId, string configId) + { + var body = JsonResponse(new + { + value = new[] { new { resourceType = "PackageConfiguration", id = configId } }, + }); + + _server + .Given(Request.Create().WithPath("/products/" + productId + "/packageConfigurations/getByInstanceID*").UsingGet()) + .RespondWith(body); + + var single = JsonResponse(new { resourceType = "PackageConfiguration", id = configId }); + _server + .Given(Request.Create().WithPath($"/products/{productId}/packageConfigurations/{configId}").UsingGet()) + .RespondWith(single); + _server + .Given(Request.Create().WithPath($"/products/{productId}/packageConfigurations/{configId}").UsingPut()) + .RespondWith(single); + return this; + } + + // ---- CreateSubmission: POST /products/{id}/submissions ---- + + public IngestionMockServer StubCreateSubmission(string productId, string submissionId) + { + _server + .Given(Request.Create().WithPath($"/products/{productId}/submissions").UsingPost()) + .RespondWith(JsonResponse(new + { + resourceType = "Submission", + id = submissionId, + state = "InProgress", + substate = "Submitted", + })); + return this; + } + + // ---- GetSubmission poll: GET /products/{id}/submissions/{id} ---- + // Returns each (state, substate) in order across successive calls, then stays on the final one. + + public IngestionMockServer StubGetSubmission( + string productId, + string submissionId, + params (string State, string Substate)[] progression) + { + var steps = progression.Length > 0 ? progression : [("Published", "InStore")]; + var request = Request.Create().WithPath($"/products/{productId}/submissions/{submissionId}").UsingGet(); + var scenario = $"submission-{productId}-{submissionId}-{_id}"; + + for (int i = 0; i < steps.Length; i++) + { + bool isLast = i == steps.Length - 1; + var body = JsonResponse(new + { + resourceType = "Submission", + id = submissionId, + state = steps[i].State, + substate = steps[i].Substate, + }); + + var builder = _server.Given(request).InScenario(scenario); + if (i > 0) + { + builder = builder.WhenStateIs(StateName(i)); + } + if (!isLast) + { + builder = builder.WillSetStateTo(StateName(i + 1)); + } + else if (i > 0) + { + builder = builder.WillSetStateTo(StateName(i)); + } + + builder.RespondWith(body); + } + + return this; + } + + // ---- Generic scenario primitives ---- + + /// Always responds to the given method/path-wildcard with the supplied status code. + public IngestionMockServer StubError(string method, string pathWildcard, HttpStatusCode statusCode) + { + _server + .Given(Request.Create().WithPath(pathWildcard).UsingMethod(method)) + .RespondWith(Response.Create().WithStatusCode((int)statusCode)); + return this; + } + + /// + /// Fails the first calls with , then + /// succeeds with the given JSON body — used to exercise the client's Polly retry policy. + /// + public IngestionMockServer StubRetryThenSuccess( + string method, + string path, + object successBody, + int failures = 2, + HttpStatusCode failureStatus = HttpStatusCode.InternalServerError) + { + var request = Request.Create().WithPath(path).UsingMethod(method); + var scenario = $"retry-{method}-{path}-{_id}"; + + for (int i = 0; i < failures; i++) + { + var builder = _server.Given(request).InScenario(scenario); + if (i > 0) + { + builder = builder.WhenStateIs(StateName(i)); + } + builder.WillSetStateTo(StateName(i + 1)) + .RespondWith(Response.Create().WithStatusCode((int)failureStatus)); + } + + _server.Given(request).InScenario(scenario) + .WhenStateIs(StateName(failures)) + .RespondWith(JsonResponse(successBody)); + return this; + } + + public void Dispose() => _server.Stop(); + + // ---- helpers ---- + + private static string StateName(int index) => $"step-{index}"; + + private static IResponseBuilder JsonResponse(object body) => + Response.Create() + .WithStatusCode(200) + .WithHeader("Content-Type", "application/json") + .WithBodyAsJson(body); + + private static IResponseBuilder ErrorResponse(ResponseScenario scenario) => + Response.Create().WithStatusCode((int)scenario switch + { + _ when scenario == ResponseScenario.ServerError => HttpStatusCode.InternalServerError, + _ when scenario == ResponseScenario.Unauthorized => HttpStatusCode.Unauthorized, + _ when scenario == ResponseScenario.NotFound => HttpStatusCode.NotFound, + _ => HttpStatusCode.InternalServerError, + }); +} diff --git a/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/ResponseScenario.cs b/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/ResponseScenario.cs new file mode 100644 index 00000000..4aaf2f24 --- /dev/null +++ b/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/ResponseScenario.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PackageUploader.IntegrationTest.Infrastructure.Mocks; + +/// How a stubbed endpoint should behave for a single, non-stateful response. +internal enum ResponseScenario +{ + /// Return a normal 2xx response with a valid body. + Success, + + /// Return a 500 Internal Server Error. + ServerError, + + /// Return a 401 Unauthorized. + Unauthorized, + + /// Return a 404 Not Found. + NotFound, +} From c4ceca209001d21d7b6e23f7a28c4103645191d1 Mon Sep 17 00:00:00 2001 From: Elshiekh Ahmed Date: Wed, 24 Jun 2026 11:21:36 -0700 Subject: [PATCH 03/11] Add WireMock.Net XFUS mock fixture (Task 2/6 Phase 2) Add XfusMockServer, a reusable WireMock.Net-backed fake of the XFUS upload service serving the three-step chunked upload (initialize, block payload PUT, continue) rooted at /api/v2/assets/. Omits directUploadParameters.sasUri so the client uses the proxy PUT path, and emits the upload status as a number to match the client serializer. Provides a no-delta success convenience plus initialize/block/continue primitives and stateful continue-progression and block-retry scenarios. --- .../Infrastructure/Mocks/XfusMockServer.cs | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 src/PackageUploader.IntegrationTest/Infrastructure/Mocks/XfusMockServer.cs diff --git a/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/XfusMockServer.cs b/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/XfusMockServer.cs new file mode 100644 index 00000000..7d9ca028 --- /dev/null +++ b/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/XfusMockServer.cs @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using WireMock.Server; + +namespace PackageUploader.IntegrationTest.Infrastructure.Mocks; + +/// +/// A reusable WireMock.Net-backed fake of the XFUS upload service. Serves the three-step chunked +/// upload conversation PackageUploader performs — initialize, per-block payload PUT, and continue — +/// for both the no-delta and delta upload paths, with configurable success / error / retry +/// scenarios. Dispose to stop the server. +/// +/// +/// The real client's base address is {UploadDomain}/api/v2/assets/, so all stubs are rooted +/// at /api/v2/assets/. Responses omit directUploadParameters.sasUri so the client +/// uploads blocks via the proxy PUT path (which this server serves) rather than to Azure blob +/// storage. The status field is emitted as a number because the client's serializer has no +/// string-enum converter (ReceivingBlocks=0, Busy=1, Completed=2). +/// +internal sealed class XfusMockServer : IDisposable +{ + private const string AssetsRoot = "/api/v2/assets"; + + private readonly WireMockServer _server; + private readonly string _id = Guid.NewGuid().ToString("N"); + + public XfusMockServer() => _server = WireMockServer.Start(); + + /// Base URL of the running mock server (e.g. http://localhost:12345). + public string Url => _server.Url!; + + /// The underlying server, for advanced stubbing or request-log assertions. + public WireMockServer Server => _server; + + /// + /// Configures a complete, successful no-delta upload: initialize returns the given blocks in the + /// ReceivingBlocks state, every block payload PUT succeeds, and continue reports Completed. + /// + public XfusMockServer StubNoDeltaUploadSuccess(params long[] blockSizes) + { + var sizes = blockSizes.Length > 0 ? blockSizes : [64L * 1024]; + StubInitialize(UploadProgress(sizes, XfusStatus.ReceivingBlocks)); + StubBlockUpload(); + StubContinue(UploadProgress([], XfusStatus.Completed)); + return this; + } + + /// Stubs POST .../{assetId}/initialize with the given JSON upload-progress body. + public XfusMockServer StubInitialize(object uploadProgressBody) + { + _server + .Given(Request.Create().WithPath($"{AssetsRoot}/*/initialize").UsingPost()) + .RespondWith(JsonResponse(uploadProgressBody)); + return this; + } + + /// Stubs PUT .../{assetId}/blocks/{blockId}/source/payload to accept block bytes. + public XfusMockServer StubBlockUpload(HttpStatusCode statusCode = HttpStatusCode.OK) + { + _server + .Given(Request.Create().WithPath($"{AssetsRoot}/*/blocks/*/source/payload").UsingPut()) + .RespondWith(Response.Create().WithStatusCode((int)statusCode)); + return this; + } + + /// Stubs POST .../{assetId}/continue with the given JSON upload-progress body. + public XfusMockServer StubContinue(object uploadProgressBody) + { + _server + .Given(Request.Create().WithPath($"{AssetsRoot}/*/continue").UsingPost()) + .RespondWith(JsonResponse(uploadProgressBody)); + return this; + } + + /// + /// Returns the given continue-progress bodies in order across successive calls, then stays on the + /// final one — used to simulate the server working through the upload (e.g. Busy then Completed) + /// or the multi-step delta plan. + /// + public XfusMockServer StubContinueProgression(params object[] uploadProgressBodies) + { + var bodies = uploadProgressBodies.Length > 0 ? uploadProgressBodies : [UploadProgress([], XfusStatus.Completed)]; + var request = Request.Create().WithPath($"{AssetsRoot}/*/continue").UsingPost(); + var scenario = $"xfus-continue-{_id}"; + + for (int i = 0; i < bodies.Length; i++) + { + bool isLast = i == bodies.Length - 1; + var builder = _server.Given(request).InScenario(scenario); + if (i > 0) + { + builder = builder.WhenStateIs(StateName(i)); + } + if (!isLast) + { + builder = builder.WillSetStateTo(StateName(i + 1)); + } + else if (i > 0) + { + builder = builder.WillSetStateTo(StateName(i)); + } + builder.RespondWith(JsonResponse(bodies[i])); + } + + return this; + } + + /// Always responds to the given method/path-wildcard with the supplied status code. + public XfusMockServer StubError(string method, string pathWildcard, HttpStatusCode statusCode) + { + _server + .Given(Request.Create().WithPath(pathWildcard).UsingMethod(method)) + .RespondWith(Response.Create().WithStatusCode((int)statusCode)); + return this; + } + + /// + /// Fails the first block payload PUTs with , + /// then accepts subsequent ones — used to exercise the uploader's block re-upload behaviour. + /// + public XfusMockServer StubBlockUploadRetryThenSuccess(int failures = 1, HttpStatusCode failureStatus = HttpStatusCode.ServiceUnavailable) + { + var request = Request.Create().WithPath($"{AssetsRoot}/*/blocks/*/source/payload").UsingPut(); + var scenario = $"xfus-block-{_id}"; + + for (int i = 0; i < failures; i++) + { + var builder = _server.Given(request).InScenario(scenario); + if (i > 0) + { + builder = builder.WhenStateIs(StateName(i)); + } + builder.WillSetStateTo(StateName(i + 1)) + .RespondWith(Response.Create().WithStatusCode((int)failureStatus)); + } + + _server.Given(request).InScenario(scenario) + .WhenStateIs(StateName(failures)) + .WillSetStateTo(StateName(failures)) + .RespondWith(Response.Create().WithStatusCode((int)HttpStatusCode.OK)); + return this; + } + + public void Dispose() => _server.Stop(); + + // ---- helpers ---- + + private enum XfusStatus + { + ReceivingBlocks = 0, + Busy = 1, + Completed = 2, + } + + private static object UploadProgress(long[] blockSizes, XfusStatus status) + { + long offset = 0; + var blocks = new List(); + for (long i = 0; i < blockSizes.Length; i++) + { + blocks.Add(new + { + id = i, + blockIdBase64 = Convert.ToBase64String(BitConverter.GetBytes(i)), + offset, + size = blockSizes[i], + }); + offset += blockSizes[i]; + } + + return new + { + pendingBlocks = blocks, + status = (int)status, + }; + } + + private static string StateName(int index) => $"step-{index}"; + + private static IResponseBuilder JsonResponse(object body) => + Response.Create() + .WithStatusCode(200) + .WithHeader("Content-Type", "application/json") + .WithBodyAsJson(body); +} From 7da05afdae62a1e9a699060e0a6347f4d078d4cd Mon Sep 17 00:00:00 2001 From: Elshiekh Ahmed Date: Mon, 29 Jun 2026 08:51:03 -0700 Subject: [PATCH 04/11] Wire WireMock mock servers into a mock-server test host (Task 2/6 Phase 3) Add MockServerTestHost, which owns the Ingestion and XFUS WireMock fakes and composes the real IPackageUploaderService pointed at them (Ingestion via IngestionConfig:BaseAddress, XFUS via the upload domain returned in the package response), keeping FakeAccessTokenProvider. Extend IngestionMockServer.StubCreatePackage to embed the XFUS upload info, and add IntegrationTestBase.CreateMockServerHost(). --- .../Infrastructure/IntegrationTestBase.cs | 3 + .../Infrastructure/MockServerTestHost.cs | 74 +++++++++++++++++++ .../Mocks/IngestionMockServer.cs | 24 +++++- 3 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 src/PackageUploader.IntegrationTest/Infrastructure/MockServerTestHost.cs diff --git a/src/PackageUploader.IntegrationTest/Infrastructure/IntegrationTestBase.cs b/src/PackageUploader.IntegrationTest/Infrastructure/IntegrationTestBase.cs index bdfa8573..05082f95 100644 --- a/src/PackageUploader.IntegrationTest/Infrastructure/IntegrationTestBase.cs +++ b/src/PackageUploader.IntegrationTest/Infrastructure/IntegrationTestBase.cs @@ -13,4 +13,7 @@ public abstract class IntegrationTestBase private protected static PackageUploaderTestHost CreateHost( Action? configureIngestion = null) => new(configureIngestion); + + /// Creates a host wired to live WireMock.Net fakes of the Ingestion API and XFUS. + private protected static MockServerTestHost CreateMockServerHost() => new(); } diff --git a/src/PackageUploader.IntegrationTest/Infrastructure/MockServerTestHost.cs b/src/PackageUploader.IntegrationTest/Infrastructure/MockServerTestHost.cs new file mode 100644 index 00000000..d59964c5 --- /dev/null +++ b/src/PackageUploader.IntegrationTest/Infrastructure/MockServerTestHost.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using PackageUploader.ClientApi; +using PackageUploader.ClientApi.Client.Ingestion.TokenProvider; +using PackageUploader.IntegrationTest.Infrastructure.Mocks; + +namespace PackageUploader.IntegrationTest.Infrastructure; + +/// +/// Composes the real wired to live WireMock.Net fakes of the +/// Ingestion API and XFUS. The Ingestion base address points at the server; +/// XFUS is reached via the upload domain returned in a stubbed package response (see +/// and ). Authentication uses +/// . Everything else (auth handler, Polly policies, +/// serialization, mappers) runs for real. Dispose to stop both servers and the provider. +/// +internal sealed class MockServerTestHost : IDisposable +{ + private readonly ServiceProvider _provider; + private readonly IServiceScope _scope; + + /// The fake Ingestion API. Configure stubs before exercising the service. + public IngestionMockServer Ingestion { get; } + + /// The fake XFUS upload service. Configure stubs before exercising the service. + public XfusMockServer Xfus { get; } + + /// The fully composed, public service under test, wired to the fakes. + public IPackageUploaderService Service { get; } + + public MockServerTestHost() + { + Ingestion = new IngestionMockServer(); + Xfus = new XfusMockServer(); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + // Trailing slash so the client's relative paths (e.g. "products/{id}") resolve under it. + ["IngestionConfig:BaseAddress"] = $"{Ingestion.Url}/", + }) + .Build(); + + var services = new ServiceCollection(); + services.AddSingleton(configuration); + services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); + + services.AddPackageUploaderService(IngestionExtensions.AuthenticationMethod.Default); + + services.RemoveAll(); + services.AddScoped(); + + _provider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); + _scope = _provider.CreateScope(); + Service = _scope.ServiceProvider.GetRequiredService(); + } + + /// The XFUS server's base URL, for use as the upload domain in a stubbed package response. + public string XfusUploadDomain => Xfus.Url; + + public void Dispose() + { + _scope.Dispose(); + _provider.Dispose(); + Ingestion.Dispose(); + Xfus.Dispose(); + } +} diff --git a/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/IngestionMockServer.cs b/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/IngestionMockServer.cs index 56ca8507..50a5a250 100644 --- a/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/IngestionMockServer.cs +++ b/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/IngestionMockServer.cs @@ -71,8 +71,28 @@ public IngestionMockServer StubGetPackageBranches( // ---- CreatePackageRequest: POST /products/{id}/packages ---- - public IngestionMockServer StubCreatePackage(string productId, string packageId, string fileName = "test.xvc") + public IngestionMockServer StubCreatePackage( + string productId, + string packageId, + string fileName = "test.xvc", + string? xfusUploadDomain = null, + string? xfusId = null, + string xfusTenant = "DCE", + string xfusToken = "fake-xfus-token") { + // When an XFUS upload domain is supplied, embed the upload info so the client uploads to the + // XFUS mock; xfusId must be a valid GUID because the client maps it to a Guid. + object uploadInfo = xfusUploadDomain is null + ? new { } + : new + { + fileName, + xfusId = xfusId ?? Guid.NewGuid().ToString(), + token = xfusToken, + uploadDomain = xfusUploadDomain, + xfusTenant, + }; + _server .Given(Request.Create().WithPath($"/products/{productId}/packages").UsingPost()) .RespondWith(JsonResponse(new @@ -81,7 +101,7 @@ public IngestionMockServer StubCreatePackage(string productId, string packageId, id = packageId, state = "PendingUpload", fileName, - uploadInfo = new { }, + uploadInfo, })); return this; } From 08ec09e2284ab82c17018353a132ff3ddf5bcfa9 Mon Sep 17 00:00:00 2001 From: Elshiekh Ahmed Date: Mon, 29 Jun 2026 08:55:49 -0700 Subject: [PATCH 05/11] Consolidate integration test hosts onto the WireMock mock-server host Remove the superseded in-process PackageUploaderTestHost and MockHttpMessageHandler, migrate the smoke test to CreateMockServerHost() (real HTTP to the WireMock Ingestion fake), and drop the CreateHost overload. MockServerTestHost is now the single canonical integration test host. --- .../Infrastructure/IntegrationTestBase.cs | 3 - .../Infrastructure/MockHttpMessageHandler.cs | 109 ------------------ .../Infrastructure/PackageUploaderTestHost.cs | 64 ---------- .../SmokeTest.cs | 24 ++-- 4 files changed, 14 insertions(+), 186 deletions(-) delete mode 100644 src/PackageUploader.IntegrationTest/Infrastructure/MockHttpMessageHandler.cs delete mode 100644 src/PackageUploader.IntegrationTest/Infrastructure/PackageUploaderTestHost.cs diff --git a/src/PackageUploader.IntegrationTest/Infrastructure/IntegrationTestBase.cs b/src/PackageUploader.IntegrationTest/Infrastructure/IntegrationTestBase.cs index 05082f95..bbeaaf90 100644 --- a/src/PackageUploader.IntegrationTest/Infrastructure/IntegrationTestBase.cs +++ b/src/PackageUploader.IntegrationTest/Infrastructure/IntegrationTestBase.cs @@ -11,9 +11,6 @@ public abstract class IntegrationTestBase { public const string Category = "Integration"; - private protected static PackageUploaderTestHost CreateHost( - Action? configureIngestion = null) => new(configureIngestion); - /// Creates a host wired to live WireMock.Net fakes of the Ingestion API and XFUS. private protected static MockServerTestHost CreateMockServerHost() => new(); } diff --git a/src/PackageUploader.IntegrationTest/Infrastructure/MockHttpMessageHandler.cs b/src/PackageUploader.IntegrationTest/Infrastructure/MockHttpMessageHandler.cs deleted file mode 100644 index 18defbf6..00000000 --- a/src/PackageUploader.IntegrationTest/Infrastructure/MockHttpMessageHandler.cs +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; - -namespace PackageUploader.IntegrationTest.Infrastructure; - -/// In-process HTTP handler that returns scripted responses and records requests, backing the mock integration suite. -internal sealed class MockHttpMessageHandler : HttpMessageHandler -{ - private readonly List _responders = []; - private readonly List _received = []; - private readonly Lock _receivedLock = new(); - - public IReadOnlyList ReceivedRequests - { - get - { - lock (_receivedLock) - { - return _received.ToArray(); - } - } - } - - public MockHttpMessageHandler When(HttpMethod method, string pathContains, - Func respond) - { - ArgumentNullException.ThrowIfNull(method); - ArgumentNullException.ThrowIfNull(pathContains); - ArgumentNullException.ThrowIfNull(respond); - - _responders.Add(new Responder(method, pathContains, respond)); - return this; - } - - public MockHttpMessageHandler WhenJson(HttpMethod method, string pathContains, string json, - HttpStatusCode status = HttpStatusCode.OK) => - When(method, pathContains, _ => new HttpResponseMessage(status) - { - Content = new StringContent(json, Encoding.UTF8, "application/json"), - }); - - protected override async Task SendAsync(HttpRequestMessage request, - CancellationToken cancellationToken) - { - string? body = request.Content is null - ? null - : await request.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); - - lock (_receivedLock) - { - _received.Add(new RecordedRequest( - request.Method, - request.RequestUri!, - CloneHeaders(request.Headers), - body)); - } - - var responder = _responders.FirstOrDefault(r => r.Matches(request)); - if (responder is null) - { - // Return a non-transient 4xx so a missing stub fails fast: the Ingestion pipeline's Polly - // policy retries on >=500, which would otherwise turn a missing stub into slow retries. - return new HttpResponseMessage(HttpStatusCode.BadRequest) - { - RequestMessage = request, - Content = new StringContent( - $"No mock responder registered for {request.Method} {request.RequestUri}", - Encoding.UTF8, "text/plain"), - }; - } - - var response = responder.Respond(request); - response.RequestMessage ??= request; - return response; - } - - private static IReadOnlyDictionary CloneHeaders(HttpRequestHeaders headers) - { - var clone = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var header in headers) - { - clone[header.Key] = string.Join(", ", header.Value); - } - return clone; - } - - private sealed class Responder(HttpMethod method, string pathContains, - Func respond) - { - public bool Matches(HttpRequestMessage request) => - request.Method == method && - request.RequestUri is not null && - request.RequestUri.PathAndQuery.Contains(pathContains, StringComparison.OrdinalIgnoreCase); - - public HttpResponseMessage Respond(HttpRequestMessage request) => respond(request); - } -} - -/// Snapshot of a request observed by . -internal sealed record RecordedRequest( - HttpMethod Method, - Uri Uri, - IReadOnlyDictionary Headers, - string? Body); diff --git a/src/PackageUploader.IntegrationTest/Infrastructure/PackageUploaderTestHost.cs b/src/PackageUploader.IntegrationTest/Infrastructure/PackageUploaderTestHost.cs deleted file mode 100644 index 9a3bc6f3..00000000 --- a/src/PackageUploader.IntegrationTest/Infrastructure/PackageUploaderTestHost.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using PackageUploader.ClientApi; -using PackageUploader.ClientApi.Client.Ingestion.TokenProvider; - -namespace PackageUploader.IntegrationTest.Infrastructure; - -/// Composes the real with the Ingestion network handler and access-token provider replaced by test doubles. -internal sealed class PackageUploaderTestHost : IDisposable -{ - private const string IngestionHttpClientName = "IIngestionHttpClient"; - - private readonly ServiceProvider _provider; - private readonly IServiceScope _scope; - - public MockHttpMessageHandler IngestionHandler { get; } - - public IPackageUploaderService Service { get; } - - public PackageUploaderTestHost( - Action? configureIngestion = null, - string ingestionBaseAddress = "https://ingestion.test.local/") - { - IngestionHandler = new MockHttpMessageHandler(); - configureIngestion?.Invoke(IngestionHandler); - - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - ["IngestionConfig:BaseAddress"] = ingestionBaseAddress, - }) - .Build(); - - var services = new ServiceCollection(); - services.AddSingleton(configuration); - services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); - - services.AddPackageUploaderService(IngestionExtensions.AuthenticationMethod.Default); - - services.RemoveAll(); - services.AddScoped(); - - services.AddHttpClient(IngestionHttpClientName) - .ConfigurePrimaryHttpMessageHandler(() => IngestionHandler); - - // IPackageUploaderService and the Ingestion auth handler are scoped; resolve them from an - // explicit scope (with scope validation on) rather than the root provider. - _provider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); - _scope = _provider.CreateScope(); - Service = _scope.ServiceProvider.GetRequiredService(); - } - - public void Dispose() - { - _scope.Dispose(); - _provider.Dispose(); - } -} diff --git a/src/PackageUploader.IntegrationTest/SmokeTest.cs b/src/PackageUploader.IntegrationTest/SmokeTest.cs index 5a83a4fb..3a729410 100644 --- a/src/PackageUploader.IntegrationTest/SmokeTest.cs +++ b/src/PackageUploader.IntegrationTest/SmokeTest.cs @@ -3,31 +3,35 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using PackageUploader.IntegrationTest.Infrastructure; -using System.Net.Http; +using System.Linq; namespace PackageUploader.IntegrationTest; /// -/// Smoke test that validates the integration project is discovered, builds, and that the mock -/// harness routes a public service call through the real pipeline to the mock handler. +/// Smoke test that validates the integration project is discovered, builds, and that the mock-server +/// host routes a public service call over real HTTP to the WireMock Ingestion fake with the fake +/// auth token attached. /// [TestClass] public sealed class SmokeTest : IntegrationTestBase { [TestMethod] - public async Task TestHost_RoutesProductLookup_ThroughMockHandlerWithFakeAuth() + public async Task TestHost_RoutesProductLookup_ThroughWireMockWithFakeAuth() { - using var host = CreateHost(mock => - mock.WhenJson(HttpMethod.Get, "/products/", "{\"id\":\"smoke-test-product\"}")); + using var host = CreateMockServerHost(); + host.Ingestion.StubGetProduct("smoke-test-product"); var product = await host.Service.GetProductByProductIdAsync("smoke-test-product", TestContext.CancellationToken); Assert.IsNotNull(product); - Assert.AreEqual(1, host.IngestionHandler.ReceivedRequests.Count); + Assert.AreEqual("smoke-test-product", product.ProductId); - var request = host.IngestionHandler.ReceivedRequests[0]; - Assert.IsTrue(request.Headers.ContainsKey("Authorization")); - StringAssert.Contains(request.Headers["Authorization"], FakeAccessTokenProvider.FakeToken); + var logs = host.Ingestion.Server.LogEntries.ToList(); + Assert.AreEqual(1, logs.Count); + + var headers = logs[0].RequestMessage!.Headers!; + Assert.IsTrue(headers.ContainsKey("Authorization")); + StringAssert.Contains(string.Join(" ", headers["Authorization"]!), FakeAccessTokenProvider.FakeToken); } public TestContext TestContext { get; set; } = null!; From 426b1650951592a6a85b86a52fd1e5f69c98c1d8 Mon Sep 17 00:00:00 2001 From: Elshiekh Ahmed Date: Mon, 29 Jun 2026 09:27:10 -0700 Subject: [PATCH 06/11] Add end-to-end integration tests validating the mock infrastructure (Task 2/6 Phase 4) Add IngestionApiTests (success, NotFound, transient-error retry, branches), UploadFlowTests (full no-delta upload tying the Ingestion and XFUS fakes together), and PublishFlowTests (submission create + poll to Published). Enhance IngestionMockServer with submission pendingUpdateInfo (mapper NRE guard), package-config market group, StubProcessPackage, and a flights stub; add fast retry config to MockServerTestHost. --- .../Infrastructure/MockServerTestHost.cs | 2 + .../Mocks/IngestionMockServer.cs | 61 +++++++++++---- .../IngestionApiTests.cs | 75 +++++++++++++++++++ .../PublishFlowTests.cs | 40 ++++++++++ .../UploadFlowTests.cs | 59 +++++++++++++++ 5 files changed, 221 insertions(+), 16 deletions(-) create mode 100644 src/PackageUploader.IntegrationTest/IngestionApiTests.cs create mode 100644 src/PackageUploader.IntegrationTest/PublishFlowTests.cs create mode 100644 src/PackageUploader.IntegrationTest/UploadFlowTests.cs diff --git a/src/PackageUploader.IntegrationTest/Infrastructure/MockServerTestHost.cs b/src/PackageUploader.IntegrationTest/Infrastructure/MockServerTestHost.cs index d59964c5..16b40e9e 100644 --- a/src/PackageUploader.IntegrationTest/Infrastructure/MockServerTestHost.cs +++ b/src/PackageUploader.IntegrationTest/Infrastructure/MockServerTestHost.cs @@ -44,6 +44,8 @@ public MockServerTestHost() { // Trailing slash so the client's relative paths (e.g. "products/{id}") resolve under it. ["IngestionConfig:BaseAddress"] = $"{Ingestion.Url}/", + ["IngestionConfig:MedianFirstRetryDelayMs"] = "1", + ["IngestionConfig:RetryCount"] = "3", }) .Build(); diff --git a/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/IngestionMockServer.cs b/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/IngestionMockServer.cs index 50a5a250..c541d970 100644 --- a/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/IngestionMockServer.cs +++ b/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/IngestionMockServer.cs @@ -66,6 +66,10 @@ public IngestionMockServer StubGetPackageBranches( _server .Given(Request.Create().WithPath("/products/" + productId + "/branches/getByModule*").UsingGet()) .RespondWith(JsonResponse(new { value = values })); + + _server + .Given(Request.Create().WithPath($"/products/{productId}/flights").UsingGet()) + .RespondWith(JsonResponse(new { value = Array.Empty() })); return this; } @@ -150,7 +154,12 @@ public IngestionMockServer StubGetPackageProcessing( // ---- GetPackageConfig: GET /products/{id}/packageConfigurations/getByInstanceID(...) and GET/PUT by id ---- - public IngestionMockServer StubPackageConfiguration(string productId, string instanceId, string configId) + public IngestionMockServer StubPackageConfiguration( + string productId, + string instanceId, + string configId, + string marketGroupId = "default", + string marketGroupName = "default") { var body = JsonResponse(new { @@ -161,7 +170,15 @@ public IngestionMockServer StubPackageConfiguration(string productId, string ins .Given(Request.Create().WithPath("/products/" + productId + "/packageConfigurations/getByInstanceID*").UsingGet()) .RespondWith(body); - var single = JsonResponse(new { resourceType = "PackageConfiguration", id = configId }); + var single = JsonResponse(new + { + resourceType = "PackageConfiguration", + id = configId, + marketGroupPackages = new[] + { + new { marketGroupId, name = marketGroupName, packageIds = Array.Empty() }, + }, + }); _server .Given(Request.Create().WithPath($"/products/{productId}/packageConfigurations/{configId}").UsingGet()) .RespondWith(single); @@ -171,22 +188,31 @@ public IngestionMockServer StubPackageConfiguration(string productId, string ins return this; } - // ---- CreateSubmission: POST /products/{id}/submissions ---- + // ---- ProcessPackage: PUT /products/{id}/packages/{packageId} ---- - public IngestionMockServer StubCreateSubmission(string productId, string submissionId) + public IngestionMockServer StubProcessPackage(string productId, string packageId, string state = "Uploaded") { _server - .Given(Request.Create().WithPath($"/products/{productId}/submissions").UsingPost()) + .Given(Request.Create().WithPath($"/products/{productId}/packages/{packageId}").UsingPut()) .RespondWith(JsonResponse(new { - resourceType = "Submission", - id = submissionId, - state = "InProgress", - substate = "Submitted", + resourceType = "GamePackage", + id = packageId, + state, })); return this; } + // ---- CreateSubmission: POST /products/{id}/submissions ---- + + public IngestionMockServer StubCreateSubmission(string productId, string submissionId) + { + _server + .Given(Request.Create().WithPath($"/products/{productId}/submissions").UsingPost()) + .RespondWith(JsonResponse(SubmissionBody(submissionId, "InProgress", "Submitted"))); + return this; + } + // ---- GetSubmission poll: GET /products/{id}/submissions/{id} ---- // Returns each (state, substate) in order across successive calls, then stays on the final one. @@ -202,13 +228,7 @@ public IngestionMockServer StubGetSubmission( for (int i = 0; i < steps.Length; i++) { bool isLast = i == steps.Length - 1; - var body = JsonResponse(new - { - resourceType = "Submission", - id = submissionId, - state = steps[i].State, - substate = steps[i].Substate, - }); + var body = JsonResponse(SubmissionBody(submissionId, steps[i].State, steps[i].Substate)); var builder = _server.Given(request).InScenario(scenario); if (i > 0) @@ -276,6 +296,15 @@ public IngestionMockServer StubRetryThenSuccess( // ---- helpers ---- + private static object SubmissionBody(string submissionId, string state, string substate) => new + { + resourceType = "Submission", + id = submissionId, + state, + substate, + pendingUpdateInfo = new { status = "Completed" }, + }; + private static string StateName(int index) => $"step-{index}"; private static IResponseBuilder JsonResponse(object body) => diff --git a/src/PackageUploader.IntegrationTest/IngestionApiTests.cs b/src/PackageUploader.IntegrationTest/IngestionApiTests.cs new file mode 100644 index 00000000..bf296e44 --- /dev/null +++ b/src/PackageUploader.IntegrationTest/IngestionApiTests.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PackageUploader.ClientApi.Client.Ingestion.Exceptions; +using PackageUploader.IntegrationTest.Infrastructure; +using PackageUploader.IntegrationTest.Infrastructure.Mocks; +using System.Net; +using System.Threading; + +namespace PackageUploader.IntegrationTest; + +/// +/// End-to-end integration tests for Ingestion API flows, exercising the real service against the +/// WireMock Ingestion fake: success, error mapping, transient-error retry, and paged collections. +/// +[TestClass] +public sealed class IngestionApiTests : IntegrationTestBase +{ + [TestMethod] + public async Task GetProductByProductId_Success_ReturnsMappedProduct() + { + using var host = CreateMockServerHost(); + host.Ingestion.StubGetProduct("9P000TEST"); + + var product = await host.Service.GetProductByProductIdAsync("9P000TEST", CancellationToken.None); + + Assert.IsNotNull(product); + Assert.AreEqual("9P000TEST", product.ProductId); + Assert.AreEqual("Test Product 9P000TEST", product.ProductName); + } + + [TestMethod] + public async Task GetProductByProductId_NotFound_ThrowsProductNotFound() + { + using var host = CreateMockServerHost(); + host.Ingestion.StubGetProduct("MISSING", ResponseScenario.NotFound); + + await Assert.ThrowsExactlyAsync( + () => host.Service.GetProductByProductIdAsync("MISSING", CancellationToken.None)); + } + + [TestMethod] + public async Task GetProductByProductId_RetriesTransientError_ThenSucceeds() + { + using var host = CreateMockServerHost(); + host.Ingestion.StubRetryThenSuccess( + "GET", + "/products/RETRYME", + new { resourceType = "AzureGameProduct", id = "RETRYME", name = "Recovered" }, + failures: 2, + failureStatus: HttpStatusCode.InternalServerError); + + var product = await host.Service.GetProductByProductIdAsync("RETRYME", CancellationToken.None); + + Assert.IsNotNull(product); + Assert.AreEqual("RETRYME", product.ProductId); + } + + [TestMethod] + public async Task GetPackageBranches_ReturnsConfiguredBranches() + { + using var host = CreateMockServerHost(); + host.Ingestion.StubGetProduct("PRODX"); + host.Ingestion.StubGetPackageBranches("PRODX", ("Main", "draft-1"), ("Beta", "draft-2")); + + var product = await host.Service.GetProductByProductIdAsync("PRODX", CancellationToken.None); + var branches = await host.Service.GetPackageBranchesAsync(product, CancellationToken.None); + + Assert.AreEqual(2, branches.Count); + CollectionAssert.AreEquivalent( + new[] { "Main", "Beta" }, + branches.Select(b => b.Name).ToArray()); + } +} diff --git a/src/PackageUploader.IntegrationTest/PublishFlowTests.cs b/src/PackageUploader.IntegrationTest/PublishFlowTests.cs new file mode 100644 index 00000000..b4773989 --- /dev/null +++ b/src/PackageUploader.IntegrationTest/PublishFlowTests.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PackageUploader.ClientApi.Client.Ingestion.Models; +using PackageUploader.IntegrationTest.Infrastructure; +using System.Threading; + +namespace PackageUploader.IntegrationTest; + +/// +/// End-to-end publish flow that exercises submission creation and polling against the Ingestion +/// fake: create a sandbox submission, then poll it until it reaches the Published state. +/// +[TestClass] +public sealed class PublishFlowTests : IntegrationTestBase +{ + [TestMethod] + public async Task PublishToSandbox_PollsSubmission_UntilPublished() + { + using var host = CreateMockServerHost(); + + const string productId = "PRODPUBLISH"; + const string submissionId = "sub-1"; + + host.Ingestion.StubGetProduct(productId); + host.Ingestion.StubGetPackageBranches(productId, ("Main", "draft-1")); + host.Ingestion.StubCreateSubmission(productId, submissionId); + host.Ingestion.StubGetSubmission(productId, submissionId, ("Published", "InStore")); + + var product = await host.Service.GetProductByProductIdAsync(productId, CancellationToken.None); + var branch = await host.Service.GetPackageBranchByFriendlyNameAsync(product, "Main", CancellationToken.None); + + var submission = await host.Service.PublishPackagesToSandboxAsync( + product, branch, "Sandbox.1", minutesToWaitForPublishing: 1, CancellationToken.None); + + Assert.IsNotNull(submission); + Assert.AreEqual(GameSubmissionState.Published, submission.GameSubmissionState); + } +} diff --git a/src/PackageUploader.IntegrationTest/UploadFlowTests.cs b/src/PackageUploader.IntegrationTest/UploadFlowTests.cs new file mode 100644 index 00000000..db2b8b04 --- /dev/null +++ b/src/PackageUploader.IntegrationTest/UploadFlowTests.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PackageUploader.ClientApi.Client.Ingestion.Models; +using PackageUploader.IntegrationTest.Fixtures; +using PackageUploader.IntegrationTest.Infrastructure; +using System.Threading; + +namespace PackageUploader.IntegrationTest; + +/// +/// End-to-end upload flow that ties both fakes together: the real service creates a package against +/// the Ingestion fake, uploads the file to the XFUS fake (no-delta), then processes and polls the +/// package to completion. +/// +[TestClass] +public sealed class UploadFlowTests : IntegrationTestBase +{ + [TestMethod] + public async Task UploadGamePackage_NoDelta_CompletesThroughIngestionAndXfus() + { + using var host = CreateMockServerHost(); + using var packageFile = SyntheticPackageFile.Create(sizeInBytes: 4096, extension: ".xvc"); + + const string productId = "PRODUPLOAD"; + const string packageId = "pkg-1"; + + host.Ingestion.StubGetProduct(productId); + host.Ingestion.StubGetPackageBranches(productId, ("Main", "draft-1")); + host.Ingestion.StubPackageConfiguration(productId, "draft-1", "config-1", marketGroupId: "NA"); + host.Ingestion.StubCreatePackage(productId, packageId, xfusUploadDomain: host.XfusUploadDomain); + host.Ingestion.StubProcessPackage(productId, packageId, "Processed"); + host.Ingestion.StubGetPackageProcessing(productId, packageId, "Processed"); + host.Xfus.StubNoDeltaUploadSuccess(1024); + + var product = await host.Service.GetProductByProductIdAsync(productId, CancellationToken.None); + var branch = await host.Service.GetPackageBranchByFriendlyNameAsync(product, "Main", CancellationToken.None); + var config = await host.Service.GetPackageConfigurationAsync(product, branch, CancellationToken.None); + var marketGroupPackage = config.MarketGroupPackages[0]; + + var result = await host.Service.UploadGamePackageAsync( + product, + branch, + marketGroupPackage, + packageFile.Path, + gameAssets: null, + minutesToWaitForProcessing: 1, + deltaUpload: false, + isXvc: false, + CancellationToken.None); + + Assert.IsNotNull(result); + Assert.AreEqual(GamePackageState.Processed, result.State); + + // The file was actually uploaded to the XFUS fake (initialize + at least one block + continue). + Assert.IsTrue(host.Xfus.Server.LogEntries.Any(), "XFUS fake should have received upload requests"); + } +} From dd2ca46b0b4942a15bee3a6a501cda8cb0e7f8de Mon Sep 17 00:00:00 2001 From: Elshiekh Ahmed Date: Mon, 29 Jun 2026 09:45:39 -0700 Subject: [PATCH 07/11] Address code review findings on Task 2 mock infrastructure - IngestionMockServer: emit null (not an empty object) for absent uploadInfo so the client's Guid mapping doesn't throw on a null XfusId; short-circuit StubRetryThenSuccess(failures<=0) to an unconditional success instead of an unmatched 404. - XfusMockServer: short-circuit StubBlockUploadRetryThenSuccess(failures<=0); document the single-asset-per-server assumption of StubContinueProgression. - MockServerTestHost: dispose the WireMock servers if composition throws, to avoid leaking listeners. - UploadFlowTests: assert a block payload PUT actually reached XFUS rather than any request. --- .../Infrastructure/MockServerTestHost.cs | 47 +++++++++++-------- .../Mocks/IngestionMockServer.cs | 14 ++++-- .../Infrastructure/Mocks/XfusMockServer.cs | 9 +++- .../UploadFlowTests.cs | 8 +++- 4 files changed, 53 insertions(+), 25 deletions(-) diff --git a/src/PackageUploader.IntegrationTest/Infrastructure/MockServerTestHost.cs b/src/PackageUploader.IntegrationTest/Infrastructure/MockServerTestHost.cs index 16b40e9e..f7329f9c 100644 --- a/src/PackageUploader.IntegrationTest/Infrastructure/MockServerTestHost.cs +++ b/src/PackageUploader.IntegrationTest/Infrastructure/MockServerTestHost.cs @@ -38,29 +38,38 @@ public MockServerTestHost() { Ingestion = new IngestionMockServer(); Xfus = new XfusMockServer(); + try + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + // Trailing slash so the client's relative paths (e.g. "products/{id}") resolve under it. + ["IngestionConfig:BaseAddress"] = $"{Ingestion.Url}/", + ["IngestionConfig:MedianFirstRetryDelayMs"] = "1", + ["IngestionConfig:RetryCount"] = "3", + }) + .Build(); - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - // Trailing slash so the client's relative paths (e.g. "products/{id}") resolve under it. - ["IngestionConfig:BaseAddress"] = $"{Ingestion.Url}/", - ["IngestionConfig:MedianFirstRetryDelayMs"] = "1", - ["IngestionConfig:RetryCount"] = "3", - }) - .Build(); + var services = new ServiceCollection(); + services.AddSingleton(configuration); + services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); - var services = new ServiceCollection(); - services.AddSingleton(configuration); - services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); + services.AddPackageUploaderService(IngestionExtensions.AuthenticationMethod.Default); - services.AddPackageUploaderService(IngestionExtensions.AuthenticationMethod.Default); + services.RemoveAll(); + services.AddScoped(); - services.RemoveAll(); - services.AddScoped(); - - _provider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); - _scope = _provider.CreateScope(); - Service = _scope.ServiceProvider.GetRequiredService(); + _provider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); + _scope = _provider.CreateScope(); + Service = _scope.ServiceProvider.GetRequiredService(); + } + catch + { + // Avoid leaking the started WireMock servers if composition fails. + Xfus.Dispose(); + Ingestion.Dispose(); + throw; + } } /// The XFUS server's base URL, for use as the upload domain in a stubbed package response. diff --git a/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/IngestionMockServer.cs b/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/IngestionMockServer.cs index c541d970..b8405de7 100644 --- a/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/IngestionMockServer.cs +++ b/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/IngestionMockServer.cs @@ -85,9 +85,11 @@ public IngestionMockServer StubCreatePackage( string xfusToken = "fake-xfus-token") { // When an XFUS upload domain is supplied, embed the upload info so the client uploads to the - // XFUS mock; xfusId must be a valid GUID because the client maps it to a Guid. - object uploadInfo = xfusUploadDomain is null - ? new { } + // XFUS mock; xfusId must be a valid GUID because the client maps it to a Guid. When absent, + // use null (not new {}) so the field is omitted entirely — an empty object would deserialize + // into a non-null upload info with a null XfusId and crash the client's Guid mapping. + object? uploadInfo = xfusUploadDomain is null + ? null : new { fileName, @@ -275,6 +277,12 @@ public IngestionMockServer StubRetryThenSuccess( var request = Request.Create().WithPath(path).UsingMethod(method); var scenario = $"retry-{method}-{path}-{_id}"; + if (failures <= 0) + { + _server.Given(request).RespondWith(JsonResponse(successBody)); + return this; + } + for (int i = 0; i < failures; i++) { var builder = _server.Given(request).InScenario(scenario); diff --git a/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/XfusMockServer.cs b/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/XfusMockServer.cs index 7d9ca028..f848ffa0 100644 --- a/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/XfusMockServer.cs +++ b/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/XfusMockServer.cs @@ -79,7 +79,8 @@ public XfusMockServer StubContinue(object uploadProgressBody) /// /// Returns the given continue-progress bodies in order across successive calls, then stays on the /// final one — used to simulate the server working through the upload (e.g. Busy then Completed) - /// or the multi-step delta plan. + /// or the multi-step delta plan. The progression is keyed per server instance, so it assumes a + /// single asset upload per (the usual one-upload-per-test pattern). /// public XfusMockServer StubContinueProgression(params object[] uploadProgressBodies) { @@ -127,6 +128,12 @@ public XfusMockServer StubBlockUploadRetryThenSuccess(int failures = 1, HttpStat var request = Request.Create().WithPath($"{AssetsRoot}/*/blocks/*/source/payload").UsingPut(); var scenario = $"xfus-block-{_id}"; + if (failures <= 0) + { + _server.Given(request).RespondWith(Response.Create().WithStatusCode((int)HttpStatusCode.OK)); + return this; + } + for (int i = 0; i < failures; i++) { var builder = _server.Given(request).InScenario(scenario); diff --git a/src/PackageUploader.IntegrationTest/UploadFlowTests.cs b/src/PackageUploader.IntegrationTest/UploadFlowTests.cs index db2b8b04..0d101b62 100644 --- a/src/PackageUploader.IntegrationTest/UploadFlowTests.cs +++ b/src/PackageUploader.IntegrationTest/UploadFlowTests.cs @@ -53,7 +53,11 @@ public async Task UploadGamePackage_NoDelta_CompletesThroughIngestionAndXfus() Assert.IsNotNull(result); Assert.AreEqual(GamePackageState.Processed, result.State); - // The file was actually uploaded to the XFUS fake (initialize + at least one block + continue). - Assert.IsTrue(host.Xfus.Server.LogEntries.Any(), "XFUS fake should have received upload requests"); + // The file was actually uploaded to the XFUS fake: a block payload PUT must have occurred. + Assert.IsTrue( + host.Xfus.Server.LogEntries.Any(e => + e.RequestMessage!.Method == "PUT" && + e.RequestMessage.Path.Contains("/source/payload")), + "XFUS fake should have received a block payload PUT"); } } From 514a723125e3df5fe2f586fb7cfe326764105f4a Mon Sep 17 00:00:00 2001 From: Elshiekh Ahmed Date: Mon, 29 Jun 2026 10:09:37 -0700 Subject: [PATCH 08/11] Trigger CI re-run (placeholder comment) --- src/PackageUploader.IntegrationTest/SmokeTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PackageUploader.IntegrationTest/SmokeTest.cs b/src/PackageUploader.IntegrationTest/SmokeTest.cs index 3a729410..a48f2ccb 100644 --- a/src/PackageUploader.IntegrationTest/SmokeTest.cs +++ b/src/PackageUploader.IntegrationTest/SmokeTest.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +// CI re-run trigger placeholder. using Microsoft.VisualStudio.TestTools.UnitTesting; using PackageUploader.IntegrationTest.Infrastructure; From 93aa9e9a258c783455927a92c8e980def462c25f Mon Sep 17 00:00:00 2001 From: Elshiekh Ahmed Date: Mon, 29 Jun 2026 10:09:55 -0700 Subject: [PATCH 09/11] Remove CI re-run placeholder comment --- src/PackageUploader.IntegrationTest/SmokeTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PackageUploader.IntegrationTest/SmokeTest.cs b/src/PackageUploader.IntegrationTest/SmokeTest.cs index a48f2ccb..3a729410 100644 --- a/src/PackageUploader.IntegrationTest/SmokeTest.cs +++ b/src/PackageUploader.IntegrationTest/SmokeTest.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -// CI re-run trigger placeholder. using Microsoft.VisualStudio.TestTools.UnitTesting; using PackageUploader.IntegrationTest.Infrastructure; From 8d30d16780be0fcc7e77b2f3931c971ab1680218 Mon Sep 17 00:00:00 2001 From: Elshiekh Ahmed Date: Tue, 30 Jun 2026 12:40:46 -0700 Subject: [PATCH 10/11] Prototype Option B: Task 2 mock suite via a Kestrel-hosted fake-API app (controllers) Replace WireMock.Net with a first-party fake-API ASP.NET Core app: IngestionController and XfusController mirror the routes the client calls and delegate to per-test scenario stores; the host boots them on a random loopback port via Kestrel and points the real client at it (real sockets, real SocketsHttpHandler). Uses only the Microsoft.AspNetCore.App shared framework (FrameworkReference) -- zero NuGet packages, so locked-mode restore needs no feed provisioning. Same 7 integration tests pass. --- .../FakeApi/FakeApiControllerBase.cs | 46 + .../FakeApi/IngestionController.cs | 16 + .../FakeApi/IngestionScenarioStore.cs | 195 +++ .../FakeApi/ScenarioStore.cs | 105 ++ .../FakeApi/XfusController.cs | 17 + .../FakeApi/XfusScenarioStore.cs | 105 ++ .../Infrastructure/MockServerTestHost.cs | 113 +- .../Mocks/IngestionMockServer.cs | 332 ---- .../Infrastructure/Mocks/ResponseScenario.cs | 2 +- .../Infrastructure/Mocks/XfusMockServer.cs | 196 --- .../PackageUploader.IntegrationTest.csproj | 5 +- .../SmokeTest.cs | 15 +- .../UploadFlowTests.cs | 8 +- .../packages.lock.json | 1445 +---------------- 14 files changed, 603 insertions(+), 1997 deletions(-) create mode 100644 src/PackageUploader.IntegrationTest/FakeApi/FakeApiControllerBase.cs create mode 100644 src/PackageUploader.IntegrationTest/FakeApi/IngestionController.cs create mode 100644 src/PackageUploader.IntegrationTest/FakeApi/IngestionScenarioStore.cs create mode 100644 src/PackageUploader.IntegrationTest/FakeApi/ScenarioStore.cs create mode 100644 src/PackageUploader.IntegrationTest/FakeApi/XfusController.cs create mode 100644 src/PackageUploader.IntegrationTest/FakeApi/XfusScenarioStore.cs delete mode 100644 src/PackageUploader.IntegrationTest/Infrastructure/Mocks/IngestionMockServer.cs delete mode 100644 src/PackageUploader.IntegrationTest/Infrastructure/Mocks/XfusMockServer.cs diff --git a/src/PackageUploader.IntegrationTest/FakeApi/FakeApiControllerBase.cs b/src/PackageUploader.IntegrationTest/FakeApi/FakeApiControllerBase.cs new file mode 100644 index 00000000..fe2b3166 --- /dev/null +++ b/src/PackageUploader.IntegrationTest/FakeApi/FakeApiControllerBase.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; +using PackageUploader.IntegrationTest.FakeApi; + +namespace PackageUploader.IntegrationTest.FakeApi; + +/// +/// Base for the fake-API controllers: records the incoming request into the given store and returns +/// the scripted response (status code, with an optional JSON body). +/// +public abstract class FakeApiControllerBase : ControllerBase +{ + private protected async Task RespondAsync(ScenarioStore store) + { + string? body = null; + if (Request.ContentLength is > 0) + { + using var reader = new StreamReader(Request.Body, leaveOpen: true); + body = await reader.ReadToEndAsync(); + } + + var headers = Request.Headers.ToDictionary( + h => h.Key, + h => string.Join(", ", h.Value.ToArray()), + StringComparer.OrdinalIgnoreCase); + + var path = Request.Path.Value ?? string.Empty; + store.Record(Request.Method, path, headers, body); + + var response = store.Resolve(Request.Method, path); + if (response.Body is null) + { + return StatusCode(response.StatusCode); + } + + return new ContentResult + { + StatusCode = response.StatusCode, + ContentType = "application/json", + Content = JsonSerializer.Serialize(response.Body), + }; + } +} diff --git a/src/PackageUploader.IntegrationTest/FakeApi/IngestionController.cs b/src/PackageUploader.IntegrationTest/FakeApi/IngestionController.cs new file mode 100644 index 00000000..aec9db89 --- /dev/null +++ b/src/PackageUploader.IntegrationTest/FakeApi/IngestionController.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Mvc; + +namespace PackageUploader.IntegrationTest.FakeApi; + +/// +/// Fake Partner Center Ingestion API. Presents the products/... route space the client calls +/// and delegates each request to the configured . +/// +public sealed class IngestionController(IngestionScenarioStore store) : FakeApiControllerBase +{ + [AcceptVerbs("GET", "POST", "PUT", Route = "products/{**rest}")] + public Task Handle() => RespondAsync(store); +} diff --git a/src/PackageUploader.IntegrationTest/FakeApi/IngestionScenarioStore.cs b/src/PackageUploader.IntegrationTest/FakeApi/IngestionScenarioStore.cs new file mode 100644 index 00000000..908730c0 --- /dev/null +++ b/src/PackageUploader.IntegrationTest/FakeApi/IngestionScenarioStore.cs @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Net.Http; +using PackageUploader.IntegrationTest.Infrastructure.Mocks; + +namespace PackageUploader.IntegrationTest.FakeApi; + +/// +/// Configurable scripted responses for the fake Ingestion API, consumed by . +/// Exposes the same fluent stub surface as the other Task 2 prototypes. +/// +public sealed class IngestionScenarioStore : ScenarioStore +{ + public IngestionScenarioStore StubGetProduct(string productId, ResponseScenario scenario = ResponseScenario.Success) + { + if (scenario != ResponseScenario.Success) + { + On(HttpMethod.Get, $"/products/{productId}", () => new FakeResponse((int)StatusFor(scenario))); + return this; + } + + On(HttpMethod.Get, $"/products/{productId}", () => new FakeResponse(200, new + { + resourceType = "AzureGameProduct", + name = $"Test Product {productId}", + id = productId, + externalIds = new[] { new { type = "StoreId", value = "9TESTBIGID000" } }, + isModularPublishing = true, + })); + return this; + } + + public IngestionScenarioStore StubGetPackageBranches( + string productId, + params (string FriendlyName, string CurrentDraftInstanceId)[] branches) + { + var values = branches.Select(b => new + { + resourceType = "Branch", + friendlyName = b.FriendlyName, + type = "Main", + module = "Package", + currentDraftInstanceId = b.CurrentDraftInstanceId, + }).ToArray(); + + On(HttpMethod.Get, $"/products/{productId}/branches/getByModule*", () => new FakeResponse(200, new { value = values })); + On(HttpMethod.Get, $"/products/{productId}/flights", () => new FakeResponse(200, new { value = Array.Empty() })); + return this; + } + + public IngestionScenarioStore StubCreatePackage( + string productId, + string packageId, + string fileName = "test.xvc", + string? xfusUploadDomain = null, + string? xfusId = null, + string xfusTenant = "DCE", + string xfusToken = "fake-xfus-token") + { + object? uploadInfo = xfusUploadDomain is null + ? null + : new + { + fileName, + xfusId = xfusId ?? Guid.NewGuid().ToString(), + token = xfusToken, + uploadDomain = xfusUploadDomain, + xfusTenant, + }; + + On(HttpMethod.Post, $"/products/{productId}/packages", () => new FakeResponse(200, new + { + resourceType = "GamePackage", + id = packageId, + state = "PendingUpload", + fileName, + uploadInfo, + })); + return this; + } + + public IngestionScenarioStore StubGetPackageProcessing(string productId, string packageId, params string[] stateProgression) + { + var states = stateProgression.Length > 0 ? stateProgression : ["Processed"]; + var responders = states.Select(state => (Func)(() => new FakeResponse(200, new + { + resourceType = "GamePackage", + id = packageId, + state, + }))).ToArray(); + + OnSequence(HttpMethod.Get, $"/products/{productId}/packages/{packageId}", responders); + return this; + } + + public IngestionScenarioStore StubProcessPackage(string productId, string packageId, string state = "Uploaded") + { + On(HttpMethod.Put, $"/products/{productId}/packages/{packageId}", () => new FakeResponse(200, new + { + resourceType = "GamePackage", + id = packageId, + state, + })); + return this; + } + + public IngestionScenarioStore StubPackageConfiguration( + string productId, + string instanceId, + string configId, + string marketGroupId = "default", + string marketGroupName = "default") + { + On(HttpMethod.Get, $"/products/{productId}/packageConfigurations/getByInstanceID*", () => new FakeResponse(200, new + { + value = new[] { new { resourceType = "PackageConfiguration", id = configId } }, + })); + + Func single = () => new FakeResponse(200, new + { + resourceType = "PackageConfiguration", + id = configId, + marketGroupPackages = new[] + { + new { marketGroupId, name = marketGroupName, packageIds = Array.Empty() }, + }, + }); + On(HttpMethod.Get, $"/products/{productId}/packageConfigurations/{configId}", single); + On(HttpMethod.Put, $"/products/{productId}/packageConfigurations/{configId}", single); + return this; + } + + public IngestionScenarioStore StubCreateSubmission(string productId, string submissionId) + { + On(HttpMethod.Post, $"/products/{productId}/submissions", () => new FakeResponse(200, SubmissionBody(submissionId, "InProgress", "Submitted"))); + return this; + } + + public IngestionScenarioStore StubGetSubmission( + string productId, + string submissionId, + params (string State, string Substate)[] progression) + { + var steps = progression.Length > 0 ? progression : [("Published", "InStore")]; + var responders = steps.Select(step => (Func)(() => + new FakeResponse(200, SubmissionBody(submissionId, step.State, step.Substate)))).ToArray(); + + OnSequence(HttpMethod.Get, $"/products/{productId}/submissions/{submissionId}", responders); + return this; + } + + public IngestionScenarioStore StubError(string method, string pathPattern, HttpStatusCode statusCode) + { + On(HttpMethod.Parse(method), pathPattern, () => new FakeResponse((int)statusCode)); + return this; + } + + public IngestionScenarioStore StubRetryThenSuccess( + string method, + string path, + object successBody, + int failures = 2, + HttpStatusCode failureStatus = HttpStatusCode.InternalServerError) + { + var responders = new List>(); + for (var i = 0; i < failures; i++) + { + responders.Add(() => new FakeResponse((int)failureStatus)); + } + responders.Add(() => new FakeResponse(200, successBody)); + + OnSequence(HttpMethod.Parse(method), path, responders); + return this; + } + + private static object SubmissionBody(string submissionId, string state, string substate) => new + { + resourceType = "Submission", + id = submissionId, + state, + substate, + // PendingUpdateInfo.Status is dereferenced by the submission-state mapper, so it must be present. + pendingUpdateInfo = new { status = "Completed" }, + }; + + private static HttpStatusCode StatusFor(ResponseScenario scenario) => scenario switch + { + ResponseScenario.ServerError => HttpStatusCode.InternalServerError, + ResponseScenario.Unauthorized => HttpStatusCode.Unauthorized, + ResponseScenario.NotFound => HttpStatusCode.NotFound, + _ => HttpStatusCode.InternalServerError, + }; +} diff --git a/src/PackageUploader.IntegrationTest/FakeApi/ScenarioStore.cs b/src/PackageUploader.IntegrationTest/FakeApi/ScenarioStore.cs new file mode 100644 index 00000000..be3f91d8 --- /dev/null +++ b/src/PackageUploader.IntegrationTest/FakeApi/ScenarioStore.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net.Http; +using System.Text.RegularExpressions; + +namespace PackageUploader.IntegrationTest.FakeApi; + +/// A scripted response: an HTTP status code and an optional JSON body object. +public sealed record FakeResponse(int StatusCode, object? Body = null); + +/// A request observed by the fake API, for test assertions. +public sealed record RecordedRequest( + HttpMethod Method, + string Path, + IReadOnlyDictionary Headers, + string? Body); + +/// +/// Per-test configurable store of scripted responses for one fake service. Tests register stubs +/// (success / error / retry / polling) before exercising the client; the matching controller calls +/// for each incoming request and to log it. Rules match by +/// HTTP method and a path pattern where * matches any run of non-slash characters; rules are +/// evaluated in registration order, first match wins, and sequential rules advance per call and stay +/// on the final response. +/// +public abstract class ScenarioStore +{ + private readonly List _rules = []; + private readonly List _received = []; + private readonly Lock _lock = new(); + + /// Every request observed by the fake service, in order, for assertions. + public IReadOnlyList ReceivedRequests + { + get + { + lock (_lock) + { + return _received.ToArray(); + } + } + } + + protected void On(HttpMethod method, string pathPattern, Func respond) + { + var regex = ToRegex(pathPattern); + lock (_lock) + { + _rules.Add(new Rule(method, regex, () => respond())); + } + } + + protected void OnSequence(HttpMethod method, string pathPattern, IReadOnlyList> responders) + { + var regex = ToRegex(pathPattern); + var index = 0; + lock (_lock) + { + _rules.Add(new Rule(method, regex, () => + { + var responder = responders[Math.Min(index, responders.Count - 1)]; + if (index < responders.Count - 1) + { + index++; + } + return responder(); + })); + } + } + + /// Returns the scripted response for a request, or 400 if no stub matches. + public FakeResponse Resolve(string method, string absolutePath) + { + var parsed = HttpMethod.Parse(method); + lock (_lock) + { + var rule = _rules.FirstOrDefault(r => r.Matches(parsed, absolutePath)); + return rule is null ? new FakeResponse(400) : rule.Respond(); + } + } + + /// Records an observed request. + public void Record(string method, string path, IReadOnlyDictionary headers, string? body) + { + lock (_lock) + { + _received.Add(new RecordedRequest(HttpMethod.Parse(method), path, headers, body)); + } + } + + private static Regex ToRegex(string pathPattern) + { + var escaped = Regex.Escape(pathPattern).Replace("\\*", "[^/]*"); + return new Regex("^" + escaped + "$", RegexOptions.IgnoreCase | RegexOptions.Compiled); + } + + private sealed class Rule(HttpMethod method, Regex pathRegex, Func respond) + { + public bool Matches(HttpMethod method2, string absolutePath) => + method2 == method && pathRegex.IsMatch(absolutePath); + + public FakeResponse Respond() => respond(); + } +} diff --git a/src/PackageUploader.IntegrationTest/FakeApi/XfusController.cs b/src/PackageUploader.IntegrationTest/FakeApi/XfusController.cs new file mode 100644 index 00000000..1c2ae7ce --- /dev/null +++ b/src/PackageUploader.IntegrationTest/FakeApi/XfusController.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Mvc; + +namespace PackageUploader.IntegrationTest.FakeApi; + +/// +/// Fake XFUS upload service. Presents the api/v2/assets/... route space the client calls +/// (initialize, block payload PUT, continue) and delegates each request to the configured +/// . +/// +public sealed class XfusController(XfusScenarioStore store) : FakeApiControllerBase +{ + [AcceptVerbs("GET", "POST", "PUT", Route = "api/v2/assets/{**rest}")] + public Task Handle() => RespondAsync(store); +} diff --git a/src/PackageUploader.IntegrationTest/FakeApi/XfusScenarioStore.cs b/src/PackageUploader.IntegrationTest/FakeApi/XfusScenarioStore.cs new file mode 100644 index 00000000..fcf38bf4 --- /dev/null +++ b/src/PackageUploader.IntegrationTest/FakeApi/XfusScenarioStore.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Net.Http; + +namespace PackageUploader.IntegrationTest.FakeApi; + +/// +/// Configurable scripted responses for the fake XFUS upload service, consumed by . +/// Serves the three-step chunked upload (initialize -> block payload PUT -> continue) for the +/// no-delta path, with configurable success / error / retry scenarios. +/// +/// +/// Responses omit directUploadParameters.sasUri so the client uploads blocks via the proxy +/// PUT path. The status field is emitted as a number (ReceivingBlocks=0, Busy=1, Completed=2). +/// +public sealed class XfusScenarioStore : ScenarioStore +{ + private const string AssetsRoot = "/api/v2/assets"; + + public XfusScenarioStore StubNoDeltaUploadSuccess(params long[] blockSizes) + { + var sizes = blockSizes.Length > 0 ? blockSizes : [64L * 1024]; + StubInitialize(UploadProgress(sizes, XfusStatus.ReceivingBlocks)); + StubBlockUpload(); + StubContinue(UploadProgress([], XfusStatus.Completed)); + return this; + } + + public XfusScenarioStore StubInitialize(object uploadProgressBody) + { + On(HttpMethod.Post, $"{AssetsRoot}/*/initialize", () => new FakeResponse(200, uploadProgressBody)); + return this; + } + + public XfusScenarioStore StubBlockUpload(HttpStatusCode statusCode = HttpStatusCode.OK) + { + On(HttpMethod.Put, $"{AssetsRoot}/*/blocks/*/source/payload", () => new FakeResponse((int)statusCode)); + return this; + } + + public XfusScenarioStore StubContinue(object uploadProgressBody) + { + On(HttpMethod.Post, $"{AssetsRoot}/*/continue", () => new FakeResponse(200, uploadProgressBody)); + return this; + } + + public XfusScenarioStore StubContinueProgression(params object[] uploadProgressBodies) + { + var bodies = uploadProgressBodies.Length > 0 ? uploadProgressBodies : [UploadProgress([], XfusStatus.Completed)]; + var responders = bodies.Select(b => (Func)(() => new FakeResponse(200, b))).ToArray(); + OnSequence(HttpMethod.Post, $"{AssetsRoot}/*/continue", responders); + return this; + } + + public XfusScenarioStore StubError(string method, string pathPattern, HttpStatusCode statusCode) + { + On(HttpMethod.Parse(method), pathPattern, () => new FakeResponse((int)statusCode)); + return this; + } + + public XfusScenarioStore StubBlockUploadRetryThenSuccess(int failures = 1, HttpStatusCode failureStatus = HttpStatusCode.ServiceUnavailable) + { + var responders = new List>(); + for (var i = 0; i < failures; i++) + { + responders.Add(() => new FakeResponse((int)failureStatus)); + } + responders.Add(() => new FakeResponse((int)HttpStatusCode.OK)); + + OnSequence(HttpMethod.Put, $"{AssetsRoot}/*/blocks/*/source/payload", responders); + return this; + } + + private enum XfusStatus + { + ReceivingBlocks = 0, + Busy = 1, + Completed = 2, + } + + private static object UploadProgress(long[] blockSizes, XfusStatus status) + { + long offset = 0; + var blocks = new List(); + for (long i = 0; i < blockSizes.Length; i++) + { + blocks.Add(new + { + id = i, + blockIdBase64 = Convert.ToBase64String(BitConverter.GetBytes(i)), + offset, + size = blockSizes[i], + }); + offset += blockSizes[i]; + } + + return new + { + pendingBlocks = blocks, + status = (int)status, + }; + } +} diff --git a/src/PackageUploader.IntegrationTest/Infrastructure/MockServerTestHost.cs b/src/PackageUploader.IntegrationTest/Infrastructure/MockServerTestHost.cs index f7329f9c..1cc5ce0a 100644 --- a/src/PackageUploader.IntegrationTest/Infrastructure/MockServerTestHost.cs +++ b/src/PackageUploader.IntegrationTest/Infrastructure/MockServerTestHost.cs @@ -1,85 +1,102 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using PackageUploader.ClientApi; using PackageUploader.ClientApi.Client.Ingestion.TokenProvider; -using PackageUploader.IntegrationTest.Infrastructure.Mocks; +using PackageUploader.IntegrationTest.FakeApi; namespace PackageUploader.IntegrationTest.Infrastructure; /// -/// Composes the real wired to live WireMock.Net fakes of the -/// Ingestion API and XFUS. The Ingestion base address points at the server; -/// XFUS is reached via the upload domain returned in a stubbed package response (see -/// and ). Authentication uses -/// . Everything else (auth handler, Polly policies, -/// serialization, mappers) runs for real. Dispose to stop both servers and the provider. +/// Hosts a fake-API ASP.NET Core app (Ingestion + XFUS controllers) on a random loopback port via +/// Kestrel, then composes the real pointed at it. The client +/// makes real HTTP calls over a loopback socket — its full pipeline (auth handler, Polly policies, +/// the XFUS SocketsHttpHandler, serialization, mappers) runs for real against the fake app. +/// Authentication uses . Uses only the ASP.NET Core shared +/// framework — no third-party package. /// internal sealed class MockServerTestHost : IDisposable { + private readonly WebApplication _app; private readonly ServiceProvider _provider; private readonly IServiceScope _scope; /// The fake Ingestion API. Configure stubs before exercising the service. - public IngestionMockServer Ingestion { get; } + public IngestionScenarioStore Ingestion { get; } /// The fake XFUS upload service. Configure stubs before exercising the service. - public XfusMockServer Xfus { get; } + public XfusScenarioStore Xfus { get; } - /// The fully composed, public service under test, wired to the fakes. + /// The fully composed, public service under test, wired to the fake app. public IPackageUploaderService Service { get; } + /// The fake app's base URL, also used as the XFUS upload domain in package responses. + public string XfusUploadDomain { get; } + public MockServerTestHost() { - Ingestion = new IngestionMockServer(); - Xfus = new XfusMockServer(); - try - { - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - // Trailing slash so the client's relative paths (e.g. "products/{id}") resolve under it. - ["IngestionConfig:BaseAddress"] = $"{Ingestion.Url}/", - ["IngestionConfig:MedianFirstRetryDelayMs"] = "1", - ["IngestionConfig:RetryCount"] = "3", - }) - .Build(); - - var services = new ServiceCollection(); - services.AddSingleton(configuration); - services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); - - services.AddPackageUploaderService(IngestionExtensions.AuthenticationMethod.Default); - - services.RemoveAll(); - services.AddScoped(); - - _provider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); - _scope = _provider.CreateScope(); - Service = _scope.ServiceProvider.GetRequiredService(); - } - catch - { - // Avoid leaking the started WireMock servers if composition fails. - Xfus.Dispose(); - Ingestion.Dispose(); - throw; - } + Ingestion = new IngestionScenarioStore(); + Xfus = new XfusScenarioStore(); + + _app = BuildFakeApp(Ingestion, Xfus); + _app.StartAsync().GetAwaiter().GetResult(); + XfusUploadDomain = _app.Services.GetRequiredService() + .Features.Get()!.Addresses.First(); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["IngestionConfig:BaseAddress"] = $"{XfusUploadDomain}/", + // Keep retry/timeout fast so retry-scenario tests don't sleep on real backoffs. + ["IngestionConfig:MedianFirstRetryDelayMs"] = "1", + ["IngestionConfig:RetryCount"] = "3", + }) + .Build(); + + var services = new ServiceCollection(); + services.AddSingleton(configuration); + services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); + + services.AddPackageUploaderService(IngestionExtensions.AuthenticationMethod.Default); + + services.RemoveAll(); + services.AddScoped(); + + _provider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); + _scope = _provider.CreateScope(); + Service = _scope.ServiceProvider.GetRequiredService(); } - /// The XFUS server's base URL, for use as the upload domain in a stubbed package response. - public string XfusUploadDomain => Xfus.Url; + private static WebApplication BuildFakeApp(IngestionScenarioStore ingestion, XfusScenarioStore xfus) + { + var builder = WebApplication.CreateBuilder(); + builder.Logging.ClearProviders(); + builder.WebHost.UseUrls("http://127.0.0.1:0"); + + builder.Services.AddSingleton(ingestion); + builder.Services.AddSingleton(xfus); + builder.Services.AddControllers().AddApplicationPart(typeof(IngestionController).Assembly); + + var app = builder.Build(); + app.MapControllers(); + return app; + } public void Dispose() { _scope.Dispose(); _provider.Dispose(); - Ingestion.Dispose(); - Xfus.Dispose(); + _app.StopAsync().GetAwaiter().GetResult(); + ((IAsyncDisposable)_app).DisposeAsync().AsTask().GetAwaiter().GetResult(); } } diff --git a/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/IngestionMockServer.cs b/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/IngestionMockServer.cs deleted file mode 100644 index b8405de7..00000000 --- a/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/IngestionMockServer.cs +++ /dev/null @@ -1,332 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Net; -using WireMock.RequestBuilders; -using WireMock.ResponseBuilders; -using WireMock.Server; - -namespace PackageUploader.IntegrationTest.Infrastructure.Mocks; - -/// -/// A reusable WireMock.Net-backed fake of the Partner Center Ingestion API. Starts a real HTTP -/// server on a random local port and exposes fluent stubs for the endpoints PackageUploader uses, -/// with configurable success / error / retry / polling scenarios. Dispose to stop the server. -/// -internal sealed class IngestionMockServer : IDisposable -{ - private readonly WireMockServer _server; - private readonly string _id = Guid.NewGuid().ToString("N"); - - public IngestionMockServer() => _server = WireMockServer.Start(); - - /// Base URL of the running mock server (e.g. http://localhost:12345). - public string Url => _server.Url!; - - /// The underlying server, for advanced stubbing or request-log assertions. - public WireMockServer Server => _server; - - // ---- GetProduct: GET /products/{id} ---- - - public IngestionMockServer StubGetProduct(string productId, ResponseScenario scenario = ResponseScenario.Success) - { - var request = Request.Create().WithPath($"/products/{productId}").UsingGet(); - if (scenario != ResponseScenario.Success) - { - _server.Given(request).RespondWith(ErrorResponse(scenario)); - return this; - } - - _server.Given(request).RespondWith(JsonResponse(new - { - resourceType = "AzureGameProduct", - name = $"Test Product {productId}", - id = productId, - externalIds = new[] { new { type = "StoreId", value = "9TESTBIGID000" } }, - isModularPublishing = true, - })); - return this; - } - - // ---- GetBranches: GET /products/{id}/branches/getByModule(module=Package) ---- - - public IngestionMockServer StubGetPackageBranches( - string productId, - params (string FriendlyName, string CurrentDraftInstanceId)[] branches) - { - var values = branches.Select(b => new - { - resourceType = "Branch", - friendlyName = b.FriendlyName, - type = "Main", - module = "Package", - currentDraftInstanceId = b.CurrentDraftInstanceId, - }); - - _server - .Given(Request.Create().WithPath("/products/" + productId + "/branches/getByModule*").UsingGet()) - .RespondWith(JsonResponse(new { value = values })); - - _server - .Given(Request.Create().WithPath($"/products/{productId}/flights").UsingGet()) - .RespondWith(JsonResponse(new { value = Array.Empty() })); - return this; - } - - // ---- CreatePackageRequest: POST /products/{id}/packages ---- - - public IngestionMockServer StubCreatePackage( - string productId, - string packageId, - string fileName = "test.xvc", - string? xfusUploadDomain = null, - string? xfusId = null, - string xfusTenant = "DCE", - string xfusToken = "fake-xfus-token") - { - // When an XFUS upload domain is supplied, embed the upload info so the client uploads to the - // XFUS mock; xfusId must be a valid GUID because the client maps it to a Guid. When absent, - // use null (not new {}) so the field is omitted entirely — an empty object would deserialize - // into a non-null upload info with a null XfusId and crash the client's Guid mapping. - object? uploadInfo = xfusUploadDomain is null - ? null - : new - { - fileName, - xfusId = xfusId ?? Guid.NewGuid().ToString(), - token = xfusToken, - uploadDomain = xfusUploadDomain, - xfusTenant, - }; - - _server - .Given(Request.Create().WithPath($"/products/{productId}/packages").UsingPost()) - .RespondWith(JsonResponse(new - { - resourceType = "GamePackage", - id = packageId, - state = "PendingUpload", - fileName, - uploadInfo, - })); - return this; - } - - // ---- GetPackage processing poll: GET /products/{id}/packages/{packageId} ---- - // Returns each state in order across successive calls, then stays on the final state. - - public IngestionMockServer StubGetPackageProcessing( - string productId, - string packageId, - params string[] stateProgression) - { - var states = stateProgression.Length > 0 ? stateProgression : ["Processed"]; - var request = Request.Create().WithPath($"/products/{productId}/packages/{packageId}").UsingGet(); - var scenario = $"package-{productId}-{packageId}-{_id}"; - - for (int i = 0; i < states.Length; i++) - { - bool isLast = i == states.Length - 1; - var body = JsonResponse(new - { - resourceType = "GamePackage", - id = packageId, - state = states[i], - }); - - var builder = _server.Given(request).InScenario(scenario); - if (i > 0) - { - builder = builder.WhenStateIs(StateName(i)); - } - if (!isLast) - { - builder = builder.WillSetStateTo(StateName(i + 1)); - } - else if (i > 0) - { - builder = builder.WillSetStateTo(StateName(i)); - } - - builder.RespondWith(body); - } - - return this; - } - - // ---- GetPackageConfig: GET /products/{id}/packageConfigurations/getByInstanceID(...) and GET/PUT by id ---- - - public IngestionMockServer StubPackageConfiguration( - string productId, - string instanceId, - string configId, - string marketGroupId = "default", - string marketGroupName = "default") - { - var body = JsonResponse(new - { - value = new[] { new { resourceType = "PackageConfiguration", id = configId } }, - }); - - _server - .Given(Request.Create().WithPath("/products/" + productId + "/packageConfigurations/getByInstanceID*").UsingGet()) - .RespondWith(body); - - var single = JsonResponse(new - { - resourceType = "PackageConfiguration", - id = configId, - marketGroupPackages = new[] - { - new { marketGroupId, name = marketGroupName, packageIds = Array.Empty() }, - }, - }); - _server - .Given(Request.Create().WithPath($"/products/{productId}/packageConfigurations/{configId}").UsingGet()) - .RespondWith(single); - _server - .Given(Request.Create().WithPath($"/products/{productId}/packageConfigurations/{configId}").UsingPut()) - .RespondWith(single); - return this; - } - - // ---- ProcessPackage: PUT /products/{id}/packages/{packageId} ---- - - public IngestionMockServer StubProcessPackage(string productId, string packageId, string state = "Uploaded") - { - _server - .Given(Request.Create().WithPath($"/products/{productId}/packages/{packageId}").UsingPut()) - .RespondWith(JsonResponse(new - { - resourceType = "GamePackage", - id = packageId, - state, - })); - return this; - } - - // ---- CreateSubmission: POST /products/{id}/submissions ---- - - public IngestionMockServer StubCreateSubmission(string productId, string submissionId) - { - _server - .Given(Request.Create().WithPath($"/products/{productId}/submissions").UsingPost()) - .RespondWith(JsonResponse(SubmissionBody(submissionId, "InProgress", "Submitted"))); - return this; - } - - // ---- GetSubmission poll: GET /products/{id}/submissions/{id} ---- - // Returns each (state, substate) in order across successive calls, then stays on the final one. - - public IngestionMockServer StubGetSubmission( - string productId, - string submissionId, - params (string State, string Substate)[] progression) - { - var steps = progression.Length > 0 ? progression : [("Published", "InStore")]; - var request = Request.Create().WithPath($"/products/{productId}/submissions/{submissionId}").UsingGet(); - var scenario = $"submission-{productId}-{submissionId}-{_id}"; - - for (int i = 0; i < steps.Length; i++) - { - bool isLast = i == steps.Length - 1; - var body = JsonResponse(SubmissionBody(submissionId, steps[i].State, steps[i].Substate)); - - var builder = _server.Given(request).InScenario(scenario); - if (i > 0) - { - builder = builder.WhenStateIs(StateName(i)); - } - if (!isLast) - { - builder = builder.WillSetStateTo(StateName(i + 1)); - } - else if (i > 0) - { - builder = builder.WillSetStateTo(StateName(i)); - } - - builder.RespondWith(body); - } - - return this; - } - - // ---- Generic scenario primitives ---- - - /// Always responds to the given method/path-wildcard with the supplied status code. - public IngestionMockServer StubError(string method, string pathWildcard, HttpStatusCode statusCode) - { - _server - .Given(Request.Create().WithPath(pathWildcard).UsingMethod(method)) - .RespondWith(Response.Create().WithStatusCode((int)statusCode)); - return this; - } - - /// - /// Fails the first calls with , then - /// succeeds with the given JSON body — used to exercise the client's Polly retry policy. - /// - public IngestionMockServer StubRetryThenSuccess( - string method, - string path, - object successBody, - int failures = 2, - HttpStatusCode failureStatus = HttpStatusCode.InternalServerError) - { - var request = Request.Create().WithPath(path).UsingMethod(method); - var scenario = $"retry-{method}-{path}-{_id}"; - - if (failures <= 0) - { - _server.Given(request).RespondWith(JsonResponse(successBody)); - return this; - } - - for (int i = 0; i < failures; i++) - { - var builder = _server.Given(request).InScenario(scenario); - if (i > 0) - { - builder = builder.WhenStateIs(StateName(i)); - } - builder.WillSetStateTo(StateName(i + 1)) - .RespondWith(Response.Create().WithStatusCode((int)failureStatus)); - } - - _server.Given(request).InScenario(scenario) - .WhenStateIs(StateName(failures)) - .RespondWith(JsonResponse(successBody)); - return this; - } - - public void Dispose() => _server.Stop(); - - // ---- helpers ---- - - private static object SubmissionBody(string submissionId, string state, string substate) => new - { - resourceType = "Submission", - id = submissionId, - state, - substate, - pendingUpdateInfo = new { status = "Completed" }, - }; - - private static string StateName(int index) => $"step-{index}"; - - private static IResponseBuilder JsonResponse(object body) => - Response.Create() - .WithStatusCode(200) - .WithHeader("Content-Type", "application/json") - .WithBodyAsJson(body); - - private static IResponseBuilder ErrorResponse(ResponseScenario scenario) => - Response.Create().WithStatusCode((int)scenario switch - { - _ when scenario == ResponseScenario.ServerError => HttpStatusCode.InternalServerError, - _ when scenario == ResponseScenario.Unauthorized => HttpStatusCode.Unauthorized, - _ when scenario == ResponseScenario.NotFound => HttpStatusCode.NotFound, - _ => HttpStatusCode.InternalServerError, - }); -} diff --git a/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/ResponseScenario.cs b/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/ResponseScenario.cs index 4aaf2f24..b81a5cc3 100644 --- a/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/ResponseScenario.cs +++ b/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/ResponseScenario.cs @@ -4,7 +4,7 @@ namespace PackageUploader.IntegrationTest.Infrastructure.Mocks; /// How a stubbed endpoint should behave for a single, non-stateful response. -internal enum ResponseScenario +public enum ResponseScenario { /// Return a normal 2xx response with a valid body. Success, diff --git a/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/XfusMockServer.cs b/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/XfusMockServer.cs deleted file mode 100644 index f848ffa0..00000000 --- a/src/PackageUploader.IntegrationTest/Infrastructure/Mocks/XfusMockServer.cs +++ /dev/null @@ -1,196 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Net; -using WireMock.RequestBuilders; -using WireMock.ResponseBuilders; -using WireMock.Server; - -namespace PackageUploader.IntegrationTest.Infrastructure.Mocks; - -/// -/// A reusable WireMock.Net-backed fake of the XFUS upload service. Serves the three-step chunked -/// upload conversation PackageUploader performs — initialize, per-block payload PUT, and continue — -/// for both the no-delta and delta upload paths, with configurable success / error / retry -/// scenarios. Dispose to stop the server. -/// -/// -/// The real client's base address is {UploadDomain}/api/v2/assets/, so all stubs are rooted -/// at /api/v2/assets/. Responses omit directUploadParameters.sasUri so the client -/// uploads blocks via the proxy PUT path (which this server serves) rather than to Azure blob -/// storage. The status field is emitted as a number because the client's serializer has no -/// string-enum converter (ReceivingBlocks=0, Busy=1, Completed=2). -/// -internal sealed class XfusMockServer : IDisposable -{ - private const string AssetsRoot = "/api/v2/assets"; - - private readonly WireMockServer _server; - private readonly string _id = Guid.NewGuid().ToString("N"); - - public XfusMockServer() => _server = WireMockServer.Start(); - - /// Base URL of the running mock server (e.g. http://localhost:12345). - public string Url => _server.Url!; - - /// The underlying server, for advanced stubbing or request-log assertions. - public WireMockServer Server => _server; - - /// - /// Configures a complete, successful no-delta upload: initialize returns the given blocks in the - /// ReceivingBlocks state, every block payload PUT succeeds, and continue reports Completed. - /// - public XfusMockServer StubNoDeltaUploadSuccess(params long[] blockSizes) - { - var sizes = blockSizes.Length > 0 ? blockSizes : [64L * 1024]; - StubInitialize(UploadProgress(sizes, XfusStatus.ReceivingBlocks)); - StubBlockUpload(); - StubContinue(UploadProgress([], XfusStatus.Completed)); - return this; - } - - /// Stubs POST .../{assetId}/initialize with the given JSON upload-progress body. - public XfusMockServer StubInitialize(object uploadProgressBody) - { - _server - .Given(Request.Create().WithPath($"{AssetsRoot}/*/initialize").UsingPost()) - .RespondWith(JsonResponse(uploadProgressBody)); - return this; - } - - /// Stubs PUT .../{assetId}/blocks/{blockId}/source/payload to accept block bytes. - public XfusMockServer StubBlockUpload(HttpStatusCode statusCode = HttpStatusCode.OK) - { - _server - .Given(Request.Create().WithPath($"{AssetsRoot}/*/blocks/*/source/payload").UsingPut()) - .RespondWith(Response.Create().WithStatusCode((int)statusCode)); - return this; - } - - /// Stubs POST .../{assetId}/continue with the given JSON upload-progress body. - public XfusMockServer StubContinue(object uploadProgressBody) - { - _server - .Given(Request.Create().WithPath($"{AssetsRoot}/*/continue").UsingPost()) - .RespondWith(JsonResponse(uploadProgressBody)); - return this; - } - - /// - /// Returns the given continue-progress bodies in order across successive calls, then stays on the - /// final one — used to simulate the server working through the upload (e.g. Busy then Completed) - /// or the multi-step delta plan. The progression is keyed per server instance, so it assumes a - /// single asset upload per (the usual one-upload-per-test pattern). - /// - public XfusMockServer StubContinueProgression(params object[] uploadProgressBodies) - { - var bodies = uploadProgressBodies.Length > 0 ? uploadProgressBodies : [UploadProgress([], XfusStatus.Completed)]; - var request = Request.Create().WithPath($"{AssetsRoot}/*/continue").UsingPost(); - var scenario = $"xfus-continue-{_id}"; - - for (int i = 0; i < bodies.Length; i++) - { - bool isLast = i == bodies.Length - 1; - var builder = _server.Given(request).InScenario(scenario); - if (i > 0) - { - builder = builder.WhenStateIs(StateName(i)); - } - if (!isLast) - { - builder = builder.WillSetStateTo(StateName(i + 1)); - } - else if (i > 0) - { - builder = builder.WillSetStateTo(StateName(i)); - } - builder.RespondWith(JsonResponse(bodies[i])); - } - - return this; - } - - /// Always responds to the given method/path-wildcard with the supplied status code. - public XfusMockServer StubError(string method, string pathWildcard, HttpStatusCode statusCode) - { - _server - .Given(Request.Create().WithPath(pathWildcard).UsingMethod(method)) - .RespondWith(Response.Create().WithStatusCode((int)statusCode)); - return this; - } - - /// - /// Fails the first block payload PUTs with , - /// then accepts subsequent ones — used to exercise the uploader's block re-upload behaviour. - /// - public XfusMockServer StubBlockUploadRetryThenSuccess(int failures = 1, HttpStatusCode failureStatus = HttpStatusCode.ServiceUnavailable) - { - var request = Request.Create().WithPath($"{AssetsRoot}/*/blocks/*/source/payload").UsingPut(); - var scenario = $"xfus-block-{_id}"; - - if (failures <= 0) - { - _server.Given(request).RespondWith(Response.Create().WithStatusCode((int)HttpStatusCode.OK)); - return this; - } - - for (int i = 0; i < failures; i++) - { - var builder = _server.Given(request).InScenario(scenario); - if (i > 0) - { - builder = builder.WhenStateIs(StateName(i)); - } - builder.WillSetStateTo(StateName(i + 1)) - .RespondWith(Response.Create().WithStatusCode((int)failureStatus)); - } - - _server.Given(request).InScenario(scenario) - .WhenStateIs(StateName(failures)) - .WillSetStateTo(StateName(failures)) - .RespondWith(Response.Create().WithStatusCode((int)HttpStatusCode.OK)); - return this; - } - - public void Dispose() => _server.Stop(); - - // ---- helpers ---- - - private enum XfusStatus - { - ReceivingBlocks = 0, - Busy = 1, - Completed = 2, - } - - private static object UploadProgress(long[] blockSizes, XfusStatus status) - { - long offset = 0; - var blocks = new List(); - for (long i = 0; i < blockSizes.Length; i++) - { - blocks.Add(new - { - id = i, - blockIdBase64 = Convert.ToBase64String(BitConverter.GetBytes(i)), - offset, - size = blockSizes[i], - }); - offset += blockSizes[i]; - } - - return new - { - pendingBlocks = blocks, - status = (int)status, - }; - } - - private static string StateName(int index) => $"step-{index}"; - - private static IResponseBuilder JsonResponse(object body) => - Response.Create() - .WithStatusCode(200) - .WithHeader("Content-Type", "application/json") - .WithBodyAsJson(body); -} diff --git a/src/PackageUploader.IntegrationTest/PackageUploader.IntegrationTest.csproj b/src/PackageUploader.IntegrationTest/PackageUploader.IntegrationTest.csproj index 37972d8d..829ceda3 100644 --- a/src/PackageUploader.IntegrationTest/PackageUploader.IntegrationTest.csproj +++ b/src/PackageUploader.IntegrationTest/PackageUploader.IntegrationTest.csproj @@ -16,7 +16,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + diff --git a/src/PackageUploader.IntegrationTest/SmokeTest.cs b/src/PackageUploader.IntegrationTest/SmokeTest.cs index 3a729410..b82133d5 100644 --- a/src/PackageUploader.IntegrationTest/SmokeTest.cs +++ b/src/PackageUploader.IntegrationTest/SmokeTest.cs @@ -3,20 +3,19 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using PackageUploader.IntegrationTest.Infrastructure; -using System.Linq; namespace PackageUploader.IntegrationTest; /// /// Smoke test that validates the integration project is discovered, builds, and that the mock-server -/// host routes a public service call over real HTTP to the WireMock Ingestion fake with the fake -/// auth token attached. +/// host routes a public service call over real HTTP to the fake Ingestion API with the fake auth +/// token attached. /// [TestClass] public sealed class SmokeTest : IntegrationTestBase { [TestMethod] - public async Task TestHost_RoutesProductLookup_ThroughWireMockWithFakeAuth() + public async Task TestHost_RoutesProductLookup_ThroughFakeApiWithFakeAuth() { using var host = CreateMockServerHost(); host.Ingestion.StubGetProduct("smoke-test-product"); @@ -26,12 +25,12 @@ public async Task TestHost_RoutesProductLookup_ThroughWireMockWithFakeAuth() Assert.IsNotNull(product); Assert.AreEqual("smoke-test-product", product.ProductId); - var logs = host.Ingestion.Server.LogEntries.ToList(); - Assert.AreEqual(1, logs.Count); + var requests = host.Ingestion.ReceivedRequests; + Assert.AreEqual(1, requests.Count); - var headers = logs[0].RequestMessage!.Headers!; + var headers = requests[0].Headers; Assert.IsTrue(headers.ContainsKey("Authorization")); - StringAssert.Contains(string.Join(" ", headers["Authorization"]!), FakeAccessTokenProvider.FakeToken); + StringAssert.Contains(headers["Authorization"], FakeAccessTokenProvider.FakeToken); } public TestContext TestContext { get; set; } = null!; diff --git a/src/PackageUploader.IntegrationTest/UploadFlowTests.cs b/src/PackageUploader.IntegrationTest/UploadFlowTests.cs index 0d101b62..b189b95d 100644 --- a/src/PackageUploader.IntegrationTest/UploadFlowTests.cs +++ b/src/PackageUploader.IntegrationTest/UploadFlowTests.cs @@ -5,6 +5,8 @@ using PackageUploader.ClientApi.Client.Ingestion.Models; using PackageUploader.IntegrationTest.Fixtures; using PackageUploader.IntegrationTest.Infrastructure; +using System.Linq; +using System.Net.Http; using System.Threading; namespace PackageUploader.IntegrationTest; @@ -55,9 +57,9 @@ public async Task UploadGamePackage_NoDelta_CompletesThroughIngestionAndXfus() // The file was actually uploaded to the XFUS fake: a block payload PUT must have occurred. Assert.IsTrue( - host.Xfus.Server.LogEntries.Any(e => - e.RequestMessage!.Method == "PUT" && - e.RequestMessage.Path.Contains("/source/payload")), + host.Xfus.ReceivedRequests.Any(r => + r.Method == HttpMethod.Put && + r.Path.Contains("/source/payload")), "XFUS fake should have received a block payload PUT"); } } diff --git a/src/PackageUploader.IntegrationTest/packages.lock.json b/src/PackageUploader.IntegrationTest/packages.lock.json index 1559dc72..1d960a7b 100644 --- a/src/PackageUploader.IntegrationTest/packages.lock.json +++ b/src/PackageUploader.IntegrationTest/packages.lock.json @@ -58,1092 +58,78 @@ "MSTest.Analyzers": "4.2.2" } }, - "WireMock.Net": { - "type": "Direct", - "requested": "[2.11.0, )", - "resolved": "2.11.0", - "contentHash": "gQgO9LPslgAPr0cHwHIx2OP3UYZX0Ex11UdAhFyiY2cHBjpxtw9A6GOcF79B56vMFOhYFBHWUgl6fcSFLin0yg==", - "dependencies": { - "WireMock.Net.GraphQL": "2.11.0", - "WireMock.Net.Matchers.SystemTextJsonPath": "2.11.0", - "WireMock.Net.MimePart": "2.11.0", - "WireMock.Net.Minimal": "2.11.0", - "WireMock.Net.OpenTelemetry": "2.11.0", - "WireMock.Net.ProtoBuf": "2.11.0" - } - }, - "AnyOf": { - "type": "Transitive", - "resolved": "0.5.0.1", - "contentHash": "WDQw5Qos3mhCumSCgKD70TM1dmqBAuJFGv1cFtNTwTaDLZR7kGy33M5C+L0vZV/bNRNwyi5ABvRGPWHL17rkNw==" - }, "Azure.Core": { "type": "Transitive", - "resolved": "1.54.0", - "contentHash": "m6hHbx1q9+GCBZ5A9ykzFylPdTwscX2APH7PlnqV+yu+DH3RRtuIDJMRqdU17cMyinv0hCPofpegoyQ6qWPW7g==", - "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "10.0.3", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.3", - "Microsoft.Extensions.Hosting.Abstractions": "10.0.3", - "Microsoft.Identity.Client": "4.83.1", - "Microsoft.Identity.Client.Extensions.Msal": "4.83.1", - "System.ClientModel": "1.10.0", - "System.Memory.Data": "10.0.3" - } - }, - "Azure.Identity": { - "type": "Transitive", - "resolved": "1.21.0", - "contentHash": "GeFv8sGwRKvDKwI2WFy8r0mhmlxEVZg24Sit2NogTjiSO8RVjllWM65OT6e1sKjOvG8V74y7hAbaELUUPjZQSw==", - "dependencies": { - "Azure.Core": "1.53.0" - } - }, - "Azure.Storage.Blobs": { - "type": "Transitive", - "resolved": "12.27.0", - "contentHash": "zI5rg1tTtnA8T2g2/21l+1iIUdDjpEQQ0FI1BabJVEQJ1JUyTQKrc41eNabAHs0SBHprl6pu/6OqIMK9Ve+4tQ==", - "dependencies": { - "Azure.Core": "1.50.0", - "Azure.Storage.Common": "12.26.0" - } - }, - "Azure.Storage.Common": { - "type": "Transitive", - "resolved": "12.26.0", - "contentHash": "XaT6CDcSshZb7KaCTwc6m4EouZbLBg7ciOEpsJSdJCvkNsZJQCvPKw7V5TtXno19AA1NpwtsZriYque8mzbQVg==", - "dependencies": { - "Azure.Core": "1.50.0", - "System.IO.Hashing": "10.0.1" - } - }, - "Castle.Core": { - "type": "Transitive", - "resolved": "5.1.1", - "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", - "dependencies": { - "System.Diagnostics.EventLog": "6.0.0" - } - }, - "Fare": { - "type": "Transitive", - "resolved": "2.2.1", - "contentHash": "21XZo/yuXK1k0EUhdLnjgRD4n0HQYmPFchV6uaORcRc65rasZ1vdm2dmJXPBKZiIBztRRYRmmg/B76W721VWkA==" - }, - "GraphQL": { - "type": "Transitive", - "resolved": "8.5.0", - "contentHash": "BZkfH7GVacTZEkyqa4XN9mW12UA/0XYrpEkkrJnNBf0Pqw8CZWQfItLaWgG2C1Ju9YHH1UUG+LmIpo8iMP6pKA==", - "dependencies": { - "GraphQL-Parser": "9.5.0", - "GraphQL.Analyzers": "8.5.0" - } - }, - "GraphQL-Parser": { - "type": "Transitive", - "resolved": "9.5.0", - "contentHash": "5XWJGKHdVi8pyD4P0EglmJmlXEGs0HzvGlEBf3+/Ve1jLYBBKIOkKvY0Ej17b9Kn1bbBxkrmghqbmsMbkLL1nQ==" - }, - "GraphQL.Analyzers": { - "type": "Transitive", - "resolved": "8.5.0", - "contentHash": "jwfvZD5agmw9J8iZEe6BUfKAY+/lC7EqDQg+6JRwXaQ6G/MCLy8jyBc2SHF/2JdAtrkygc1bVyCUP5mR2PHzVA==" - }, - "GraphQL.NewtonsoftJson": { - "type": "Transitive", - "resolved": "8.5.0", - "contentHash": "tAeUoUhJih5fdZRCV0ue3G/gsu8YBiyNZkgLVFyk0wTk8vJGLTBDJaP5o5LVo1edVnk0bR+0/PaNXAEJxkVrTw==", - "dependencies": { - "GraphQL": "[8.5.0, 9.0.0)", - "Newtonsoft.Json": "13.0.3" - } - }, - "Handlebars.Net": { - "type": "Transitive", - "resolved": "2.1.6", - "contentHash": "WsYWCEXsIM6hEOSOSRHtIYLjC8BnbT5MVmqhNKRqUI7qiv0t8x3nJiBTEv0ZZfvUAMAFnadGIzSsS/U2anVG1Q==" - }, - "Handlebars.Net.Helpers": { - "type": "Transitive", - "resolved": "2.5.5", - "contentHash": "MZ0/Nvy3XdEy/igZD4fJy5HiUKcKUA170Sq+2RmJFsGiWSqlroXzp8TQqJTDCi1aBtETfQOVdBG0YNvuDs9+uQ==", - "dependencies": { - "Handlebars.Net.Helpers.Core": "2.5.5" - } - }, - "Handlebars.Net.Helpers.Core": { - "type": "Transitive", - "resolved": "2.5.5", - "contentHash": "vLTL6UrLUPPiWDCKig8FLhSU+i9J4n/8RfrhadvnvxqziyK0ArxKMT2gLqQ+X/8vJaRcI9zvD5HxA8KjWbq3Dw==", - "dependencies": { - "Handlebars.Net": "2.1.6", - "Stef.Validation": "0.1.1" - } - }, - "Handlebars.Net.Helpers.Humanizer": { - "type": "Transitive", - "resolved": "2.5.5", - "contentHash": "A7TmfLtv7x8HiVckXBmKmOAsO5GKxjSOjxymXS70upqzLLH8BjrhFl+QIGFCdVIWQRx3+yNjGcsz/JXNwt9YZg==", - "dependencies": { - "Handlebars.Net.Helpers": "2.5.5", - "Handlebars.Net.Helpers.Core": "2.5.5", - "Humanizer": "[2.14.1, 4.0.0)" - } - }, - "Handlebars.Net.Helpers.Json": { - "type": "Transitive", - "resolved": "2.5.5", - "contentHash": "iRBo/ik0M8M6ezJt4QzZm5KQptEdeh6bVtnDbieuxh5YPTUsPMFvtoq0gg426PwrahE+5rXoFZmIM11Oy5GwTg==", - "dependencies": { - "Handlebars.Net.Helpers": "2.5.5", - "Handlebars.Net.Helpers.Core": "2.5.5", - "Newtonsoft.Json": "13.0.3" - } - }, - "Handlebars.Net.Helpers.Random": { - "type": "Transitive", - "resolved": "2.5.5", - "contentHash": "zKcfFDN4QxgEjk4Em9yz/PQu0mBpIgEaqjhacg2Fl6M0oSsF7VBVflae2WRM9MtiVeRTwLkVwcy7TvJ6iqFuVQ==", - "dependencies": { - "Handlebars.Net.Helpers": "2.5.5", - "Handlebars.Net.Helpers.Core": "2.5.5", - "RandomDataGenerator.Net": "1.0.19" - } - }, - "Handlebars.Net.Helpers.Xeger": { - "type": "Transitive", - "resolved": "2.5.5", - "contentHash": "J+w9KalIuYlTKMeIv8eoisdoMEz44elri0UOLtfTAuDbADwnBBsJGp4kAQI107+hBcqery9OCRCXm8fvH4eCxQ==", - "dependencies": { - "Fare": "2.2.1", - "Handlebars.Net.Helpers": "2.5.5", - "Handlebars.Net.Helpers.Core": "2.5.5" - } - }, - "Handlebars.Net.Helpers.XPath": { - "type": "Transitive", - "resolved": "2.5.5", - "contentHash": "uUGzjR5w5YCv+BdWQ4RpWAho0tUG0zfAKG5v+abXS6+E+fjbfSshOg7LyoWTVcGTWO0PouukhSMUFaumB2K4tg==", - "dependencies": { - "Handlebars.Net.Helpers": "2.5.5", - "Handlebars.Net.Helpers.Core": "2.5.5", - "XPath2.Extensions": "1.1.5" - } - }, - "Handlebars.Net.Helpers.Xslt": { - "type": "Transitive", - "resolved": "2.5.5", - "contentHash": "bOaX47avO4Uja6jTZcBAgS5KjL/2ZaewCpB0Oy7cVegctPyxiiRx/T44XGSt0133hHry9f5nJVsjFKNLrYq0Pg==", - "dependencies": { - "Handlebars.Net.Helpers": "2.5.5", - "Handlebars.Net.Helpers.Core": "2.5.5" - } - }, - "Humanizer": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "/FUTD3cEceAAmJSCPN9+J+VhGwmL/C12jvwlyM1DFXShEMsBzvLzLqSrJ2rb+k/W2znKw7JyflZgZpyE+tI7lA==", - "dependencies": { - "Humanizer.Core.af": "2.14.1", - "Humanizer.Core.ar": "2.14.1", - "Humanizer.Core.az": "2.14.1", - "Humanizer.Core.bg": "2.14.1", - "Humanizer.Core.bn-BD": "2.14.1", - "Humanizer.Core.cs": "2.14.1", - "Humanizer.Core.da": "2.14.1", - "Humanizer.Core.de": "2.14.1", - "Humanizer.Core.el": "2.14.1", - "Humanizer.Core.es": "2.14.1", - "Humanizer.Core.fa": "2.14.1", - "Humanizer.Core.fi-FI": "2.14.1", - "Humanizer.Core.fr": "2.14.1", - "Humanizer.Core.fr-BE": "2.14.1", - "Humanizer.Core.he": "2.14.1", - "Humanizer.Core.hr": "2.14.1", - "Humanizer.Core.hu": "2.14.1", - "Humanizer.Core.hy": "2.14.1", - "Humanizer.Core.id": "2.14.1", - "Humanizer.Core.is": "2.14.1", - "Humanizer.Core.it": "2.14.1", - "Humanizer.Core.ja": "2.14.1", - "Humanizer.Core.ko-KR": "2.14.1", - "Humanizer.Core.ku": "2.14.1", - "Humanizer.Core.lv": "2.14.1", - "Humanizer.Core.ms-MY": "2.14.1", - "Humanizer.Core.mt": "2.14.1", - "Humanizer.Core.nb": "2.14.1", - "Humanizer.Core.nb-NO": "2.14.1", - "Humanizer.Core.nl": "2.14.1", - "Humanizer.Core.pl": "2.14.1", - "Humanizer.Core.pt": "2.14.1", - "Humanizer.Core.ro": "2.14.1", - "Humanizer.Core.ru": "2.14.1", - "Humanizer.Core.sk": "2.14.1", - "Humanizer.Core.sl": "2.14.1", - "Humanizer.Core.sr": "2.14.1", - "Humanizer.Core.sr-Latn": "2.14.1", - "Humanizer.Core.sv": "2.14.1", - "Humanizer.Core.th-TH": "2.14.1", - "Humanizer.Core.tr": "2.14.1", - "Humanizer.Core.uk": "2.14.1", - "Humanizer.Core.uz-Cyrl-UZ": "2.14.1", - "Humanizer.Core.uz-Latn-UZ": "2.14.1", - "Humanizer.Core.vi": "2.14.1", - "Humanizer.Core.zh-CN": "2.14.1", - "Humanizer.Core.zh-Hans": "2.14.1", - "Humanizer.Core.zh-Hant": "2.14.1" - } - }, - "Humanizer.Core": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" - }, - "Humanizer.Core.af": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "BoQHyu5le+xxKOw+/AUM7CLXneM/Bh3++0qh1u0+D95n6f9eGt9kNc8LcAHLIOwId7Sd5hiAaaav0Nimj3peNw==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.ar": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "3d1V10LDtmqg5bZjWkA/EkmGFeSfNBcyCH+TiHcHP+HGQQmRq3eBaLcLnOJbVQVn3Z6Ak8GOte4RX4kVCxQlFA==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.az": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "8Z/tp9PdHr/K2Stve2Qs/7uqWPWLUK9D8sOZDNzyv42e20bSoJkHFn7SFoxhmaoVLJwku2jp6P7HuwrfkrP18Q==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.bg": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "S+hIEHicrOcbV2TBtyoPp1AVIGsBzlarOGThhQYCnP6QzEYo/5imtok6LMmhZeTnBFoKhM8yJqRfvJ5yqVQKSQ==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.bn-BD": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "U3bfj90tnUDRKlL1ZFlzhCHoVgpTcqUlTQxjvGCaFKb+734TTu3nkHUWVZltA1E/swTvimo/aXLtkxnLFrc0EQ==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.cs": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "jWrQkiCTy3L2u1T86cFkgijX6k7hoB0pdcFMWYaSZnm6rvG/XJE40tfhYyKhYYgIc1x9P2GO5AC7xXvFnFdqMQ==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.da": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "5o0rJyE/2wWUUphC79rgYDnif/21MKTTx9LIzRVz9cjCIVFrJ2bDyR2gapvI9D6fjoyvD1NAfkN18SHBsO8S9g==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.de": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "9JD/p+rqjb8f5RdZ3aEJqbjMYkbk4VFii2QDnnOdNo6ywEfg/A5YeOQ55CaBJmy7KvV4tOK4+qHJnX/tg3Z54A==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.el": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "Xmv6sTL5mqjOWGGpqY7bvbfK5RngaUHSa8fYDGSLyxY9mGdNbDcasnRnMOvi0SxJS9gAqBCn21Xi90n2SHZbFA==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.es": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "e//OIAeMB7pjBV1HqqI4pM2Bcw3Jwgpyz9G5Fi4c+RJvhqFwztoWxW57PzTnNJE2lbhGGLQZihFZjsbTUsbczA==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.fa": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "nzDOj1x0NgjXMjsQxrET21t1FbdoRYujzbmZoR8u8ou5CBWY1UNca0j6n/PEJR/iUbt4IxstpszRy41wL/BrpA==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.fi-FI": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "Vnxxx4LUhp3AzowYi6lZLAA9Lh8UqkdwRh4IE2qDXiVpbo08rSbokATaEzFS+o+/jCNZBmoyyyph3vgmcSzhhQ==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.fr": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "2p4g0BYNzFS3u9SOIDByp2VClYKO0K1ecDV4BkB9EYdEPWfFODYnF+8CH8LpUrpxL2TuWo2fiFx/4Jcmrnkbpg==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.fr-BE": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "o6R3SerxCRn5Ij8nCihDNMGXlaJ/1AqefteAssgmU2qXYlSAGdhxmnrQAXZUDlE4YWt/XQ6VkNLtH7oMqsSPFQ==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.he": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "FPsAhy7Iw6hb+ZitLgYC26xNcgGAHXb0V823yFAzcyoL5ozM+DCJtYfDPYiOpsJhEZmKFTM9No0jUn1M89WGvg==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.hr": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "chnaD89yOlST142AMkAKLuzRcV5df3yyhDyRU5rypDiqrq2HN8y1UR3h1IicEAEtXLoOEQyjSAkAQ6QuXkn7aw==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.hu": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "hAfnaoF9LTGU/CmFdbnvugN4tIs8ppevVMe3e5bD24+tuKsggMc5hYta9aiydI8JH9JnuVmxvNI4DJee1tK05A==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.hy": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "sVIKxOiSBUb4gStRHo9XwwAg9w7TNvAXbjy176gyTtaTiZkcjr9aCPziUlYAF07oNz6SdwdC2mwJBGgvZ0Sl2g==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.id": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "4Zl3GTvk3a49Ia/WDNQ97eCupjjQRs2iCIZEQdmkiqyaLWttfb+cYXDMGthP42nufUL0SRsvBctN67oSpnXtsg==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.is": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "R67A9j/nNgcWzU7gZy1AJ07ABSLvogRbqOWvfRDn4q6hNdbg/mjGjZBp4qCTPnB2mHQQTCKo3oeCUayBCNIBCw==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.it": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "jYxGeN4XIKHVND02FZ+Woir3CUTyBhLsqxu9iqR/9BISArkMf1Px6i5pRZnvq4fc5Zn1qw71GKKoCaHDJBsLFw==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.ja": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "TM3ablFNoYx4cYJybmRgpDioHpiKSD7q0QtMrmpsqwtiiEsdW5zz/q4PolwAczFnvrKpN6nBXdjnPPKVet93ng==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.ko-KR": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "CtvwvK941k/U0r8PGdEuBEMdW6jv/rBiA9tUhakC7Zd2rA/HCnDcbr1DiNZ+/tRshnhzxy/qwmpY8h4qcAYCtQ==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.ku": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "vHmzXcVMe+LNrF9txpdHzpG7XJX65SiN9GQd/Zkt6gsGIIEeECHrkwCN5Jnlkddw2M/b0HS4SNxdR1GrSn7uCA==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.lv": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "E1/KUVnYBS1bdOTMNDD7LV/jdoZv/fbWTLPtvwdMtSdqLyRTllv6PGM9xVQoFDYlpvVGtEl/09glCojPHw8ffA==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.ms-MY": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "vX8oq9HnYmAF7bek4aGgGFJficHDRTLgp/EOiPv9mBZq0i4SA96qVMYSjJ2YTaxs7Eljqit7pfpE2nmBhY5Fnw==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.mt": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "pEgTBzUI9hzemF7xrIZigl44LidTUhNu4x/P6M9sAwZjkUF0mMkbpxKkaasOql7lLafKrnszs0xFfaxQyzeuZQ==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.nb": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "mbs3m6JJq53ssLqVPxNfqSdTxAcZN3njlG8yhJVx83XVedpTe1ECK9aCa8FKVOXv93Gl+yRHF82Hw9T9LWv2hw==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.nb-NO": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "AsJxrrVYmIMbKDGe8W6Z6//wKv9dhWH7RsTcEHSr4tQt/80pcNvLi0hgD3fqfTtg0tWKtgch2cLf4prorEV+5A==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.nl": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "24b0OUdzJxfoqiHPCtYnR5Y4l/s4Oh7KW7uDp+qX25NMAHLCGog2eRfA7p2kRJp8LvnynwwQxm2p534V9m55wQ==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.pl": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "17mJNYaBssENVZyQHduiq+bvdXS0nhZJGEXtPKoMhKv3GD//WO0mEfd9wjEBsWCSmWI7bjRqhCidxzN+YtJmsg==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.pt": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "8HB8qavcVp2la1GJX6t+G9nDYtylPKzyhxr9LAooIei9MnQvNsjEiIE4QvHoeDZ4weuQ9CsPg1c211XUMVEZ4A==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.ro": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "psXNOcA6R8fSHoQYhpBTtTTYiOk8OBoN3PKCEDgsJKIyeY5xuK81IBdGi77qGZMu/OwBRQjQCBMtPJb0f4O1+A==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.ru": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "zm245xUWrajSN2t9H7BTf84/2APbUkKlUJpcdgsvTdAysr1ag9fi1APu6JEok39RRBXDfNRVZHawQ/U8X0pSvQ==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.sk": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "Ncw24Vf3ioRnbU4MsMFHafkyYi8JOnTqvK741GftlQvAbULBoTz2+e7JByOaasqeSi0KfTXeegJO+5Wk1c0Mbw==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.sl": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "l8sUy4ciAIbVThWNL0atzTS2HWtv8qJrsGWNlqrEKmPwA4SdKolSqnTes9V89fyZTc2Q43jK8fgzVE2C7t009A==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.sr": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "rnNvhpkOrWEymy7R/MiFv7uef8YO5HuXDyvojZ7JpijHWA5dXuVXooCOiA/3E93fYa3pxDuG2OQe4M/olXbQ7w==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.sr-Latn": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "nuy/ykpk974F8ItoQMS00kJPr2dFNjOSjgzCwfysbu7+gjqHmbLcYs7G4kshLwdA4AsVncxp99LYeJgoh1JF5g==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.sv": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "E53+tpAG0RCp+cSSI7TfBPC+NnsEqUuoSV0sU+rWRXWr9MbRWx1+Zj02XMojqjGzHjjOrBFBBio6m74seFl0AA==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.th-TH": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "eSevlJtvs1r4vQarNPfZ2kKDp/xMhuD00tVVzRXkSh1IAZbBJI/x2ydxUOwfK9bEwEp+YjvL1Djx2+kw7ziu7g==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.tr": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "rQ8N+o7yFcFqdbtu1mmbrXFi8TQ+uy+fVH9OPI0CI3Cu1om5hUU/GOMC3hXsTCI6d79y4XX+0HbnD7FT5khegA==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.uk": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "2uEfujwXKNm6bdpukaLtEJD+04uUtQD65nSGCetA1fYNizItEaIBUboNfr3GzJxSMQotNwGVM3+nSn8jTd0VSg==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.uz-Cyrl-UZ": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "TD3ME2sprAvFqk9tkWrvSKx5XxEMlAn1sjk+cYClSWZlIMhQQ2Bp/w0VjX1Kc5oeKjxRAnR7vFcLUFLiZIDk9Q==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.uz-Latn-UZ": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "/kHAoF4g0GahnugZiEMpaHlxb+W6jCEbWIdsq9/I1k48ULOsl/J0pxZj93lXC3omGzVF1BTVIeAtv5fW06Phsg==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.vi": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "rsQNh9rmHMBtnsUUlJbShMsIMGflZtPmrMM6JNDw20nhsvqfrdcoDD8cMnLAbuSovtc3dP+swRmLQzKmXDTVPA==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.zh-CN": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "uH2dWhrgugkCjDmduLdAFO9w1Mo0q07EuvM0QiIZCVm6FMCu/lGv2fpMu4GX+4HLZ6h5T2Pg9FIdDLCPN2a67w==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.zh-Hans": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "WH6IhJ8V1UBG7rZXQk3dZUoP2gsi8a0WkL8xL0sN6WGiv695s8nVcmab9tWz20ySQbuzp0UkSxUQFi5jJHIpOQ==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "Humanizer.Core.zh-Hant": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "VIXB7HCUC34OoaGnO3HJVtSv2/wljPhjV7eKH4+TFPgQdJj2lvHNKY41Dtg0Bphu7X5UaXFR4zrYYyo+GNOjbA==", - "dependencies": { - "Humanizer.Core": "[2.14.1]" - } - }, - "JmesPath.Net": { - "type": "Transitive", - "resolved": "1.1.0", - "contentHash": "sL1LeqBm+BWSKvgZN/T470IqkXcKQXmOYsRUZU18jDZeiIBmvUfIe9m3VhiII/jOK/6WmrQ+W8Pqwz3k28WX9g==", - "dependencies": { - "JmesPath.Net.Parser": "1.1.0", - "Newtonsoft.Json": "13.0.4" - } - }, - "JmesPath.Net.Parser": { - "type": "Transitive", - "resolved": "1.1.0", - "contentHash": "NLTE/dPy8lMcZO6E7SL5Jw3fay8Vesll7+hkeRVSRaVNg1RRyPBV3/u6CM7QNgtnzhvyFPDyxUHUuRdh0vzCSg==" - }, - "Json.More.Net": { - "type": "Transitive", - "resolved": "3.0.1", - "contentHash": "fRctF2J2SILYG6wqP21drmeEODmCVkVQ/b3MndDu2fT1swfySyUgq7ePCk+aENGlDcIm05fyfjh9vcuqDEfv3w==" - }, - "JsonConverter.Abstractions": { - "type": "Transitive", - "resolved": "0.13.0", - "contentHash": "Ci3nuKx3GgMDfW9JA4dJpU+hJV5G1ve72mploQP8ivSDpOmo2QbfVAQkxsVzb3UQJCgwxxM6rdE/fPXwM0yj0g==" - }, - "JsonConverter.Newtonsoft.Json": { - "type": "Transitive", - "resolved": "0.13.0", - "contentHash": "K6doeW12emLiJV4laUf58y3kkjng6/IARRtC7+20qIOBrP1pBxGRsjz2IfxCgSBkTML3q6FWe7O+/UE374lsaA==", - "dependencies": { - "JsonConverter.Abstractions": "0.13.0", - "Newtonsoft.Json": "13.0.4", - "Stef.Validation": "0.1.1" - } - }, - "JsonConverter.System.Text.Json": { - "type": "Transitive", - "resolved": "0.13.0", - "contentHash": "UtRbkZT16Z0OVZ9n/h60E0GlUDTul/DjpuuajdsCNvefsmTxf6WNB8Cq9Hjwu9pGjfqOaFOmaz8k54DaHBmc0g==", - "dependencies": { - "JsonConverter.Abstractions": "0.13.0", - "Stef.Validation": "0.1.1" - } - }, - "JsonPath.Net": { - "type": "Transitive", - "resolved": "3.0.2", - "contentHash": "Cmt2mvPYOLljjqSfM1xUYZYTPf8MPbwv2XpCpPxxq9u23/CGrz/fljgd1fJUNujd3+E1adNOyF1TLwppbyQwxg==", - "dependencies": { - "Json.More.Net": "3.0.1" - } - }, - "MetadataReferenceService.Abstractions": { - "type": "Transitive", - "resolved": "0.0.1", - "contentHash": "Sf5ip58vlqWkQIAULIOKFIIFuhtRd8lChsJRZdFo746NVApEp/qgxNf/zCLjbB/RA/8TQGXWrFPKpqjyeh3EMg==", - "dependencies": { - "Microsoft.CodeAnalysis.CSharp": "4.8.0", - "Stef.Validation": "0.1.1" - } - }, - "MetadataReferenceService.Default": { - "type": "Transitive", - "resolved": "0.0.1", - "contentHash": "ihrchqYobpQMA9tn0W+MGD3oe5onqCttbR3lQfEiVzwF0V9/DS+K4YtvsUPGDC9XIie2Xw3lugSSk97k+OUwnQ==", - "dependencies": { - "MetadataReferenceService.Abstractions": "0.0.1" - } - }, - "Microsoft.ApplicationInsights": { - "type": "Transitive", - "resolved": "2.23.0", - "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" - }, - "Microsoft.AspNetCore.Http": { - "type": "Transitive", - "resolved": "2.3.9", - "contentHash": "+CcfWi1LoKYbcxt+3toO4xbBG+qSSMbPuuow+cbZKIrITXuu1geN1traamL4jG8QaHdHGm3M0eCh+EOgdMgNPA==", - "dependencies": { - "Microsoft.AspNetCore.Http.Abstractions": "2.3.0", - "Microsoft.AspNetCore.WebUtilities": "2.3.0", - "Microsoft.Extensions.ObjectPool": "8.0.11", - "Microsoft.Extensions.Options": "8.0.2", - "Microsoft.Net.Http.Headers": "2.3.8" - } - }, - "Microsoft.AspNetCore.Http.Abstractions": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "39r9PPrjA6s0blyFv5qarckjNkaHRA5B+3b53ybuGGNTXEj1/DStQJ4NWjFL6QTRQpL9zt7nDyKxZdJOlcnq+Q==", - "dependencies": { - "Microsoft.AspNetCore.Http.Features": "2.3.0" - } - }, - "Microsoft.AspNetCore.Http.Features": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "f10WUgcsKqrkmnz6gt8HeZ7kyKjYN30PO7cSic1lPtH7paPtnQqXPOveul/SIPI43PhRD4trttg4ywnrEmmJpA==", - "dependencies": { - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Microsoft.AspNetCore.WebUtilities": { - "type": "Transitive", - "resolved": "2.3.0", - "contentHash": "trbXdWzoAEUVd0PE2yTopkz4kjZaAIA7xUWekd5uBw+7xE8Do/YOVTeb9d9koPTlbtZT539aESJjSLSqD8eYrQ==", - "dependencies": { - "Microsoft.Net.Http.Headers": "2.3.0" - } - }, - "Microsoft.Bcl.AsyncInterfaces": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "TV62UsrJZPX6gbt3c4WrtXh7bmaDIcMqf9uft1cc4L6gJXOU07hDGEh+bFQh/L2Az0R1WVOkiT66lFqS6G2NmA==" - }, - "Microsoft.CodeAnalysis.Analyzers": { - "type": "Transitive", - "resolved": "3.3.4", - "contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g==" - }, - "Microsoft.CodeAnalysis.Common": { - "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==", - "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "3.3.4" - } - }, - "Microsoft.CodeAnalysis.CSharp": { - "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "+3+qfdb/aaGD8PZRCrsdobbzGs1m9u119SkkJt8e/mk3xLJz/udLtS2T6nY27OTXxBBw10HzAbC8Z9w08VyP/g==", - "dependencies": { - "Microsoft.CodeAnalysis.Common": "[4.8.0]" - } - }, - "Microsoft.DiaSymReader": { - "type": "Transitive", - "resolved": "2.2.3", - "contentHash": "bhwzJfzyiJM0nXJyNB7Y9OfsEXyxLdDBHG99soIp5JjnPydwkOaBdRCtRtWgQh3noSLi2cSIZ/wpbHNNE9knxQ==" - }, - "Microsoft.Extensions.Configuration": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "wZbGh7J8R1vXN525O6d8dlcDTxhRTnd5MyW4LdfP5S0tSnTwTCseYSrq6g0Mxh7W9xn8P/2xPuf0D/m6k2dy2w==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.7", - "Microsoft.Extensions.Primitives": "10.0.7" - } - }, - "Microsoft.Extensions.Configuration.Abstractions": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "t56nEgvECcyLPojZIUFWJknQQDAbgfTf9J+QMYJE1YYvVgz69vN6B/AKL8Grvj3Lcnp8kTpNqwmwFhb3YLJmtQ==", - "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.7" - } - }, - "Microsoft.Extensions.Configuration.Binder": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "8bS1qIaRivny+WX+49pmeJ6iAylbtX8C0DLEcCQWZjdxQvLqaMssXiGD9P/6pYElrHbK5/nAHmjbQ8STqdMYeg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.7", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.7" - } - }, - "Microsoft.Extensions.Configuration.CommandLine": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "3lNjglxfFxOzI9zG+3HSg/YSGqo//8Fqw6u6iuIamZb4JCorbA3JLaeWOpfKTAPi2UJwaispOXWx14dUqcGz4A==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.7", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.7" - } - }, - "Microsoft.Extensions.Configuration.EnvironmentVariables": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "TWto3imA+mJMLZI+5sbgLiFFoOFNFkizQYNaC5jTuiHKn3diwm1RN7mWDOEZN9kG2bixw7IvgpvtUG5/teSRzA==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.7", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.7" - } - }, - "Microsoft.Extensions.Configuration.FileExtensions": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "qbZLvLsoTdArSloEnSxs21P781YUmwVmHc5NJPQD/ezAreQ7884z+6QfAZVKi86WAZtzx83jK2uC4itxOM44gQ==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.7", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.7", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.7", - "Microsoft.Extensions.FileProviders.Physical": "10.0.7", - "Microsoft.Extensions.Primitives": "10.0.7" - } - }, - "Microsoft.Extensions.Configuration.Json": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "64dimvyyKk0dbUbrLg/YCv4ugJ4sVz2aXLwfvZwR1EC4tJqW9ru/oVRcXwoJRa2lQGXtYtlpk4maWOeIb48tQw==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.7", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.7", - "Microsoft.Extensions.Configuration.FileExtensions": "10.0.7", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.7" - } - }, - "Microsoft.Extensions.Configuration.UserSecrets": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "YqVIICoIdl0016wkeO2WQS+uEbEXbUhMLKdC5rZNl1X3nu59F+nwaAHdHjq/4OK+Cx31DYmNUSFh+MUot8qSDw==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.7", - "Microsoft.Extensions.Configuration.Json": "10.0.7", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.7", - "Microsoft.Extensions.FileProviders.Physical": "10.0.7" - } - }, - "Microsoft.Extensions.DependencyInjection": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "91F/o3emPV/+xY/ip3s2LqDNF14kjttlVtq0BXgg6p4MnCzeSZxnUJm+t6WRrtD3JdGo88/oX+z7OwK4y8PZuw==", + "resolved": "1.54.0", + "contentHash": "m6hHbx1q9+GCBZ5A9ykzFylPdTwscX2APH7PlnqV+yu+DH3RRtuIDJMRqdU17cMyinv0hCPofpegoyQ6qWPW7g==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7" + "Microsoft.Bcl.AsyncInterfaces": "10.0.3", + "Microsoft.Identity.Client": "4.83.1", + "Microsoft.Identity.Client.Extensions.Msal": "4.83.1", + "System.ClientModel": "1.10.0", + "System.Memory.Data": "10.0.3" } }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "Z6mfFEaFcwCfSboxJwOLfu7/31npCY9q70WUamHW/vRQhDvBKOT4Vf9YkZj5J6hLvJpb0oDEYfHunQZj0xxvKw==" - }, - "Microsoft.Extensions.DependencyModel": { - "type": "Transitive", - "resolved": "8.0.2", - "contentHash": "mUBDZZRgZrSyFOsJ2qJJ9fXfqd/kXJwf3AiDoqLD9m6TjY5OO/vLNOb9fb4juC0487eq4hcGN/M2Rh/CKS7QYw==" - }, - "Microsoft.Extensions.Diagnostics": { + "Azure.Identity": { "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "l+smp1qPlU0OUXD0OGfdp7OUFrbdq7ZaP5T7m2WpfZ4RFKD7iG73BAT7tjSMxNmbSXkhAn1jYHOAqzYG1r9sNg==", + "resolved": "1.21.0", + "contentHash": "GeFv8sGwRKvDKwI2WFy8r0mhmlxEVZg24Sit2NogTjiSO8RVjllWM65OT6e1sKjOvG8V74y7hAbaELUUPjZQSw==", "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.7", - "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.7", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.7" + "Azure.Core": "1.53.0" } }, - "Microsoft.Extensions.Diagnostics.Abstractions": { + "Azure.Storage.Blobs": { "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "uJ9JP677y+uy+C0vtaSfi7XXgFAdz8DhU3M9lwwIXDfQKcyQ0yxM9DVYa0NXDtdVTYA2eBUtVFZ8LY0GCdeE/w==", + "resolved": "12.27.0", + "contentHash": "zI5rg1tTtnA8T2g2/21l+1iIUdDjpEQQ0FI1BabJVEQJ1JUyTQKrc41eNabAHs0SBHprl6pu/6OqIMK9Ve+4tQ==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7", - "Microsoft.Extensions.Options": "10.0.7" + "Azure.Core": "1.50.0", + "Azure.Storage.Common": "12.26.0" } }, - "Microsoft.Extensions.FileProviders.Abstractions": { + "Azure.Storage.Common": { "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "teioDgVpi8L186wUfrXQV1YuBt6lCSPmFZiMZo53+FZxHFjOV+f4GXo4LXgJ273Mku9//AdXWVjk9J7eJP6inw==", + "resolved": "12.26.0", + "contentHash": "XaT6CDcSshZb7KaCTwc6m4EouZbLBg7ciOEpsJSdJCvkNsZJQCvPKw7V5TtXno19AA1NpwtsZriYque8mzbQVg==", "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.7" + "Azure.Core": "1.50.0", + "System.IO.Hashing": "10.0.1" } }, - "Microsoft.Extensions.FileProviders.Physical": { + "Castle.Core": { "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "zhgWg/i0ECj5v0jLFBSZHplvc5ygCI91DR4nne+BP4XAKF5ycz0pEKnFiTw8C1jCABJEZsnBZh6pXAvn71kFmw==", - "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.7", - "Microsoft.Extensions.FileSystemGlobbing": "10.0.7", - "Microsoft.Extensions.Primitives": "10.0.7" - } + "resolved": "5.1.1", + "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==" }, - "Microsoft.Extensions.FileSystemGlobbing": { + "Microsoft.ApplicationInsights": { "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "NTUspqB+vH9g4wAD6KPOBx01xqYuKXR/cHXm449zpbq1GqfjdAxBmg7eJXrNsPw7SKwIdT2cJ05GxYVvc+lvsA==" + "resolved": "2.23.0", + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" }, - "Microsoft.Extensions.Hosting": { + "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "M/vBpfWcschvS2EUeq7cHfscsxabiGTptXwV7GeSueovGiSoNjyo1j5PMcWuOAAQrRW3nRqxZk8NeumrmpzUBg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.7", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.7", - "Microsoft.Extensions.Configuration.Binder": "10.0.7", - "Microsoft.Extensions.Configuration.CommandLine": "10.0.7", - "Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.7", - "Microsoft.Extensions.Configuration.FileExtensions": "10.0.7", - "Microsoft.Extensions.Configuration.Json": "10.0.7", - "Microsoft.Extensions.Configuration.UserSecrets": "10.0.7", - "Microsoft.Extensions.DependencyInjection": "10.0.7", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7", - "Microsoft.Extensions.Diagnostics": "10.0.7", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.7", - "Microsoft.Extensions.FileProviders.Physical": "10.0.7", - "Microsoft.Extensions.Hosting.Abstractions": "10.0.7", - "Microsoft.Extensions.Logging": "10.0.7", - "Microsoft.Extensions.Logging.Abstractions": "10.0.7", - "Microsoft.Extensions.Logging.Configuration": "10.0.7", - "Microsoft.Extensions.Logging.Console": "10.0.7", - "Microsoft.Extensions.Logging.Debug": "10.0.7", - "Microsoft.Extensions.Logging.EventLog": "10.0.7", - "Microsoft.Extensions.Logging.EventSource": "10.0.7", - "Microsoft.Extensions.Options": "10.0.7" - } + "resolved": "10.0.3", + "contentHash": "TV62UsrJZPX6gbt3c4WrtXh7bmaDIcMqf9uft1cc4L6gJXOU07hDGEh+bFQh/L2Az0R1WVOkiT66lFqS6G2NmA==" }, - "Microsoft.Extensions.Hosting.Abstractions": { + "Microsoft.DiaSymReader": { "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "5s8d6qC6EA8UOI4wR/+zlsq7SXttJMRb9d7zvVZ7+bE3CQEfVtC9ITUDCommm87R1zzj6WJBbCnztuIJXnP3DA==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.7", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7", - "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.7", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.7", - "Microsoft.Extensions.Logging.Abstractions": "10.0.7" - } + "resolved": "2.2.3", + "contentHash": "bhwzJfzyiJM0nXJyNB7Y9OfsEXyxLdDBHG99soIp5JjnPydwkOaBdRCtRtWgQh3noSLi2cSIZ/wpbHNNE9knxQ==" }, - "Microsoft.Extensions.Http": { + "Microsoft.Extensions.DependencyModel": { "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "1wbd+RPhRo3hJKNJhdGEO5ls0LGe55Ho4BUjlFtRUrWxDVVBd7g0Ydq9fbNy86pmvx/j7AGcSPo7YNCo1IRI6Q==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.7", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7", - "Microsoft.Extensions.Diagnostics": "10.0.7", - "Microsoft.Extensions.Logging": "10.0.7", - "Microsoft.Extensions.Logging.Abstractions": "10.0.7", - "Microsoft.Extensions.Options": "10.0.7" - } + "resolved": "8.0.2", + "contentHash": "mUBDZZRgZrSyFOsJ2qJJ9fXfqd/kXJwf3AiDoqLD9m6TjY5OO/vLNOb9fb4juC0487eq4hcGN/M2Rh/CKS7QYw==" }, "Microsoft.Extensions.Http.Polly": { "type": "Transitive", "resolved": "10.0.7", "contentHash": "pcUsPoqMHvOp+QJsLA/Hlg/W+IBnAoUXKEBc7FqMcY0sUez15DOKXtbEo81TvHL9xwjWQcF3ZMayNpcvpI7Bqg==", "dependencies": { - "Microsoft.Extensions.Http": "10.0.7", "Polly": "7.2.4", "Polly.Extensions.Http": "3.0.0" } }, - "Microsoft.Extensions.Logging": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "hOeRIQ63GkgiYCB/MIFp+LQs8aXpJXpB55t6Aj37ab7t2/6WeFcPXxYM9hdy/o5tffzwf8mhqzLJP6mjGYCxjw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection": "10.0.7", - "Microsoft.Extensions.Logging.Abstractions": "10.0.7", - "Microsoft.Extensions.Options": "10.0.7" - } - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "tIEcQ2gvERrH2KiCjdsVcHGhXt9lIsuDStfOIeZWr7/fP8IXhGiYfx0/80PNI7WPO2IYuFtlZLSlnTS8+/Mchw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7" - } - }, - "Microsoft.Extensions.Logging.Configuration": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "7BBnoGF37USiu7j434put9mDp7EjdlNDIZsR4vHfC1FbLZeLqiWjgJbeEtF0p59Ryqt8AtraHawf0ZKbe5jibg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.7", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.7", - "Microsoft.Extensions.Configuration.Binder": "10.0.7", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7", - "Microsoft.Extensions.Logging": "10.0.7", - "Microsoft.Extensions.Logging.Abstractions": "10.0.7", - "Microsoft.Extensions.Options": "10.0.7", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.7" - } - }, - "Microsoft.Extensions.Logging.Console": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "DA++Es6v6W0HfrOrw+K8WyN6jNnZHp640PDdEvl8yfeVmgflKdn6vSSFvufNUSOuY+M2ZaSUgfY+jUKtNpXcCw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7", - "Microsoft.Extensions.Logging": "10.0.7", - "Microsoft.Extensions.Logging.Abstractions": "10.0.7", - "Microsoft.Extensions.Logging.Configuration": "10.0.7", - "Microsoft.Extensions.Options": "10.0.7" - } - }, - "Microsoft.Extensions.Logging.Debug": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "Y6DSt/JZApunYWKqTtqbdsR6iqAvHx3D0tavbNJ1rnC24MUpF+3XO/VKgFi+9PFqMyvQ2GHBBGb8H3cLSw7rDg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7", - "Microsoft.Extensions.Logging": "10.0.7", - "Microsoft.Extensions.Logging.Abstractions": "10.0.7" - } - }, - "Microsoft.Extensions.Logging.EventLog": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "1C8eTuxF6BLncNSJ1HCfmaBcjpUSqQDPlBVdYTlet9oldHTPpNh9iatxSJLs8TOqdp/FOpH+nSLdBve7fu9mTQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7", - "Microsoft.Extensions.Logging": "10.0.7", - "Microsoft.Extensions.Logging.Abstractions": "10.0.7", - "Microsoft.Extensions.Options": "10.0.7", - "System.Diagnostics.EventLog": "10.0.7" - } - }, - "Microsoft.Extensions.Logging.EventSource": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "YWfndnDX1jVMGCN8d5T+rO+BO8sDw6BkYlUk0BYui+WP7+HhlWx8QLdA4yUDjrkGVb3AQxIWWEPVKw5Nnfj5GQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7", - "Microsoft.Extensions.Logging": "10.0.7", - "Microsoft.Extensions.Logging.Abstractions": "10.0.7", - "Microsoft.Extensions.Options": "10.0.7", - "Microsoft.Extensions.Primitives": "10.0.7" - } - }, - "Microsoft.Extensions.ObjectPool": { - "type": "Transitive", - "resolved": "8.0.11", - "contentHash": "6ApKcHNJigXBfZa6XlDQ8feJpq7SG1ogZXg6M4FiNzgd6irs3LUAzo0Pfn4F2ZI9liGnH1XIBR/OtSbZmJAV5w==" - }, - "Microsoft.Extensions.Options": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "00SHUGTh2jSMvIr6x9Xwd2nE+B5/qFCO/9hDwUDhJsjYRDlADmaBZ7tqehXzBDsfjHSXJzuRHJzPYPPjphBQ7Q==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7", - "Microsoft.Extensions.Primitives": "10.0.7" - } - }, - "Microsoft.Extensions.Options.ConfigurationExtensions": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "IT7f+EMXZtkjatEcF+o6aOw/7OE4etRrMiDGEWH/iiTu2R3uhC4NEQJCfHiibtX45U3sIQ5Fh6tbb1qaOz3YAg==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.7", - "Microsoft.Extensions.Configuration.Binder": "10.0.7", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7", - "Microsoft.Extensions.Options": "10.0.7", - "Microsoft.Extensions.Primitives": "10.0.7" - } - }, - "Microsoft.Extensions.Options.DataAnnotations": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "KWepqdSD4PxhFvVh3mckkvJ03u3q/VChkr6nT3nf5mm2XBk8ojxt2E4It0RMblb3GE7hJ0zQzFzxGKL0d6TfXA==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7", - "Microsoft.Extensions.Options": "10.0.7" - } - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "D5M0Jr551iTgwkZMN9rm0pSkgNLj5quUWQUmQPMZh7k/bnvZTnXRGfE2KuvXf1EEjt/ofD9yw9IumpgdP9QCnw==" - }, "Microsoft.Identity.Client": { "type": "Transitive", "resolved": "4.83.1", @@ -1166,56 +152,6 @@ "resolved": "8.14.0", "contentHash": "iwbCpSjD3ehfTwBhtSNEtKPK0ICun6ov7Ibx6ISNA9bfwIyzI2Siwyi9eJFCJBwxowK9xcA1mj+jBWiigeqgcQ==" }, - "Microsoft.IdentityModel.JsonWebTokens": { - "type": "Transitive", - "resolved": "6.34.0", - "contentHash": "CZMom/ZoWcgjxLMxmCmcEkuoA0OA4swN1CGeMBQyxF/hEZgRbWK9EnWVJ9/oMUq3D1+OGJjnbN+W6gFq9kZcEg==", - "dependencies": { - "Microsoft.IdentityModel.Tokens": "6.34.0" - } - }, - "Microsoft.IdentityModel.Logging": { - "type": "Transitive", - "resolved": "6.34.0", - "contentHash": "E0AbluNkI30/VKa96PxJhhFZDx/NGYIXFrRIRq1N5/V0TToaiuc3hM90QLFszT2BBQefnp/wjm12ilSudmt9bg==", - "dependencies": { - "Microsoft.IdentityModel.Abstractions": "6.34.0" - } - }, - "Microsoft.IdentityModel.Protocols": { - "type": "Transitive", - "resolved": "6.34.0", - "contentHash": "xrqYK+V3FW+fMQ5oI7cwku2wj1RHz8qym3kh+rD+BTgCw1RmfFyWrLQ8/rVEqTl2nn4NcC0N+sHk0Q4qQ8dK9A==", - "dependencies": { - "Microsoft.IdentityModel.Logging": "6.34.0", - "Microsoft.IdentityModel.Tokens": "6.34.0" - } - }, - "Microsoft.IdentityModel.Protocols.OpenIdConnect": { - "type": "Transitive", - "resolved": "6.34.0", - "contentHash": "SN3eZtssgpfnTCUlKsTJn9/0UiSc/HsbGLFl5Xp8vXFLXBeweWiDu54jFngSirjtJd6lSw3GgZhK5LZvVXGGLQ==", - "dependencies": { - "Microsoft.IdentityModel.Protocols": "6.34.0", - "System.IdentityModel.Tokens.Jwt": "6.34.0" - } - }, - "Microsoft.IdentityModel.Tokens": { - "type": "Transitive", - "resolved": "6.34.0", - "contentHash": "PEPcGMqbEwEwbpQ6nTld9Nqq6V5BPZSOfk71qXZ7h7DuGuxa13bWvjImhJba5Ko88YvIuZuOBJWFZmjLfwbNXA==", - "dependencies": { - "Microsoft.IdentityModel.Logging": "6.34.0" - } - }, - "Microsoft.Net.Http.Headers": { - "type": "Transitive", - "resolved": "2.3.8", - "contentHash": "JO60u/VVUdaZfv4XQ//zgcH54y8rnxdpcvXnsDqWLKB4adDKaCiaozixDfQ/6H+PKYfkNV2CL8b8U+F9mciE3Q==", - "dependencies": { - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, "Microsoft.Testing.Extensions.Telemetry": { "type": "Transitive", "resolved": "2.2.2", @@ -1267,91 +203,6 @@ "resolved": "4.2.2", "contentHash": "0VUx09Q6MdPlTCG+xTqEoXIrjr32F1Ya5EI/hfQdRSczZh61AWWtCdGXRCe3DDfUUbPVvFBZTJcrlTT1Cv25Dg==" }, - "Namotion.Reflection": { - "type": "Transitive", - "resolved": "2.1.2", - "contentHash": "7tSHAzX8GWKy0qrW6OgQWD7kAZiqzhq+m1503qczuwuK6ZYhOGCQUxw+F3F4KkRM70aB6RMslsRVSCFeouIehw==" - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "13.0.4", - "contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A==" - }, - "NJsonSchema": { - "type": "Transitive", - "resolved": "10.9.0", - "contentHash": "IBPo6Srxn2MEcIFM3HdM4QImrJbsIeujENQyzHL2Pv6wLsKSYAyAEilecRqaLOhoy3snEiPLx7hhv7opbhOxKQ==", - "dependencies": { - "Namotion.Reflection": "2.1.2", - "Newtonsoft.Json": "9.0.1" - } - }, - "NJsonSchema.Extensions": { - "type": "Transitive", - "resolved": "0.2.0", - "contentHash": "zLHUfuCmnaaQbKxqvTALrxhXV6Pbdy4G3ZlAI+7oaXdJmSyQPpMHGcxDBmw0+qHziT7jVImxU1BjcidKJHeprg==", - "dependencies": { - "NJsonSchema": "10.9.0" - } - }, - "NSwag.Core": { - "type": "Transitive", - "resolved": "13.16.1", - "contentHash": "xiX+H3Bv6zxrqJExPepO5WQVutkDUMdlUA3NqQ8VguwsYwJlkV05eF8XvmbJn/yGJWUag7vLImuXAoj0/327Bg==", - "dependencies": { - "NJsonSchema": "10.7.2", - "Newtonsoft.Json": "9.0.1" - } - }, - "OpenTelemetry": { - "type": "Transitive", - "resolved": "1.15.3", - "contentHash": "N0i6WjPoHPbZyms1ugbDIFAJFuGlpeExJMU/+XSL0lQRUkg/D0utFkDoLXf8Z1km5B+xVZ2GyMXXiX8qdeNmPg==", - "dependencies": { - "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", - "Microsoft.Extensions.Logging.Configuration": "10.0.0", - "OpenTelemetry.Api.ProviderBuilderExtensions": "1.15.3" - } - }, - "OpenTelemetry.Api": { - "type": "Transitive", - "resolved": "1.15.3", - "contentHash": "fX+fkCysfPut+qCcT3bKqyX4QN9Saf4CgX8HLOHywEVD+Xr7sULtfuypITpoDysjx8R59dn/3mWhgimMH8cm/g==" - }, - "OpenTelemetry.Api.ProviderBuilderExtensions": { - "type": "Transitive", - "resolved": "1.15.3", - "contentHash": "SYn0lqYDwLMWhv/zlNGsQcl2yX++yTumanX46bmOZE/ZDOd1WjPBO2kZaZgKLEZTZk48pavIFGJ6vOvxXgWVFQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", - "OpenTelemetry.Api": "1.15.3" - } - }, - "OpenTelemetry.Exporter.OpenTelemetryProtocol": { - "type": "Transitive", - "resolved": "1.15.3", - "contentHash": "FEXJepcseTGbATiCkUfP7ipoFEYYfl/0UmmUwi0KxCPg9PaUA8ab2P1LGopK+/HExasJ1ZutFhZrN6WvUIR23g==", - "dependencies": { - "OpenTelemetry": "1.15.3" - } - }, - "OpenTelemetry.Extensions.Hosting": { - "type": "Transitive", - "resolved": "1.15.3", - "contentHash": "u8n/W8yIlqv0BXZmvId1iVaeWXG42tGKdTkuLYg5g57Y/r9CeUNzqtrSHNdG5IoO8iPX79w3v+WsbAHgUQbfeg==", - "dependencies": { - "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", - "OpenTelemetry": "1.15.3" - } - }, - "OpenTelemetry.Instrumentation.AspNetCore": { - "type": "Transitive", - "resolved": "1.15.2", - "contentHash": "2nPd7r0ug/gd6/CNFL6Rlu+RSQ9WYGSGHAYQ1ssbSqyzKJpqTunfx2I/1O0WB5k+L0cyXbG4XVZpoSoUc3M7wg==", - "dependencies": { - "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.3, 2.0.0)" - } - }, "Polly": { "type": "Transitive", "resolved": "7.2.4", @@ -1370,73 +221,11 @@ "Polly": "7.1.0" } }, - "protobuf-net": { - "type": "Transitive", - "resolved": "3.2.52", - "contentHash": "XbZurNU3B/VaL/5OJ0kshO+AWxsZroI1saKuLfZpDwH2ngb2K9bdF1nIW6elFOViZw7TQCmfVZapxrMKCDqecQ==", - "dependencies": { - "protobuf-net.Core": "3.2.52" - } - }, - "protobuf-net.Core": { - "type": "Transitive", - "resolved": "3.2.52", - "contentHash": "zOpGtUo2QTgbsiI0D0yCe8aUTgDPov6kqIu1CDHI6isqhYcAHdirRrdnfsQXmAUfAWx1LwVYGgC6xe6fNS4UAg==" - }, - "ProtoBufJsonConverter": { - "type": "Transitive", - "resolved": "0.11.0", - "contentHash": "lxvcZQlCtgYZpfm9hhAJVZ1jsPkb9g3fyaAOnQLyEu8MiAowEIdED6jzwfjqLqOhY4AahqnbfRvZ904Ud43X7w==", - "dependencies": { - "MetadataReferenceService.Default": "0.0.1", - "Microsoft.CodeAnalysis.CSharp": "4.8.0", - "Newtonsoft.Json": "13.0.3", - "Stef.Validation": "0.1.1", - "protobuf-net": "3.2.52" - } - }, - "RamlToOpenApiConverter.SourceOnly": { - "type": "Transitive", - "resolved": "0.11.0", - "contentHash": "uYuENc27+ly789oJITPXGDEjvq/gzXrIZ0STCaN2Gkg6rmYTLPkcQyqXuia6HFzMaNBDEI1+WT5V+G4mYYq6bQ==" - }, - "RandomDataGenerator.Net": { - "type": "Transitive", - "resolved": "1.0.19.1", - "contentHash": "OkAqBA69VbYYg+biX2DYWcucOI/yEivkdJ/XPqife/mQAC0r/NArcAU/EI3i/1oRFUUCjibV0b7qEjK+TYTqDw==", - "dependencies": { - "Fare": "2.2.1", - "Stef.Validation": "0.1.1" - } - }, - "Scriban.Signed": { - "type": "Transitive", - "resolved": "7.2.0", - "contentHash": "AGI7QURe/UR9lsfM53yylQngbe77PngBnpzZQ+QV3wcevP4cydyCT+YAMR4rntWEle7v++xUjmfmtXuAvp6Rtw==" - }, - "SharpYaml": { - "type": "Transitive", - "resolved": "2.1.3", - "contentHash": "qd6kDo2uZPG/AvQ3NS2CXr7fDtP+rjiNQ81No3mFGpn41DcG0FEklgnj/j5hVhgQ6YTgTYbvyzkdDnvT+RtuLQ==" - }, - "SimMetrics.Net": { - "type": "Transitive", - "resolved": "1.0.5", - "contentHash": "LaSDYOJDh2WncgRboqiWtk/Igqoim/LV7v808qBeWY/f36Ol5oEKguEYpKrWw5ap8KYP0SRXf7/v3zil9koY6Q==" - }, - "Stef.Validation": { - "type": "Transitive", - "resolved": "0.3.0", - "contentHash": "OfzmxQMK4eBzmobph43p1NsLTgVAC3XGTcvQS0odhsdL6uS7UsFzBMK6S9mAfIijZdLW4q98aZ3dtTOeTaQo6Q==" - }, "System.ClientModel": { "type": "Transitive", "resolved": "1.10.0", "contentHash": "lBEWs54F5Y5pZ9hC+8z4S/X76957ex+DPk7WecRHlbIHtrPfbRMMlOgI3iDn4Jpb3bSxvBnKaaHoD59auFjlBA==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.3", - "Microsoft.Extensions.Hosting.Abstractions": "10.0.3", - "Microsoft.Extensions.Logging.Abstractions": "10.0.3", "System.Memory.Data": "10.0.3" } }, @@ -1445,20 +234,6 @@ "resolved": "2.0.7", "contentHash": "ih4yNLLF2Ebz85xJJBaPeddLa4d1AekYId7Y1g8oSsEaBHHd/CtyeBJ+tDvQadqeXz7i591K5ry/td+4aaHnQA==" }, - "System.Diagnostics.EventLog": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "WbmDLeTPYhEzXhvYVioTVn/D1XX6bovyny9n5p8Zxtf03+eY385RB818teZm6n+fA63iZNvng0/Np4tLuhkMhQ==" - }, - "System.IdentityModel.Tokens.Jwt": { - "type": "Transitive", - "resolved": "6.34.0", - "contentHash": "c0misfmFT3QxKY+a16PGlj+DtiUzoPaf26m2avyPZaLRc9vlIdLtmovfRY5MqN+y/SEoBSRXrgVaeZGPgFQQ6w==", - "dependencies": { - "Microsoft.IdentityModel.JsonWebTokens": "6.34.0", - "Microsoft.IdentityModel.Tokens": "6.34.0" - } - }, "System.Interactive.Async": { "type": "Transitive", "resolved": "7.0.1", @@ -1487,146 +262,9 @@ "resolved": "4.5.0", "contentHash": "wLBKzFnDCxP12VL9ANydSYhk59fC4cvOr9ypYQLPnAj48NQIhqnjdD2yhP8yEKyBJEjERWS9DisKL7rX5eU25Q==" }, - "TinyMapper.Signed": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "W5uc9QXp8PUgP3VQ1Qyt3vK8ptyjj38tJ7nEAtRKA6R/4e6+2gsgYrAmRg9fCK1hhe3E0yeAm5acC14qx2CINg==" - }, - "WireMock.Net.Abstractions": { - "type": "Transitive", - "resolved": "2.11.0", - "contentHash": "/iy5Jn1QHW1CDmyVj6e+/rhUQpuNHk5Y6Qy6MQSFv48R86YovqZ2Au0VwtL2nVvwbRY0TeMH3XkhARHcynRUPA==" - }, - "WireMock.Net.GraphQL": { - "type": "Transitive", - "resolved": "2.11.0", - "contentHash": "QVfnWZrjI9P9v3pOzfDByZ0IN4JmV91jo+H6jz5heJ2dWPFFmmqLpxIc2Y2ExuJGYQAJmvNYAMC3/2YSXK7+wQ==", - "dependencies": { - "GraphQL.NewtonsoftJson": "8.5.0", - "WireMock.Net.Shared": "2.11.0" - } - }, - "WireMock.Net.Matchers.SystemTextJsonPath": { - "type": "Transitive", - "resolved": "2.11.0", - "contentHash": "B8qofF1bdEgtwHRgswtOJkZxHmLmmf8RIH4Jk6KJSHn4QKwwMrtPdEOD632/03P8n93O2K2mfd8PDsovLQ7gcQ==", - "dependencies": { - "JsonPath.Net": "3.0.2", - "WireMock.Net.Shared": "2.11.0" - } - }, - "WireMock.Net.MimePart": { - "type": "Transitive", - "resolved": "2.11.0", - "contentHash": "+aCR6r3cx+ZvS0hhu5CUjNAstub5Sy1JmqFC7q3QQK82yazlH7OB4Dzh1rTJH+BXcxoWKC91nqXgt6xfD8Uv8g==", - "dependencies": { - "Stef.Validation": "0.3.0", - "WireMock.Net.Shared": "2.11.0" - } - }, - "WireMock.Net.Minimal": { - "type": "Transitive", - "resolved": "2.11.0", - "contentHash": "NmQrlaNiZRx535OFUJYCG3+HZRXEi0rom4mN+09qas6RVaZgnh+ChxnWFIdOS8rXoTRlpDmFHG1fdUVHHX4mhA==", - "dependencies": { - "JmesPath.Net": "1.1.0", - "Microsoft.IdentityModel.Protocols.OpenIdConnect": "6.34.0", - "NJsonSchema.Extensions": "0.2.0", - "NSwag.Core": "13.16.1", - "Scriban.Signed": "7.2.0", - "SimMetrics.Net": "1.0.5", - "TinyMapper.Signed": "4.0.0", - "WireMock.Net.OpenApiParser": "2.11.0", - "WireMock.Net.Shared": "2.11.0", - "WireMock.Org.Abstractions": "2.11.0" - } - }, - "WireMock.Net.OpenApiParser": { - "type": "Transitive", - "resolved": "2.11.0", - "contentHash": "b3awrEjgWG6iPCT2wO5Jdye6JaGlCeGoCrgtaeucP0GVYn/BmeNGqKU6BGpIsx7J0+W6YBj0QgpNQp7/nRgeaw==", - "dependencies": { - "Newtonsoft.Json": "13.0.4", - "RamlToOpenApiConverter.SourceOnly": "0.11.0", - "RandomDataGenerator.Net": "1.0.19.1", - "SharpYaml": "2.1.3", - "Stef.Validation": "0.3.0", - "WireMock.Net.Abstractions": "2.11.0", - "YamlDotNet": "16.3.0" - } - }, - "WireMock.Net.OpenTelemetry": { - "type": "Transitive", - "resolved": "2.11.0", - "contentHash": "T+ShaY7mI82RhQmCUrs5n5ZX67xlTyIfC1/gNiftca/wiaXCctj+b8gt7tr7z+LuKroTnMRQDuOFdHhBKVjUQQ==", - "dependencies": { - "OpenTelemetry.Exporter.OpenTelemetryProtocol": "1.15.3", - "OpenTelemetry.Extensions.Hosting": "1.15.3", - "OpenTelemetry.Instrumentation.AspNetCore": "1.15.2", - "WireMock.Net.Shared": "2.11.0" - } - }, - "WireMock.Net.ProtoBuf": { - "type": "Transitive", - "resolved": "2.11.0", - "contentHash": "cWJTxP4oG9PZoOQDTzRNbuJX8mY/MtmjMCiwnSx2h6Vm33HxHFngdmHdgK4ZjbZfnqq6gKIXPel+DvEExXa6lQ==", - "dependencies": { - "ProtoBufJsonConverter": "0.11.0", - "WireMock.Net.Shared": "2.11.0" - } - }, - "WireMock.Net.Shared": { - "type": "Transitive", - "resolved": "2.11.0", - "contentHash": "WvVfCUHP6noNmNdpCWuWeBok44QFjgwQITGoTiMd0v40fCB85SKJU/1uocP8ZgRABELBMbbLALsIuz31KdeNcA==", - "dependencies": { - "AnyOf": "0.5.0.1", - "Handlebars.Net.Helpers": "2.5.5", - "Handlebars.Net.Helpers.Humanizer": "2.5.5", - "Handlebars.Net.Helpers.Json": "2.5.5", - "Handlebars.Net.Helpers.Random": "2.5.5", - "Handlebars.Net.Helpers.XPath": "2.5.5", - "Handlebars.Net.Helpers.Xeger": "2.5.5", - "Handlebars.Net.Helpers.Xslt": "2.5.5", - "JsonConverter.Newtonsoft.Json": "0.13.0", - "JsonConverter.System.Text.Json": "0.13.0", - "Microsoft.AspNetCore.Http": "2.3.9", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", - "Stef.Validation": "0.3.0", - "WireMock.Net.Abstractions": "2.11.0" - } - }, - "WireMock.Org.Abstractions": { - "type": "Transitive", - "resolved": "2.11.0", - "contentHash": "ERlcOKHCgOblIaph2QZYpj9wrIwHRirwHFRqzfo4qDUVxH3QB8PoxuVEaL6LGFUM9gKBilrdvs4ulsxKiwjqwQ==" - }, - "XPath2": { - "type": "Transitive", - "resolved": "1.1.5", - "contentHash": "LQg7kZyAmmb+qvv5TiOuuijxN97rRbR05qbMkVIH+i+sx9CA2UNUKGNtdVxWEXOabS8BIwlXm6ox1OOTjvZ6jw==" - }, - "XPath2.Extensions": { - "type": "Transitive", - "resolved": "1.1.5", - "contentHash": "oEbdGUJsF25QL3Vj1GgSlT2xdbxnka5dKcjuA9CouWCV/l9ecSfypOv78B1+YUD8a8w47prLNw2i3ofLNcrbGA==", - "dependencies": { - "Newtonsoft.Json": "13.0.3", - "XPath2": "1.1.5" - } - }, - "YamlDotNet": { - "type": "Transitive", - "resolved": "16.3.0", - "contentHash": "SgMOdxbz8X65z8hraIs6hOEdnkH6hESTAIUa7viEngHOYaH+6q5XJmwr1+yb9vJpNQ19hCQY69xbFsLtXpobQA==" - }, "PackageUploader": { "type": "Project", "dependencies": { - "Microsoft.Extensions.Configuration.Binder": "[10.0.7, )", - "Microsoft.Extensions.Configuration.Json": "[10.0.7, )", - "Microsoft.Extensions.Hosting": "[10.0.7, )", - "Microsoft.Extensions.Options.DataAnnotations": "[10.0.7, )", "PackageUploader.ClientApi": "[1.0.0, )", "PackageUploader.FileLogger": "[1.0.0, )", "System.CommandLine": "[2.0.7, )" @@ -1638,22 +276,13 @@ "Azure.Core": "[1.54.0, )", "Azure.Identity": "[1.21.0, )", "Azure.Storage.Blobs": "[12.27.0, )", - "Microsoft.Extensions.Configuration.Binder": "[10.0.7, )", - "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.7, )", - "Microsoft.Extensions.Http": "[10.0.7, )", "Microsoft.Extensions.Http.Polly": "[10.0.7, )", - "Microsoft.Extensions.Options.ConfigurationExtensions": "[10.0.7, )", - "Microsoft.Extensions.Options.DataAnnotations": "[10.0.7, )", "Polly.Contrib.WaitAndRetry": "[1.1.1, )", "System.Linq.Async": "[7.0.1, )" } }, "packageuploader.filelogger": { - "type": "Project", - "dependencies": { - "Microsoft.Extensions.Logging": "[10.0.7, )", - "Microsoft.Extensions.Logging.Configuration": "[10.0.7, )" - } + "type": "Project" } } } From 2db5cba0585282f56ce2b50efd4298b825237cb3 Mon Sep 17 00:00:00 2001 From: Elshiekh Ahmed Date: Tue, 30 Jun 2026 13:20:34 -0700 Subject: [PATCH 11/11] Address code review findings on the fake-API prototype (#131) - MockServerTestHost: dispose the Kestrel app if composition throws after StartAsync, to avoid leaking a listener. - FakeApiControllerBase: record binary (octet-stream) request bodies as a size marker instead of a lossy UTF-8 text read. - IngestionScenarioStore: include marketGroupPackages in the package-configuration paged list so the already-initialized path is exercised; default StubProcessPackage state to Processed and document that it (not the poll) drives the returned state. - UploadFlowTests: assert the package processing poll GET was issued so the polling stub is load-bearing. --- .../FakeApi/FakeApiControllerBase.cs | 15 +++- .../FakeApi/IngestionScenarioStore.cs | 18 +++-- .../Infrastructure/MockServerTestHost.cs | 68 +++++++++++-------- .../UploadFlowTests.cs | 8 +++ 4 files changed, 74 insertions(+), 35 deletions(-) diff --git a/src/PackageUploader.IntegrationTest/FakeApi/FakeApiControllerBase.cs b/src/PackageUploader.IntegrationTest/FakeApi/FakeApiControllerBase.cs index fe2b3166..3677bd4b 100644 --- a/src/PackageUploader.IntegrationTest/FakeApi/FakeApiControllerBase.cs +++ b/src/PackageUploader.IntegrationTest/FakeApi/FakeApiControllerBase.cs @@ -18,8 +18,19 @@ private protected async Task RespondAsync(ScenarioStore store) string? body = null; if (Request.ContentLength is > 0) { - using var reader = new StreamReader(Request.Body, leaveOpen: true); - body = await reader.ReadToEndAsync(); + // Only decode textual bodies; binary payloads (e.g. XFUS octet-stream block uploads) + // would be corrupted by a UTF-8 text read, so record their size instead. + var contentType = Request.ContentType ?? string.Empty; + if (contentType.Contains("json", StringComparison.OrdinalIgnoreCase) || + contentType.StartsWith("text/", StringComparison.OrdinalIgnoreCase)) + { + using var reader = new StreamReader(Request.Body, leaveOpen: true); + body = await reader.ReadToEndAsync(); + } + else + { + body = $"<{Request.ContentLength} binary bytes>"; + } } var headers = Request.Headers.ToDictionary( diff --git a/src/PackageUploader.IntegrationTest/FakeApi/IngestionScenarioStore.cs b/src/PackageUploader.IntegrationTest/FakeApi/IngestionScenarioStore.cs index 908730c0..7125cc03 100644 --- a/src/PackageUploader.IntegrationTest/FakeApi/IngestionScenarioStore.cs +++ b/src/PackageUploader.IntegrationTest/FakeApi/IngestionScenarioStore.cs @@ -95,8 +95,10 @@ public IngestionScenarioStore StubGetPackageProcessing(string productId, string return this; } - public IngestionScenarioStore StubProcessPackage(string productId, string packageId, string state = "Uploaded") + public IngestionScenarioStore StubProcessPackage(string productId, string packageId, string state = "Processed") { + // Note: WaitForPackageProcessingAsync returns this PUT result, so this 'state' is the package + // state surfaced by UploadGamePackageAsync (the GET poll state only governs loop timing). On(HttpMethod.Put, $"/products/{productId}/packages/{packageId}", () => new FakeResponse(200, new { resourceType = "GamePackage", @@ -113,19 +115,23 @@ public IngestionScenarioStore StubPackageConfiguration( string marketGroupId = "default", string marketGroupName = "default") { + var marketGroupPackages = new[] + { + new { marketGroupId, name = marketGroupName, packageIds = Array.Empty() }, + }; + + // Include marketGroupPackages so the service treats the config as already-initialized (the + // common path) rather than entering first-time initialization. On(HttpMethod.Get, $"/products/{productId}/packageConfigurations/getByInstanceID*", () => new FakeResponse(200, new { - value = new[] { new { resourceType = "PackageConfiguration", id = configId } }, + value = new[] { new { resourceType = "PackageConfiguration", id = configId, marketGroupPackages } }, })); Func single = () => new FakeResponse(200, new { resourceType = "PackageConfiguration", id = configId, - marketGroupPackages = new[] - { - new { marketGroupId, name = marketGroupName, packageIds = Array.Empty() }, - }, + marketGroupPackages, }); On(HttpMethod.Get, $"/products/{productId}/packageConfigurations/{configId}", single); On(HttpMethod.Put, $"/products/{productId}/packageConfigurations/{configId}", single); diff --git a/src/PackageUploader.IntegrationTest/Infrastructure/MockServerTestHost.cs b/src/PackageUploader.IntegrationTest/Infrastructure/MockServerTestHost.cs index 1cc5ce0a..a57c4311 100644 --- a/src/PackageUploader.IntegrationTest/Infrastructure/MockServerTestHost.cs +++ b/src/PackageUploader.IntegrationTest/Infrastructure/MockServerTestHost.cs @@ -50,31 +50,40 @@ public MockServerTestHost() _app = BuildFakeApp(Ingestion, Xfus); _app.StartAsync().GetAwaiter().GetResult(); - XfusUploadDomain = _app.Services.GetRequiredService() - .Features.Get()!.Addresses.First(); - - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - ["IngestionConfig:BaseAddress"] = $"{XfusUploadDomain}/", - // Keep retry/timeout fast so retry-scenario tests don't sleep on real backoffs. - ["IngestionConfig:MedianFirstRetryDelayMs"] = "1", - ["IngestionConfig:RetryCount"] = "3", - }) - .Build(); - - var services = new ServiceCollection(); - services.AddSingleton(configuration); - services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); - - services.AddPackageUploaderService(IngestionExtensions.AuthenticationMethod.Default); - - services.RemoveAll(); - services.AddScoped(); - - _provider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); - _scope = _provider.CreateScope(); - Service = _scope.ServiceProvider.GetRequiredService(); + try + { + XfusUploadDomain = _app.Services.GetRequiredService() + .Features.Get()!.Addresses.First(); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["IngestionConfig:BaseAddress"] = $"{XfusUploadDomain}/", + // Keep retry/timeout fast so retry-scenario tests don't sleep on real backoffs. + ["IngestionConfig:MedianFirstRetryDelayMs"] = "1", + ["IngestionConfig:RetryCount"] = "3", + }) + .Build(); + + var services = new ServiceCollection(); + services.AddSingleton(configuration); + services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); + + services.AddPackageUploaderService(IngestionExtensions.AuthenticationMethod.Default); + + services.RemoveAll(); + services.AddScoped(); + + _provider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); + _scope = _provider.CreateScope(); + Service = _scope.ServiceProvider.GetRequiredService(); + } + catch + { + // Avoid leaking the started Kestrel listener if composition fails. + StopApp(); + throw; + } } private static WebApplication BuildFakeApp(IngestionScenarioStore ingestion, XfusScenarioStore xfus) @@ -94,8 +103,13 @@ private static WebApplication BuildFakeApp(IngestionScenarioStore ingestion, Xfu public void Dispose() { - _scope.Dispose(); - _provider.Dispose(); + _scope?.Dispose(); + _provider?.Dispose(); + StopApp(); + } + + private void StopApp() + { _app.StopAsync().GetAwaiter().GetResult(); ((IAsyncDisposable)_app).DisposeAsync().AsTask().GetAwaiter().GetResult(); } diff --git a/src/PackageUploader.IntegrationTest/UploadFlowTests.cs b/src/PackageUploader.IntegrationTest/UploadFlowTests.cs index b189b95d..c2b306e3 100644 --- a/src/PackageUploader.IntegrationTest/UploadFlowTests.cs +++ b/src/PackageUploader.IntegrationTest/UploadFlowTests.cs @@ -61,5 +61,13 @@ public async Task UploadGamePackage_NoDelta_CompletesThroughIngestionAndXfus() r.Method == HttpMethod.Put && r.Path.Contains("/source/payload")), "XFUS fake should have received a block payload PUT"); + + // The package processing poll was actually issued (a GET on the package), so the polling + // stub is load-bearing rather than incidentally satisfied by the process (PUT) result. + Assert.IsTrue( + host.Ingestion.ReceivedRequests.Any(r => + r.Method == HttpMethod.Get && + r.Path.Contains($"/packages/{packageId}")), + "Ingestion fake should have received the package processing poll"); } }