11package appstore
22
33import (
4+ "archive/zip"
5+ "bytes"
46 "errors"
7+ "fmt"
8+ "io"
9+ gohttp "net/http"
10+ "net/http/httptest"
11+ "strconv"
12+ "strings"
13+ "sync/atomic"
514 "time"
615
716 "github.com/majd/ipatool/v2/pkg/http"
817 "github.com/majd/ipatool/v2/pkg/util/machine"
918 . "github.com/onsi/ginkgo/v2"
1019 . "github.com/onsi/gomega"
1120 "go.uber.org/mock/gomock"
21+ "howett.net/plist"
1222)
1323
24+ func testIPA (displayVersion string , releaseDate interface {}, modified time.Time ) []byte {
25+ buffer := new (bytes.Buffer )
26+ zipWriter := zip .NewWriter (buffer )
27+
28+ fillerHeader := & zip.FileHeader {
29+ Name : "Payload/Test.app/Filler.bin" ,
30+ Method : zip .Store ,
31+ }
32+ filler , err := zipWriter .CreateHeader (fillerHeader )
33+ Expect (err ).ToNot (HaveOccurred ())
34+
35+ _ , err = filler .Write (make ([]byte , 1024 * 1024 ))
36+ Expect (err ).ToNot (HaveOccurred ())
37+
38+ infoHeader := & zip.FileHeader {
39+ Name : "Payload/Test.app/Info.plist" ,
40+ Method : zip .Deflate ,
41+ Modified : modified ,
42+ }
43+
44+ infoFile , err := zipWriter .CreateHeader (infoHeader )
45+ Expect (err ).ToNot (HaveOccurred ())
46+
47+ info := map [string ]interface {}{
48+ "CFBundleExecutable" : "Test" ,
49+ "CFBundleShortVersionString" : displayVersion ,
50+ }
51+ if releaseDate != nil {
52+ info ["releaseDate" ] = releaseDate
53+ }
54+
55+ infoData , err := plist .Marshal (info , plist .BinaryFormat )
56+ Expect (err ).ToNot (HaveOccurred ())
57+
58+ _ , err = infoFile .Write (infoData )
59+ Expect (err ).ToNot (HaveOccurred ())
60+
61+ err = zipWriter .Close ()
62+ Expect (err ).ToNot (HaveOccurred ())
63+
64+ return buffer .Bytes ()
65+ }
66+
67+ func testIPAServer (data []byte ) (* httptest.Server , * int64 , * int64 ) {
68+ return testIPAServerWithRangeLog (data , nil )
69+ }
70+
71+ func testIPAServerWithRangeLog (data []byte , rangeLog * []string ) (* httptest.Server , * int64 , * int64 ) {
72+ var (
73+ servedBytes int64
74+ wholeGetCount int64
75+ )
76+
77+ server := httptest .NewServer (gohttp .HandlerFunc (func (w gohttp.ResponseWriter , r * gohttp.Request ) {
78+ if r .Method != gohttp .MethodGet {
79+ w .WriteHeader (gohttp .StatusMethodNotAllowed )
80+
81+ return
82+ }
83+
84+ rangeHeader := r .Header .Get ("Range" )
85+ if rangeLog != nil {
86+ * rangeLog = append (* rangeLog , rangeHeader )
87+ }
88+
89+ if rangeHeader == "" {
90+ atomic .AddInt64 (& wholeGetCount , 1 )
91+ w .WriteHeader (gohttp .StatusOK )
92+ _ , _ = w .Write (data )
93+
94+ return
95+ }
96+
97+ start , end , err := testRangeBounds (rangeHeader , len (data ))
98+ if err != nil {
99+ w .WriteHeader (gohttp .StatusRequestedRangeNotSatisfiable )
100+
101+ return
102+ }
103+
104+ w .Header ().Set ("Accept-Ranges" , "bytes" )
105+ w .Header ().Set ("Content-Range" , fmt .Sprintf ("bytes %d-%d/%d" , start , end , len (data )))
106+ w .Header ().Set ("Content-Length" , strconv .Itoa (end - start + 1 ))
107+ w .WriteHeader (gohttp .StatusPartialContent )
108+
109+ n , _ := w .Write (data [start : end + 1 ])
110+ atomic .AddInt64 (& servedBytes , int64 (n ))
111+ }))
112+
113+ return server , & servedBytes , & wholeGetCount
114+ }
115+
116+ func testRangeBounds (header string , size int ) (int , int , error ) {
117+ if ! strings .HasPrefix (header , "bytes=" ) {
118+ return 0 , 0 , fmt .Errorf ("invalid range header: %s" , header )
119+ }
120+
121+ parts := strings .Split (strings .TrimPrefix (header , "bytes=" ), "-" )
122+ if len (parts ) != 2 {
123+ return 0 , 0 , fmt .Errorf ("invalid range header: %s" , header )
124+ }
125+
126+ start , err := strconv .Atoi (parts [0 ])
127+ if err != nil {
128+ return 0 , 0 , fmt .Errorf ("failed to parse range start: %w" , err )
129+ }
130+
131+ end := size - 1
132+ if parts [1 ] != "" {
133+ end , err = strconv .Atoi (parts [1 ])
134+ if err != nil {
135+ return 0 , 0 , fmt .Errorf ("failed to parse range end: %w" , err )
136+ }
137+ }
138+
139+ if start < 0 || start >= size || end < start {
140+ return 0 , 0 , fmt .Errorf ("invalid range bounds: %s" , header )
141+ }
142+
143+ if end >= size {
144+ end = size - 1
145+ }
146+
147+ return start , end , nil
148+ }
149+
150+ var _ = Describe ("HTTPRangeReaderAt" , func () {
151+ It ("clamps reads that cross EOF" , func () {
152+ data := []byte ("abcdef" )
153+ rangeLog := []string {}
154+ server , _ , _ := testIPAServerWithRangeLog (data , & rangeLog )
155+ defer server .Close ()
156+
157+ reader , size , err := newHTTPRangeReaderAt (http.NewClient [interface {}](http.Args {}), server .URL )
158+ Expect (err ).NotTo (HaveOccurred ())
159+ Expect (size ).To (Equal (int64 (len (data ))))
160+
161+ buf := make ([]byte , 4 )
162+ n , err := reader .ReadAt (buf , 4 )
163+ Expect (n ).To (Equal (2 ))
164+ Expect (err ).To (Equal (io .EOF ))
165+ Expect (string (buf [:n ])).To (Equal ("ef" ))
166+ Expect (rangeLog ).To (ContainElement ("bytes=4-5" ))
167+ })
168+ })
169+
14170var _ = Describe ("AppStore (GetVersionMetadata)" , func () {
15171 var (
16172 ctrl * gomock.Controller
@@ -26,6 +182,7 @@ var _ = Describe("AppStore (GetVersionMetadata)", func() {
26182 as = & appstore {
27183 machine : mockMachine ,
28184 downloadClient : mockDownloadClient ,
185+ httpClient : http.NewClient [interface {}](http.Args {}),
29186 }
30187 })
31188
@@ -227,7 +384,12 @@ var _ = Describe("AppStore (GetVersionMetadata)", func() {
227384 })
228385
229386 When ("fails to parse release date" , func () {
387+ var server * httptest.Server
388+
230389 BeforeEach (func () {
390+ ipa := testIPA ("1.0.0" , "invalid-date" , time .Date (2024 , 3 , 19 , 12 , 0 , 0 , 0 , time .UTC ))
391+ server , _ , _ = testIPAServer (ipa )
392+
231393 mockMachine .EXPECT ().
232394 MacAddress ().
233395 Return ("00:11:22:33:44:55" , nil )
@@ -238,24 +400,36 @@ var _ = Describe("AppStore (GetVersionMetadata)", func() {
238400 Data : downloadResult {
239401 Items : []downloadItemResult {
240402 {
403+ URL : server .URL ,
241404 Metadata : map [string ]interface {}{
242- "releaseDate" : "invalid-date" ,
405+ "bundleShortVersionString" : "1.0.0" ,
406+ "releaseDate" : "invalid-date" ,
243407 },
244408 },
245409 },
246410 },
247411 }, nil )
248412 })
249413
414+ AfterEach (func () {
415+ server .Close ()
416+ })
417+
250418 It ("returns error" , func () {
251419 _ , err := as .GetVersionMetadata (GetVersionMetadataInput {})
252420 Expect (err ).To (HaveOccurred ())
253421 Expect (err .Error ()).To (ContainSubstring ("failed to parse release date" ))
254422 })
255423 })
256424
257- When ("successfully gets version metadata" , func () {
425+ When ("IPA metadata cannot be read" , func () {
426+ var server * httptest.Server
427+
258428 BeforeEach (func () {
429+ server = httptest .NewServer (gohttp .HandlerFunc (func (w gohttp.ResponseWriter , r * gohttp.Request ) {
430+ w .WriteHeader (gohttp .StatusOK )
431+ }))
432+
259433 mockMachine .EXPECT ().
260434 MacAddress ().
261435 Return ("00:11:22:33:44:55" , nil )
@@ -266,8 +440,57 @@ var _ = Describe("AppStore (GetVersionMetadata)", func() {
266440 Data : downloadResult {
267441 Items : []downloadItemResult {
268442 {
443+ URL : server .URL ,
269444 Metadata : map [string ]interface {}{
445+ "bundleShortVersionString" : "1.0.0" ,
270446 "releaseDate" : "2024-03-20T12:00:00Z" ,
447+ },
448+ },
449+ },
450+ },
451+ }, nil )
452+ })
453+
454+ AfterEach (func () {
455+ server .Close ()
456+ })
457+
458+ It ("returns error instead of falling back to API metadata" , func () {
459+ _ , err := as .GetVersionMetadata (GetVersionMetadataInput {})
460+ Expect (err ).To (HaveOccurred ())
461+ Expect (err .Error ()).To (ContainSubstring ("failed to read version metadata" ))
462+ })
463+ })
464+
465+ When ("successfully gets version metadata" , func () {
466+ var (
467+ server * httptest.Server
468+ ipa []byte
469+ servedBytes * int64
470+ wholeGetCount * int64
471+ releaseDate time.Time
472+ displayVersion string
473+ )
474+
475+ BeforeEach (func () {
476+ releaseDate = time .Date (2024 , 4 , 2 , 12 , 0 , 0 , 0 , time .UTC )
477+ displayVersion = "2.0.0"
478+ ipa = testIPA (displayVersion , fmt .Sprintf (" \n %s\t " , releaseDate .Format (time .RFC3339 )), time .Date (2020 , 1 , 1 , 0 , 0 , 0 , 0 , time .UTC ))
479+ server , servedBytes , wholeGetCount = testIPAServer (ipa )
480+
481+ mockMachine .EXPECT ().
482+ MacAddress ().
483+ Return ("00:11:22:33:44:55" , nil )
484+
485+ mockDownloadClient .EXPECT ().
486+ Send (gomock .Any ()).
487+ Return (http.Result [downloadResult ]{
488+ Data : downloadResult {
489+ Items : []downloadItemResult {
490+ {
491+ URL : server .URL ,
492+ Metadata : map [string ]interface {}{
493+ "releaseDate" : "2020-01-01T00:00:00Z" ,
271494 "bundleShortVersionString" : "1.0.0" ,
272495 },
273496 },
@@ -276,6 +499,10 @@ var _ = Describe("AppStore (GetVersionMetadata)", func() {
276499 }, nil )
277500 })
278501
502+ AfterEach (func () {
503+ server .Close ()
504+ })
505+
279506 It ("returns version metadata" , func () {
280507 output , err := as .GetVersionMetadata (GetVersionMetadataInput {
281508 Account : Account {
@@ -288,8 +515,10 @@ var _ = Describe("AppStore (GetVersionMetadata)", func() {
288515 })
289516
290517 Expect (err ).NotTo (HaveOccurred ())
291- Expect (output .DisplayVersion ).To (Equal ("1.0.0" ))
292- Expect (output .ReleaseDate ).To (Equal (time .Date (2024 , 3 , 20 , 12 , 0 , 0 , 0 , time .UTC )))
518+ Expect (output .DisplayVersion ).To (Equal (displayVersion ))
519+ Expect (output .ReleaseDate ).To (Equal (releaseDate ))
520+ Expect (atomic .LoadInt64 (wholeGetCount )).To (BeZero ())
521+ Expect (atomic .LoadInt64 (servedBytes )).To (BeNumerically ("<" , int64 (len (ipa )/ 2 )))
293522 })
294523 })
295524})
0 commit comments