From e5cf4427b8cde0d94d02247ab09721520b79a34e Mon Sep 17 00:00:00 2001 From: Elshiekh Ahmed Date: Wed, 24 Jun 2026 09:51:26 -0700 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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;