From 59c459c194f1393c6fbd5ac471b98eabf102127a Mon Sep 17 00:00:00 2001 From: Majd Alfhaily Date: Sun, 13 Sep 2026 22:13:11 +0200 Subject: [PATCH] feat: include platforms in search results --- pkg/appstore/appstore_search.go | 70 ++++++++++++++++++++++++++++ pkg/appstore/appstore_search_test.go | 42 ++++++++++++++++- 2 files changed, 110 insertions(+), 2 deletions(-) diff --git a/pkg/appstore/appstore_search.go b/pkg/appstore/appstore_search.go index f1bc9f65..94727432 100644 --- a/pkg/appstore/appstore_search.go +++ b/pkg/appstore/appstore_search.go @@ -1,10 +1,12 @@ package appstore import ( + "encoding/json" "errors" "fmt" gohttp "net/http" "net/url" + "slices" "strconv" "strings" @@ -106,6 +108,15 @@ func (t *appstore) searchVisionOS(input SearchInput, countryCode string) (Search if metadata, ok := hydrated[app.ID]; ok { storefrontApps[index] = metadata } + + platforms := slices.DeleteFunc(storefrontApps[index].Platforms, func(platform Platform) bool { + return platform == PlatformUnknown + }) + if !slices.Contains(platforms, PlatformVisionOS) { + platforms = append(platforms, PlatformVisionOS) + } + + storefrontApps[index].Platforms = platforms } return SearchOutput{ @@ -119,6 +130,65 @@ type searchResult struct { Results []App `json:"results,omitempty"` } +// UnmarshalJSON derives platforms from catalog metadata without exposing the +// catalog's device list in app output. +func (r *searchResult) UnmarshalJSON(data []byte) error { + var result struct { + Count int `json:"resultCount"` + Results []struct { + App + Kind string `json:"kind"` + SupportedDevices []string `json:"supportedDevices"` + } `json:"results"` + } + + if err := json.Unmarshal(data, &result); err != nil { + return fmt.Errorf("failed to decode search results: %w", err) + } + + r.Count = result.Count + r.Results = nil + + if result.Results != nil { + r.Results = make([]App, 0, len(result.Results)) + } + + for _, item := range result.Results { + platforms := item.Platforms + + for _, family := range []struct { + prefix string + platform Platform + }{ + {"iPhone", PlatformIPhone}, + {"iPod", PlatformIPhone}, + {"iPad", PlatformIPad}, + {"AppleTV", PlatformAppleTV}, + {"RealityDevice", PlatformVisionOS}, + {"Mac", PlatformMacOS}, + } { + for _, device := range item.SupportedDevices { + if strings.HasPrefix(device, family.prefix) && !slices.Contains(platforms, family.platform) { + platforms = append(platforms, family.platform) + } + } + } + + if item.Kind == "mac-software" && !slices.Contains(platforms, PlatformMacOS) { + platforms = append(platforms, PlatformMacOS) + } + + if len(platforms) == 0 { + platforms = []Platform{PlatformUnknown} + } + + item.Platforms = platforms + r.Results = append(r.Results, item.App) + } + + return nil +} + func (t *appstore) searchRequest(term, countryCode string, limit int64, platform Platform) (http.Request, error) { url, err := t.searchURL(term, countryCode, limit, platform) if err != nil { diff --git a/pkg/appstore/appstore_search_test.go b/pkg/appstore/appstore_search_test.go index 786bd32e..5473dc74 100644 --- a/pkg/appstore/appstore_search_test.go +++ b/pkg/appstore/appstore_search_test.go @@ -1,6 +1,7 @@ package appstore import ( + "encoding/json" "errors" "net/url" @@ -32,6 +33,27 @@ var _ = Describe("AppStore (Search)", func() { ctrl.Finish() }) + DescribeTable("decodes catalog platforms", + func(metadata string, expected []Platform) { + var result searchResult + Expect(json.Unmarshal([]byte(`{"resultCount":1,"results":[{"trackId":42,`+metadata+`}]}`), &result)).To(Succeed()) + mockClient.EXPECT().Send(gomock.Any()).Return(http.Result[searchResult]{StatusCode: 200, Data: result}, nil) + out, err := as.Search(SearchInput{Account: Account{StoreFront: "143441"}}) + Expect(err).ToNot(HaveOccurred()) + Expect(out.Count).To(Equal(1)) + Expect(out.Results).To(Equal([]App{{ID: 42, Platforms: expected}})) + }, + Entry("universal app with duplicate devices", `"supportedDevices":["iPadAir-iPadAir","iPhone5s-iPhone5s","iPhone6-iPhone6","iPodTouchSixthGen-iPodTouchSixthGen"]`, []Platform{PlatformIPhone, PlatformIPad}), + Entry("iPad only", `"supportedDevices":["iPadAir-iPadAir"]`, []Platform{PlatformIPad}), + Entry("iPod", `"supportedDevices":["iPodTouchSixthGen-iPodTouchSixthGen"]`, []Platform{PlatformIPhone}), + Entry("TV", `"supportedDevices":["AppleTV4-AppleTV4"]`, []Platform{PlatformAppleTV}), + Entry("vision", `"supportedDevices":["RealityDevice-RealityDevice"]`, []Platform{PlatformVisionOS}), + Entry("Mac", `"kind":"mac-software"`, []Platform{PlatformMacOS}), + Entry("multiple families", `"supportedDevices":["MacDesktop-MacDesktop","RealityDevice-RealityDevice","iPadAir-iPadAir"]`, []Platform{PlatformIPad, PlatformVisionOS, PlatformMacOS}), + Entry("missing metadata", `"kind":"software"`, []Platform{PlatformUnknown}), + Entry("unrecognized devices", `"supportedDevices":["FutureDevice"]`, []Platform{PlatformUnknown}), + ) + When("request is successful", func() { const ( testID = 0 @@ -184,12 +206,28 @@ var _ = Describe("AppStore (Search)", func() { Expect(err).ToNot(HaveOccurred()) Expect(out.Count).To(Equal(2)) Expect(out.Results).To(Equal([]App{ - {ID: 200, BundleID: "hydrated.two", Name: "Hydrated Two", Version: "2.0"}, - {ID: 100, BundleID: "hydrated.one", Name: "Hydrated One", Version: "1.0"}, + {ID: 200, BundleID: "hydrated.two", Name: "Hydrated Two", Version: "2.0", Platforms: []Platform{PlatformVisionOS}}, + {ID: 100, BundleID: "hydrated.one", Name: "Hydrated One", Version: "1.0", Platforms: []Platform{PlatformVisionOS}}, })) }) }) + DescribeTable("preserves confirmed visionOS support", + func(metadata []App, expected []Platform) { + page := `` + mockStorefrontClient.EXPECT().Send(gomock.Any()).Return(http.Result[[]byte]{StatusCode: 200, Data: []byte(page)}, nil) + mockClient.EXPECT().Send(gomock.Any()).Return(http.Result[searchResult]{StatusCode: 200, Data: searchResult{Results: metadata}}, nil) + out, err := as.Search(SearchInput{Account: Account{StoreFront: "143441"}, Platform: PlatformVisionOS, Limit: 1}) + Expect(err).ToNot(HaveOccurred()) + Expect(out.Results).To(HaveLen(1)) + Expect(out.Results[0].Platforms).To(Equal(expected)) + }, + Entry("missing lookup result", nil, []Platform{PlatformVisionOS}), + Entry("unknown lookup platforms", []App{{ID: 42, Platforms: []Platform{PlatformUnknown}}}, []Platform{PlatformVisionOS}), + Entry("additional lookup platforms", []App{{ID: 42, Platforms: []Platform{PlatformIPhone, PlatformIPad}}}, []Platform{PlatformIPhone, PlatformIPad, PlatformVisionOS}), + Entry("vision already present", []App{{ID: 42, Platforms: []Platform{PlatformVisionOS}}}, []Platform{PlatformVisionOS}), + ) + When("store front is invalid", func() { It("returns error", func() { _, err := as.Search(SearchInput{