Skip to content

Commit 51576d4

Browse files
azchohfiCopilot
andcommitted
Stop publishing when the submission carries no pricing
Addresses Copilot review feedback on #175. TryPreservePricing treated a missing pricing object as preservable and let it through, but the API rejects such an update with "Pricing data was not provided in the request.". Publish would have surfaced a raw 400 from deep inside UpdateSubmissionAsync instead of stopping with the same actionable guidance it already gives for a non round-trippable price id. Missing pricing is now handled by the existing stop path, with wording specific to that case, and --priceId still recovers it. The fixtures were modelling something the API never returns. Every real app submission comes back carrying a pricing object, so AddDefaultFakeSubmission now defaults to one instead of to null, which also means the existing publish tests exercise a realistic payload and assert the price survives. A withoutPricing switch covers the degenerate case on purpose. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 06440e13-c898-4e58-8764-d7abf304d9d8
1 parent b72ce89 commit 51576d4

3 files changed

Lines changed: 31 additions & 14 deletions

File tree

MSStore.CLI.UnitTests/BaseCommandLineTest.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -410,14 +410,17 @@ internal void AddFakeAccount(AccountEnrollment? accountEnrollment)
410410
});
411411
}
412412

413-
protected void AddDefaultFakeSubmission(string listingDescription = "BaseListingDescription", Pricing? pricing = null)
413+
protected void AddDefaultFakeSubmission(string listingDescription = "BaseListingDescription", Pricing? pricing = null, bool withoutPricing = false)
414414
{
415415
var fakeSubmission = new DevCenterSubmission
416416
{
417417
Id = "123456789",
418418
ApplicationCategory = DevCenterApplicationCategory.NotSet,
419419
FileUploadUrl = "https://azureblob.com/fileupload",
420-
Pricing = pricing,
420+
421+
// Every real app submission comes back carrying a pricing object, so that is what
422+
// the fixtures model. 'withoutPricing' exists only to cover the degenerate case.
423+
Pricing = withoutPricing ? null : pricing ?? new Pricing { PriceId = PriceIds.Free },
421424
ApplicationPackages =
422425
[
423426
new ApplicationPackage
@@ -574,9 +577,9 @@ internal void InitDefaultFlightSubmissionStatusResponseQueue()
574577
});
575578
}
576579

