From 78156794313de23a2fd46fa2e5e3071d70eee975 Mon Sep 17 00:00:00 2001 From: Majd Alfhaily Date: Sun, 13 Sep 2026 23:49:05 +0200 Subject: [PATCH] fix: enable update fallback for macos downloads --- pkg/appstore/appstore_download_product.go | 4 +- pkg/appstore/appstore_update_product_test.go | 65 +++++++++++++++++++- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/pkg/appstore/appstore_download_product.go b/pkg/appstore/appstore_download_product.go index d279b824..ecf1798a 100644 --- a/pkg/appstore/appstore_download_product.go +++ b/pkg/appstore/appstore_download_product.go @@ -77,7 +77,7 @@ func (t *appstore) sendDownloadProduct(acc Account, app App, guid, externalVersi redownloadRes, err := t.downloadClient.Send(t.downloadProductRequest(redownload, acc, app, guid, externalVersionID)) if bag.UpdateEndpoint != "" && externalVersionID != "" && - (platform == "" || platform == PlatformIPhone || platform == PlatformIPad) && + (platform == "" || platform == PlatformIPhone || platform == PlatformIPad || platform == PlatformMacOS) && (isEmptyRedownloadError(err) || (err == nil && isUnavailableDownloadProductResponse(redownloadRes))) { updateRes, updateErr := t.sendUpdateProduct(bag.UpdateEndpoint, acc, app, guid, externalVersionID) @@ -108,7 +108,7 @@ func (t *appstore) sendUpdateProduct(endpoint string, acc Account, app App, guid return http.Result[downloadResult]{}, err } - // The bag's updateProduct can serve pinned iOS versions when redownload + // The bag's updateProduct can serve pinned iOS and macOS versions when redownload // returns an empty HTTP 500 or a message-only availability error. Keep // the same session and version selection. res, err := t.downloadClient.Send(t.downloadProductRequest(update, acc, app, guid, externalVersionID)) diff --git a/pkg/appstore/appstore_update_product_test.go b/pkg/appstore/appstore_update_product_test.go index e9851ae6..c4b90a9d 100644 --- a/pkg/appstore/appstore_update_product_test.go +++ b/pkg/appstore/appstore_update_product_test.go @@ -68,7 +68,7 @@ var _ = Describe("AppStore (Update Product)", func() { Return(http.Result[bagResult]{StatusCode: gohttp.StatusOK, Data: bag}, nil) } - DescribeTable("retries once with the same pinned iOS request", + DescribeTable("retries once with the same pinned request", func(versionID string, platform Platform) { previous := expectPrimary() if versionID == "" { @@ -115,6 +115,7 @@ var _ = Describe("AppStore (Update Product)", func() { Entry("latest iPhone", "", PlatformIPhone), Entry("latest iPad", "", PlatformIPad), Entry("explicit historical version", testVersionID, PlatformIPhone), + Entry("explicit macOS version", testVersionID, PlatformMacOS), Entry("explicit version without a platform", testVersionID, Platform("")), ) @@ -157,6 +158,66 @@ var _ = Describe("AppStore (Update Product)", func() { Entry("update also unavailable", false, true), ) + DescribeTable("keeps TestFlight's Mac offer pinned through update fallback", + func(versionID string, emptyRedownload bool, updateUnavailable bool) { + app = App{ID: 899247664, BundleID: "com.apple.TestFlight"} + expected.Data.Items[0].URL = "https://example.com/TestFlight.pkg" + expected.Data.Items[0].Metadata["itemId"] = uint64(app.ID) + expected.Data.Items[0].Metadata["softwareVersionBundleId"] = app.BundleID + expected.Data.Items[0].Metadata["software-platform"] = "macos" + if versionID == "" { + pages := http.NewMockClient[[]byte](ctrl) + store.storefrontClient = pages + pages.EXPECT().Send(gomock.Any()).Do(func(req http.Request) { + Expect(req.URL).To(Equal("https://apps.apple.com/us/app/id899247664?platform=mac")) + }).Return(http.Result[[]byte]{StatusCode: gohttp.StatusOK, Data: macVersionPage( + `{"purchaseConfiguration":{"bundleId":"com.apple.TestFlight","appPlatforms":["mac"],"buyParams":"salableAdamId=899247664&appExtVrsId=123456789"}}`, + )}, nil) + } + unavailable := http.Result[downloadResult]{StatusCode: gohttp.StatusOK, + Data: downloadResult{CustomerMessage: "“TestFlight” No Longer Available"}} + redownload := unavailable + var requestErr error + if emptyRedownload { + redownload = http.Result[downloadResult]{} + requestErr = redownloadErr + } + update := expected + if updateUnavailable { + update = unavailable + } + var redownloadRequest http.Request + gomock.InOrder( + mockDownloadClient.EXPECT().Send(gomock.Any()).Do(func(req http.Request) { + Expect(req.Payload.(*http.XMLPayload).Content).To(HaveKeyWithValue("externalVersionId", testVersionID)) + }).Return(unavailable, nil), + mockBagClient.EXPECT().Send(gomock.Any()).Return(http.Result[bagResult]{StatusCode: gohttp.StatusOK, Data: bag}, nil), + mockDownloadClient.EXPECT().Send(gomock.Any()).Do(func(req http.Request) { + Expect(req.URL).To(Equal(testRedownloadEndpoint + "?guid=" + testGUID)) + redownloadRequest = req + }).Return(redownload, requestErr), + mockDownloadClient.EXPECT().Send(gomock.Any()).Do(func(req http.Request) { + Expect(req.URL).To(Equal(testUpdateEndpoint + "?guid=" + testGUID)) + Expect(req.Headers).To(Equal(redownloadRequest.Headers)) + Expect(req.Payload).To(Equal(redownloadRequest.Payload)) + Expect(req.Payload.(*http.XMLPayload).Content).To(HaveKeyWithValue("appExtVrsId", testVersionID)) + }).Return(update, nil), + ) + actual, platform, err := store.sendDownloadProduct(account, app, testGUID, versionID, PlatformMacOS) + Expect(platform).To(Equal(PlatformMacOS)) + Expect(actual).To(Equal(update)) + if updateUnavailable { + Expect(err).To(MatchError(ContainSubstring(unavailable.Data.CustomerMessage))) + } else { + Expect(err).ToNot(HaveOccurred()) + } + }, + Entry("latest Mac offer after availability errors", "", false, false), + Entry("explicit Mac version after availability errors", testVersionID, false, false), + Entry("latest Mac offer after empty HTTP 500", "", true, false), + Entry("update also unavailable", "", false, true), + ) + DescribeTable("preserves availability responses when update is ineligible", func(platform Platform, updateEndpoint, failureType, message string, status int) { bag.URLBag.UpdateEndpoint = updateEndpoint @@ -170,7 +231,6 @@ var _ = Describe("AppStore (Update Product)", func() { Expect(actual).To(Equal(response)) }, Entry("missing endpoint", PlatformIPhone, "", "", "“Messenger” No Longer Available", 200), - Entry("macOS", PlatformMacOS, testUpdateEndpoint, "", "“Messenger” No Longer Available", 200), Entry("tvOS", PlatformAppleTV, testUpdateEndpoint, "", "“Messenger” No Longer Available", 200), Entry("visionOS", PlatformVisionOS, testUpdateEndpoint, "", "“Messenger” No Longer Available", 200), Entry("license failure", PlatformIPhone, testUpdateEndpoint, FailureTypeLicenseNotFound, "“Messenger” No Longer Available", 200), @@ -241,7 +301,6 @@ var _ = Describe("AppStore (Update Product)", func() { Expect(errors.Is(err, redownloadErr)).To(BeTrue()) Expect(resolvedPlatform).To(Equal(platform)) }, - Entry("pinned macOS", testVersionID, PlatformMacOS), Entry("pinned tvOS", testVersionID, PlatformAppleTV), Entry("pinned visionOS", testVersionID, PlatformVisionOS), )