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  }