go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cipd/appengine/impl/repo/processing/client_extractor_test.go (about) 1 // Copyright 2018 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 processing 16 17 import ( 18 "bytes" 19 "context" 20 "io" 21 "strings" 22 "testing" 23 24 "google.golang.org/grpc/codes" 25 "google.golang.org/grpc/status" 26 27 "go.chromium.org/luci/gae/impl/memory" 28 "go.chromium.org/luci/gae/service/datastore" 29 30 api "go.chromium.org/luci/cipd/api/cipd/v1" 31 "go.chromium.org/luci/cipd/appengine/impl/gs" 32 "go.chromium.org/luci/cipd/appengine/impl/model" 33 "go.chromium.org/luci/cipd/appengine/impl/testutil" 34 "go.chromium.org/luci/cipd/common" 35 36 . "github.com/smartystreets/goconvey/convey" 37 . "go.chromium.org/luci/common/testing/assertions" 38 ) 39 40 func phonyHexDigest(algo api.HashAlgo, letter string) string { 41 return strings.Repeat(letter, map[api.HashAlgo]int{ 42 api.HashAlgo_SHA1: 40, 43 api.HashAlgo_SHA256: 64, 44 }[algo]) 45 } 46 47 func phonyInstanceID(algo api.HashAlgo, letter string) string { 48 return common.ObjectRefToInstanceID(&api.ObjectRef{ 49 HashAlgo: algo, 50 HexDigest: phonyHexDigest(algo, letter), 51 }) 52 } 53 54 func instance(ctx context.Context, pkg string, algo api.HashAlgo) *model.Instance { 55 return &model.Instance{ 56 InstanceID: phonyInstanceID(algo, "a"), 57 Package: model.PackageKey(ctx, pkg), 58 } 59 } 60 61 func hexDigest(algo api.HashAlgo, body string) string { 62 h := common.MustNewHash(algo) 63 if _, err := h.Write([]byte(body)); err != nil { 64 panic(err) 65 } 66 return common.HexDigest(h) 67 } 68 69 func TestGetClientPackage(t *testing.T) { 70 t.Parallel() 71 72 Convey("works", t, func() { 73 pkg, err := GetClientPackage("linux-amd64") 74 So(err, ShouldBeNil) 75 So(pkg, ShouldEqual, "infra/tools/cipd/linux-amd64") 76 }) 77 78 Convey("fails", t, func() { 79 _, err := GetClientPackage("../sneaky") 80 So(err, ShouldErrLike, "invalid package name") 81 }) 82 } 83 84 func TestClientExtractor(t *testing.T) { 85 t.Parallel() 86 87 ctx := memory.Use(context.Background()) 88 89 originalBody := strings.Repeat("01234567", 8960) 90 expectedDigests := map[string]string{ 91 "SHA1": hexDigest(api.HashAlgo_SHA1, originalBody), 92 "SHA256": hexDigest(api.HashAlgo_SHA256, originalBody), 93 } 94 95 instSHA256 := instance(ctx, "infra/tools/cipd/linux-amd64", api.HashAlgo_SHA256) 96 instSHA1 := instance(ctx, "infra/tools/cipd/linux-amd64", api.HashAlgo_SHA1) 97 98 goodPkg := packageReader(map[string]string{"cipd": originalBody}) 99 100 Convey("With mocks", t, func() { 101 var publishedRef *api.ObjectRef 102 var canceled bool 103 104 expectedUploadAlgo := api.HashAlgo_SHA256 105 106 cas := testutil.MockCAS{ 107 BeginUploadImpl: func(_ context.Context, r *api.BeginUploadRequest) (*api.UploadOperation, error) { 108 So(r.HashAlgo, ShouldEqual, expectedUploadAlgo) 109 return &api.UploadOperation{ 110 OperationId: "op_id", 111 UploadUrl: "http://example.com/upload", 112 }, nil 113 }, 114 FinishUploadImpl: func(_ context.Context, r *api.FinishUploadRequest) (*api.UploadOperation, error) { 115 So(r.UploadOperationId, ShouldEqual, "op_id") 116 publishedRef = r.ForceHash 117 return &api.UploadOperation{Status: api.UploadStatus_PUBLISHED}, nil 118 }, 119 CancelUploadImpl: func(_ context.Context, r *api.CancelUploadRequest) (*api.UploadOperation, error) { 120 So(r.UploadOperationId, ShouldEqual, "op_id") 121 canceled = true 122 return &api.UploadOperation{Status: api.UploadStatus_CANCELED}, nil 123 }, 124 } 125 126 extracted := bytes.Buffer{} 127 uploader := &trackingWriter{w: &extracted} 128 129 ce := ClientExtractor{ 130 CAS: &cas, 131 132 uploader: func(ctx context.Context, size int64, uploadURL string) io.Writer { return uploader }, 133 bufferSize: 64 * 1024, 134 } 135 136 Convey("Happy path, SHA256", func() { 137 res, err := ce.Run(ctx, instSHA256, goodPkg) 138 So(err, ShouldBeNil) 139 So(res.Err, ShouldBeNil) 140 141 r := res.Result.(ClientExtractorResult) 142 So(r.ClientBinary.Size, ShouldEqual, len(originalBody)) 143 So(r.ClientBinary.HashAlgo, ShouldEqual, "SHA256") 144 So(r.ClientBinary.HashDigest, ShouldEqual, expectedDigests["SHA256"]) 145 So(r.ClientBinary.AllHashDigests, ShouldResemble, expectedDigests) 146 147 So(extracted.String(), ShouldEqual, originalBody) 148 So(publishedRef, ShouldResembleProto, &api.ObjectRef{ 149 HashAlgo: api.HashAlgo_SHA256, 150 HexDigest: expectedDigests["SHA256"], 151 }) 152 153 // Was written by 64 Kb chunks, NOT 32 Kb (as used by zip.Reader). 154 So(uploader.calls, ShouldResemble, []int{64 * 1024, 6 * 1024}) 155 }) 156 157 // TODO(vadimsh): Delete this test once SHA1 uploads are forbidden. 158 Convey("Happy path, SHA1", func() { 159 expectedUploadAlgo = api.HashAlgo_SHA1 160 res, err := ce.Run(ctx, instSHA1, goodPkg) 161 So(err, ShouldBeNil) 162 So(res.Err, ShouldBeNil) 163 164 r := res.Result.(ClientExtractorResult) 165 So(r.ClientBinary.Size, ShouldEqual, len(originalBody)) 166 So(r.ClientBinary.HashAlgo, ShouldEqual, "SHA1") 167 So(r.ClientBinary.HashDigest, ShouldEqual, expectedDigests["SHA1"]) 168 So(r.ClientBinary.AllHashDigests, ShouldResemble, expectedDigests) 169 170 So(publishedRef, ShouldResembleProto, &api.ObjectRef{ 171 HashAlgo: api.HashAlgo_SHA1, 172 HexDigest: expectedDigests["SHA1"], 173 }) 174 }) 175 176 Convey("No such file in the package", func() { 177 res, err := ce.Run(ctx, instSHA256, packageReader(nil)) 178 So(err, ShouldBeNil) 179 So(res.Err, ShouldErrLike, `failed to open the file for reading: no file "cipd" inside the package`) 180 }) 181 182 Convey("Internal error when initiating the upload", func() { 183 cas.BeginUploadImpl = func(_ context.Context, r *api.BeginUploadRequest) (*api.UploadOperation, error) { 184 return nil, status.Errorf(codes.Internal, "boo") 185 } 186 _, err := ce.Run(ctx, instSHA256, goodPkg) 187 So(err, ShouldErrLike, `failed to open a CAS upload: rpc error: code = Internal desc = boo`) 188 }) 189 190 Convey("Internal error when finalizing the upload", func() { 191 cas.FinishUploadImpl = func(_ context.Context, r *api.FinishUploadRequest) (*api.UploadOperation, error) { 192 return nil, status.Errorf(codes.Internal, "boo") 193 } 194 _, err := ce.Run(ctx, instSHA256, goodPkg) 195 So(err, ShouldErrLike, `failed to finalize the CAS upload: rpc error: code = Internal desc = boo`) 196 }) 197 198 Convey("Asked to restart the upload", func() { 199 uploader.err = &gs.RestartUploadError{Offset: 124} 200 201 _, err := ce.Run(ctx, instSHA256, goodPkg) 202 So(err, ShouldErrLike, `asked to restart the upload from faraway offset: the upload should be restarted from offset 124`) 203 So(canceled, ShouldBeTrue) 204 }) 205 }) 206 207 Convey("Applicable works", t, func() { 208 ce := ClientExtractor{} 209 210 res, err := ce.Applicable(ctx, instance(ctx, "infra/tools/cipd/linux", api.HashAlgo_SHA256)) 211 So(err, ShouldBeNil) 212 So(res, ShouldBeTrue) 213 214 res, err = ce.Applicable(ctx, instance(ctx, "infra/tools/stuff/linux", api.HashAlgo_SHA256)) 215 So(err, ShouldBeNil) 216 So(res, ShouldBeFalse) 217 }) 218 } 219 220 func TestGetResult(t *testing.T) { 221 t.Parallel() 222 223 Convey("With datastore", t, func() { 224 ctx := memory.Use(context.Background()) 225 226 instRef := &api.ObjectRef{ 227 HashAlgo: api.HashAlgo_SHA256, 228 HexDigest: phonyHexDigest(api.HashAlgo_SHA256, "a"), 229 } 230 231 write := func(res *ClientExtractorResult, err string) { 232 r := model.ProcessingResult{ 233 ProcID: ClientExtractorProcID, 234 Instance: datastore.KeyForObj(ctx, &model.Instance{ 235 InstanceID: common.ObjectRefToInstanceID(instRef), 236 Package: model.PackageKey(ctx, "a/b/c"), 237 }), 238 } 239 if res != nil { 240 r.Success = true 241 So(r.WriteResult(&res), ShouldBeNil) 242 } else { 243 r.Error = err 244 } 245 So(datastore.Put(ctx, &r), ShouldBeNil) 246 } 247 248 Convey("Happy path", func() { 249 res := ClientExtractorResult{} 250 res.ClientBinary.HashAlgo = "SHA256" 251 res.ClientBinary.HashDigest = phonyHexDigest(api.HashAlgo_SHA256, "b") 252 res.ClientBinary.AllHashDigests = map[string]string{ 253 "SHA1": phonyHexDigest(api.HashAlgo_SHA1, "c"), 254 "SHA256": phonyHexDigest(api.HashAlgo_SHA256, "b"), 255 "SHA999": strings.Repeat("e", 99), // should silently be skipped 256 } 257 write(&res, "") 258 259 out, err := GetClientExtractorResult(ctx, &api.Instance{ 260 Package: "a/b/c", 261 Instance: instRef, 262 }) 263 So(err, ShouldBeNil) 264 So(out, ShouldResemble, &res) 265 266 ref, err := out.ToObjectRef() 267 So(err, ShouldBeNil) 268 So(ref, ShouldResembleProto, &api.ObjectRef{ 269 HashAlgo: api.HashAlgo_SHA256, 270 HexDigest: res.ClientBinary.HashDigest, 271 }) 272 273 So(out.ObjectRefAliases(), ShouldResembleProto, []*api.ObjectRef{ 274 {HashAlgo: api.HashAlgo_SHA1, HexDigest: phonyHexDigest(api.HashAlgo_SHA1, "c")}, 275 {HashAlgo: api.HashAlgo_SHA256, HexDigest: phonyHexDigest(api.HashAlgo_SHA256, "b")}, 276 }) 277 }) 278 279 Convey("Legacy SHA1 record with no AllHashDigests", func() { 280 res := ClientExtractorResult{} 281 res.ClientBinary.HashAlgo = "SHA1" 282 res.ClientBinary.HashDigest = phonyHexDigest(api.HashAlgo_SHA1, "a") 283 284 ref, err := res.ToObjectRef() 285 So(err, ShouldBeNil) 286 So(ref, ShouldResembleProto, &api.ObjectRef{ 287 HashAlgo: api.HashAlgo_SHA1, 288 HexDigest: res.ClientBinary.HashDigest, 289 }) 290 291 So(res.ObjectRefAliases(), ShouldResembleProto, []*api.ObjectRef{ 292 {HashAlgo: api.HashAlgo_SHA1, HexDigest: res.ClientBinary.HashDigest}, 293 }) 294 }) 295 296 Convey("Failed processor", func() { 297 write(nil, "boom") 298 299 _, err := GetClientExtractorResult(ctx, &api.Instance{ 300 Package: "a/b/c", 301 Instance: instRef, 302 }) 303 So(err, ShouldErrLike, "boom") 304 }) 305 306 Convey("No result", func() { 307 _, err := GetClientExtractorResult(ctx, &api.Instance{ 308 Package: "a/b/c", 309 Instance: instRef, 310 }) 311 So(err, ShouldEqual, datastore.ErrNoSuchEntity) 312 }) 313 }) 314 }