go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cipd/appengine/impl/cas/signed_urls_test.go (about) 1 // Copyright 2017 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package cas 16 17 import ( 18 "context" 19 "fmt" 20 "testing" 21 "time" 22 23 "google.golang.org/grpc/codes" 24 25 "go.chromium.org/luci/cipd/appengine/impl/testutil" 26 "go.chromium.org/luci/common/clock/testclock" 27 "go.chromium.org/luci/grpc/grpcutil" 28 "go.chromium.org/luci/server/caching" 29 30 . "github.com/smartystreets/goconvey/convey" 31 . "go.chromium.org/luci/common/testing/assertions" 32 ) 33 34 func TestGetSignedURL(t *testing.T) { 35 t.Parallel() 36 37 Convey("with context", t, func() { 38 ctx := caching.WithEmptyProcessCache(context.Background()) 39 // Use TestRecentTimeUTC, not TestRecentTimeLocal, so the 40 // timestamps in the following tests do not depend on the 41 // local timezone. 42 ctx, cl := testclock.UseTime(ctx, testclock.TestRecentTimeUTC.Local()) 43 44 var signed []byte 45 var signature string 46 var signErr error 47 signer := func(context.Context) (*signer, error) { 48 return &signer{ 49 Email: "test@example.com", 50 SignBytes: func(_ context.Context, data []byte) (key string, sig []byte, err error) { 51 signed = data 52 return "", []byte(signature), signErr 53 }, 54 }, nil 55 } 56 57 Convey("Works", func() { 58 signature = "sig1" 59 url1, size, err := getSignedURL(ctx, "/bucket/path", "", signer, &mockedSignerGS{exists: true}) 60 So(err, ShouldBeNil) 61 So(url1, ShouldEqual, "https://storage.googleapis.com"+ 62 "/bucket/path?Expires=1454479506&"+ 63 "GoogleAccessId=test%40example.com&"+ 64 "Signature=c2lnMQ%3D%3D") 65 So(size, ShouldEqual, 123) 66 So(string(signed), ShouldEqual, "GET\n\n\n1454479506\n/bucket/path") 67 68 // 1h later returns same cached URL. 69 cl.Add(time.Hour) 70 71 signature = "sig2" // must not be picked up 72 url2, _, err := getSignedURL(ctx, "/bucket/path", "", signer, &mockedSignerGS{exists: true}) 73 So(err, ShouldBeNil) 74 So(url2, ShouldEqual, url1) 75 76 // 31min later the cache expires and new link is generated. 77 cl.Add(31 * time.Minute) 78 79 signature = "sig3" 80 url3, _, err := getSignedURL(ctx, "/bucket/path", "", signer, &mockedSignerGS{exists: true}) 81 So(err, ShouldBeNil) 82 So(url3, ShouldEqual, "https://storage.googleapis.com"+ 83 "/bucket/path?Expires=1454484966&"+ 84 "GoogleAccessId=test%40example.com&"+ 85 "Signature=c2lnMw%3D%3D") 86 }) 87 88 Convey("Absence is cached", func() { 89 gs := &mockedSignerGS{exists: false} 90 _, _, err := getSignedURL(ctx, "/bucket/path", "", signer, gs) 91 So(err, ShouldErrLike, "doesn't exist") 92 So(grpcutil.Code(err), ShouldEqual, codes.NotFound) 93 So(gs.calls, ShouldEqual, 1) 94 95 // 30 sec later same check is reused. 96 cl.Add(30 * time.Second) 97 _, _, err = getSignedURL(ctx, "/bucket/path", "", signer, gs) 98 So(err, ShouldErrLike, "doesn't exist") 99 So(gs.calls, ShouldEqual, 1) 100 101 // 31 sec later the cache expires and new check is made. 102 cl.Add(31 * time.Second) 103 _, _, err = getSignedURL(ctx, "/bucket/path", "", signer, gs) 104 So(err, ShouldErrLike, "doesn't exist") 105 So(gs.calls, ShouldEqual, 2) 106 }) 107 108 Convey("Signing error", func() { 109 signErr = fmt.Errorf("boo, error") 110 _, _, err := getSignedURL(ctx, "/bucket/path", "", signer, &mockedSignerGS{exists: true}) 111 So(err, ShouldErrLike, "boo, error") 112 So(grpcutil.Code(err), ShouldEqual, codes.Internal) 113 }) 114 115 Convey("Content-Disposition", func() { 116 signature = "sig1" 117 url, _, _ := getSignedURL(ctx, "/bucket/path", "name.txt", signer, &mockedSignerGS{exists: true}) 118 So(url, ShouldEqual, "https://storage.googleapis.com"+ 119 "/bucket/path?Expires=1454479506&"+ 120 "GoogleAccessId=test%40example.com&"+ 121 "Signature=c2lnMQ%3D%3D&"+ 122 "response-content-disposition=attachment%3B+filename%3D%22name.txt%22") 123 }) 124 }) 125 } 126 127 type mockedSignerGS struct { 128 testutil.NoopGoogleStorage 129 130 exists bool 131 calls int 132 } 133 134 func (m *mockedSignerGS) Size(ctx context.Context, path string) (size uint64, exists bool, err error) { 135 m.calls++ 136 return 123, m.exists, nil 137 }