577-
protected void AddDefaultFakeSuccessfulSubmission(Pricing? pricing = null)
580+
protected void AddDefaultFakeSuccessfulSubmission(Pricing? pricing = null, bool withoutPricing = false)
578581
{
579-
AddDefaultFakeSubmission(pricing: pricing);
582+
AddDefaultFakeSubmission(pricing: pricing, withoutPricing: withoutPricing);
580583
InitDefaultSubmissionStatusResponseQueue();
581584

582585
FakeStorePackagedAPI

MSStore.CLI.UnitTests/PublishCommandPricingUnitTests.cs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,13 @@ public void Init()
3737
private async Task<((string Output, string Error) Result, DevCenterSubmission? Sent)> PublishMsixAsync(
3838
Pricing? pricing,
3939
int? expectedExitCode = 0,
40+
bool withoutPricing = false,
4041
params string[] extraArgs)
4142
{
4243
var path = CopyFilesRecursively("MSIXProject");
4344
var msixPath = Path.Combine(path, "test.msix");
4445

45-
AddDefaultFakeSuccessfulSubmission(pricing);
46+
AddDefaultFakeSuccessfulSubmission(pricing, withoutPricing);
4647

4748
DevCenterSubmission? sent = null;
4849
FakeStorePackagedAPI
@@ -135,6 +136,7 @@ public async Task PublishWithPriceIdShouldRecoverAProductWhoseBasePriceIsNotRoun
135136
var (result, sent) = await PublishMsixAsync(
136137
new Pricing { PriceId = "Base" },
137138
0,
139+
false,
138140
"--priceId",
139141
"Tier1012");
140142

@@ -148,27 +150,34 @@ public async Task PublishWithPriceIdShouldOverrideAnExistingTier()
148150
var (_, sent) = await PublishMsixAsync(
149151
new Pricing { PriceId = "Tier1012" },
150152
0,
153+
false,
151154
"--priceId",
152155
"Tier1424");
153156

154157
sent!.Pricing!.PriceId.Should().Be("Tier1424");
155158
}
156159

157160
[TestMethod]
158-
public async Task PublishShouldSucceedWhenTheProductHasNoPricingAtAll()
161+
public async Task PublishShouldStopWhenTheProductHasNoPricingAtAll()
159162
{
160-
var (result, sent) = await PublishMsixAsync(null);
163+
// Missing pricing is just as unsendable as a bad price id: the API answers
164+
// "Pricing data was not provided in the request.". Fail fast with guidance instead
165+
// of letting UpdateSubmissionAsync surface a raw 400.
166+
var (result, sent) = await PublishMsixAsync(null, -1, withoutPricing: true);
161167

162-
result.Error.Should().Contain("Submission commit success! Here is some data:");
163-
sent!.Pricing.Should().BeNull();
168+
result.Error.Should().Contain("Could not preserve this product's price");
169+
result.Error.Should().Contain("returned no pricing for this product");
170+
result.Error.Should().Contain("--priceId");
171+
172+
sent.Should().BeNull();
164173
}
165174

166175
[TestMethod]
167176
public async Task PublishWithPriceIdShouldApplyEvenWhenTheProductHasNoPricingAtAll()
168177
{
169178
// The API rejects an update that omits pricing, so an explicit price has to be
170179
// materialized rather than silently dropped.
171-
var (result, sent) = await PublishMsixAsync(null, 0, "--priceId", "Tier1012");
180+
var (result, sent) = await PublishMsixAsync(null, 0, true, "--priceId", "Tier1012");
172181

173182
result.Error.Should().Contain("Submission commit success! Here is some data:");
174183
sent!.Pricing.Should().NotBeNull();

MSStore.CLI/Helpers/IStorePackagedAPIExtensions.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -824,16 +824,21 @@ internal static bool TryPreservePricing(IAnsiConsole ansiConsole, DevCenterSubmi
824824
return true;
825825
}
826826

827-
if (submission.Pricing == null || PriceIds.IsRoundTrippable(submission.Pricing.PriceId))
827+
if (submission.Pricing != null && PriceIds.IsRoundTrippable(submission.Pricing.PriceId))
828828
{
829829
return true;
830830
}
831831

832-
var priceId = submission.Pricing.PriceId;
833-
logger.LogError("Cannot preserve the product's price. The API returned PriceId '{PriceId}', which it does not accept back on update.", priceId);
832+
// A missing pricing object is just as unsendable as a bad price id - the API answers
833+
// "Pricing data was not provided in the request." - so stop here with usable guidance
834+
// rather than letting the update fail with a raw 400 further down.
835+
var priceId = submission.Pricing?.PriceId;
836+
logger.LogError("Cannot preserve the product's price. The submission has PriceId '{PriceId}', which the API does not accept on update.", priceId);
834837

835838
ansiConsole.MarkupLine("[red bold]Could not preserve this product's price.[/]");
836-
ansiConsole.MarkupLine($"The Store returned a base price of [yellow]'{(priceId ?? "<empty>").EscapeMarkup()}'[/], which the submission API refuses on update. This happens when the price is managed per market from Partner Center.");
839+
ansiConsole.MarkupLine(submission.Pricing == null
840+
? "The Store returned no pricing for this product, and the submission API rejects an update that does not carry one."
841+
: $"The Store returned a base price of [yellow]'{(priceId ?? "<empty>").EscapeMarkup()}'[/], which the submission API refuses on update. This happens when the price is managed per market from Partner Center.");
837842
ansiConsole.MarkupLine("Publishing would reset the product to [bold]Free[/], so it has been stopped instead.");
838843
ansiConsole.MarkupLine("Re-run with [green]--priceId[/] to state the base price explicitly (for example [green]--priceId Tier1012[/]), or publish this submission from Partner Center.");
839844

0 commit comments

Comments
 (0)