From f065e96b479b84c3f746bbc322cef86b3dc95c6b Mon Sep 17 00:00:00 2001 From: zebrapurring <103900164+zebrapurring@users.noreply.github.com> Date: Sun, 27 Sep 2026 12:07:41 +0200 Subject: [PATCH 1/2] fix: enable update fallback for tvos downloads --- pkg/appstore/appstore_download_product.go | 4 ++-- pkg/appstore/appstore_update_product_test.go | 24 +++++++++++++------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/pkg/appstore/appstore_download_product.go b/pkg/appstore/appstore_download_product.go index ecf1798a..63786b16 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 == PlatformMacOS) && + (platform == "" || platform == PlatformIPhone || platform == PlatformIPad || platform == PlatformMacOS || platform == PlatformAppleTV) && (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 and macOS versions when redownload + // The bag's updateProduct can serve pinned iOS, macOS, and tvOS 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 c4b90a9d..7ee1eaa9 100644 --- a/pkg/appstore/appstore_update_product_test.go +++ b/pkg/appstore/appstore_update_product_test.go @@ -116,11 +116,12 @@ var _ = Describe("AppStore (Update Product)", func() { Entry("latest iPad", "", PlatformIPad), Entry("explicit historical version", testVersionID, PlatformIPhone), Entry("explicit macOS version", testVersionID, PlatformMacOS), + Entry("explicit tvOS version", testVersionID, PlatformAppleTV), Entry("explicit version without a platform", testVersionID, Platform("")), ) DescribeTable("tries update once for a message-only availability response", - func(primaryUnavailable, updateUnavailable bool) { + func(platform Platform, primaryUnavailable, updateUnavailable bool) { unavailable := http.Result[downloadResult]{StatusCode: gohttp.StatusOK, Data: downloadResult{CustomerMessage: "“Messenger” No Longer Available"}} primary := http.Result[downloadResult]{StatusCode: gohttp.StatusOK} @@ -132,20 +133,25 @@ var _ = Describe("AppStore (Update Product)", func() { update = unavailable } + var redownloadRequest http.Request gomock.InOrder( mockDownloadClient.EXPECT().Send(gomock.Any()).Return(primary, 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)) Expect(req.Payload.(*http.XMLPayload).Content).To(HaveKeyWithValue("appExtVrsId", testVersionID)) + redownloadRequest = req }).Return(unavailable, nil), 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, _, err := store.sendDownloadProduct(account, app, testGUID, testVersionID, PlatformIPhone) + actual, resolvedPlatform, err := store.sendDownloadProduct(account, app, testGUID, testVersionID, platform) + Expect(resolvedPlatform).To(Equal(platform)) Expect(actual).To(Equal(update)) if updateUnavailable { Expect(err).To(MatchError(ContainSubstring(unavailable.Data.CustomerMessage))) @@ -153,9 +159,12 @@ var _ = Describe("AppStore (Update Product)", func() { Expect(err).ToNot(HaveOccurred()) } }, - Entry("redownload unavailable", false, false), - Entry("both initial endpoints unavailable", true, false), - Entry("update also unavailable", false, true), + Entry("redownload unavailable", PlatformIPhone, false, false), + Entry("both initial endpoints unavailable", PlatformIPhone, true, false), + Entry("update also unavailable", PlatformIPhone, false, true), + Entry("tvOS redownload unavailable", PlatformAppleTV, false, false), + Entry("tvOS both initial endpoints unavailable", PlatformAppleTV, true, false), + Entry("tvOS update also unavailable", PlatformAppleTV, false, true), ) DescribeTable("keeps TestFlight's Mac offer pinned through update fallback", @@ -231,7 +240,6 @@ var _ = Describe("AppStore (Update Product)", func() { Expect(actual).To(Equal(response)) }, Entry("missing endpoint", PlatformIPhone, "", "", "“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), Entry("other message", PlatformIPhone, testUpdateEndpoint, "", "Sign in required", 200), @@ -291,7 +299,7 @@ var _ = Describe("AppStore (Update Product)", func() { Expect(err).To(MatchError("invalid download endpoint in bag")) }) - DescribeTable("does not extend the fallback to unverified platforms", + DescribeTable("does not update unpinned tvOS or unverified platforms", func(versionID string, platform Platform) { previous := expectPrimary() mockDownloadClient.EXPECT().Send(gomock.Any()).After(previous). @@ -301,7 +309,7 @@ var _ = Describe("AppStore (Update Product)", func() { Expect(errors.Is(err, redownloadErr)).To(BeTrue()) Expect(resolvedPlatform).To(Equal(platform)) }, - Entry("pinned tvOS", testVersionID, PlatformAppleTV), + Entry("unpinned tvOS", "", PlatformAppleTV), Entry("pinned visionOS", testVersionID, PlatformVisionOS), ) From c288a852c2fe762694b865e5215e049cd560c3bd Mon Sep 17 00:00:00 2001 From: Majd Alfhaily Date: Tue, 29 Sep 2026 23:39:46 +0200 Subject: [PATCH 2/2] test: cover tvos update fallback safeguards --- pkg/appstore/appstore_download_test.go | 3 +++ pkg/appstore/appstore_update_product_test.go | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/pkg/appstore/appstore_download_test.go b/pkg/appstore/appstore_download_test.go index 73e073c7..c488f682 100644 --- a/pkg/appstore/appstore_download_test.go +++ b/pkg/appstore/appstore_download_test.go @@ -828,6 +828,9 @@ var _ = Describe("AppStore (Download)", func() { Entry("normal unspecified-platform behavior is preserved", Platform(""), "", "", "AppleTVOS", true), Entry("an explicit tvOS version without a platform is preserved", Platform(""), "818970197", "", "AppleTVOS", true), Entry("explicit tvOS downloads still work", PlatformAppleTV, "818970197", "", "AppleTVOS", true), + Entry("tvOS update accepts tvOS", PlatformAppleTV, "818970197", "update", "AppleTVOS", true), + Entry("tvOS update rejects iOS", PlatformAppleTV, "818970197", "update", "iPhoneOS", false), + Entry("tvOS update rejects visionOS", PlatformAppleTV, "818970197", "update", "XROS", false), ) Describe("macOS packages", func() { diff --git a/pkg/appstore/appstore_update_product_test.go b/pkg/appstore/appstore_update_product_test.go index 7ee1eaa9..c4bdc8d8 100644 --- a/pkg/appstore/appstore_update_product_test.go +++ b/pkg/appstore/appstore_update_product_test.go @@ -313,6 +313,18 @@ var _ = Describe("AppStore (Update Product)", func() { Entry("pinned visionOS", testVersionID, PlatformVisionOS), ) + It("preserves an unpinned tvOS availability response without updating", func() { + previous := expectPrimary() + response := http.Result[downloadResult]{StatusCode: gohttp.StatusOK, + Data: downloadResult{CustomerMessage: "No Longer Available"}} + mockDownloadClient.EXPECT().Send(gomock.Any()).After(previous).Return(response, nil) + + actual, platform, err := store.sendDownloadProduct(account, app, testGUID, "", PlatformAppleTV) + Expect(err).ToNot(HaveOccurred()) + Expect(actual).To(Equal(response)) + Expect(platform).To(Equal(PlatformAppleTV)) + }) + It("propagates an update error without retrying again", func() { previous := expectPrimary() gomock.InOrder(