go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/rpc/common_test.go (about)

     1  // Copyright 2020 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 rpc
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"strings"
    21  	"testing"
    22  	"time"
    23  
    24  	"cloud.google.com/go/bigquery/storage/apiv1/storagepb"
    25  	"google.golang.org/grpc/metadata"
    26  
    27  	"go.chromium.org/luci/common/clock/testclock"
    28  	"go.chromium.org/luci/gae/impl/memory"
    29  	"go.chromium.org/luci/gae/service/datastore"
    30  	"go.chromium.org/luci/server/bqlog"
    31  	"go.chromium.org/luci/server/secrets"
    32  	"go.chromium.org/luci/server/secrets/testsecrets"
    33  
    34  	"go.chromium.org/luci/buildbucket"
    35  	"go.chromium.org/luci/buildbucket/appengine/internal/buildtoken"
    36  	"go.chromium.org/luci/buildbucket/appengine/model"
    37  	pb "go.chromium.org/luci/buildbucket/proto"
    38  	"go.chromium.org/luci/buildbucket/protoutil"
    39  
    40  	. "github.com/smartystreets/goconvey/convey"
    41  	. "go.chromium.org/luci/common/testing/assertions"
    42  )
    43  
    44  func installTestSecret(ctx context.Context) context.Context {
    45  	store := &testsecrets.Store{
    46  		Secrets: map[string]secrets.Secret{
    47  			"key": {Active: []byte("stuff")},
    48  		},
    49  	}
    50  	ctx = secrets.Use(ctx, store)
    51  	return secrets.GeneratePrimaryTinkAEADForTest(ctx)
    52  }
    53  
    54  func TestLogToBQ(t *testing.T) {
    55  	t.Parallel()
    56  
    57  	Convey("logToBQ", t, func(c C) {
    58  		b := &bqlog.Bundler{
    59  			CloudProject: "project",
    60  			Dataset:      "dataset",
    61  		}
    62  		ctx := withBundler(context.Background(), b)
    63  		b.RegisterSink(bqlog.Sink{
    64  			Prototype: &pb.PRPCRequestLog{},
    65  			Table:     "table",
    66  		})
    67  		b.Start(ctx, &bqlog.FakeBigQueryWriter{
    68  			Send: func(req *storagepb.AppendRowsRequest) error {
    69  				rows := req.GetProtoRows().GetRows().GetSerializedRows()
    70  				// TODO(crbug/1250459): Check that rows being sent to BQ look correct.
    71  				c.So(len(rows), ShouldEqual, 1)
    72  				return nil
    73  			},
    74  		})
    75  		defer b.Shutdown(ctx)
    76  
    77  		logToBQ(ctx, "id", "parent", "method")
    78  	})
    79  }
    80  
    81  func TestValidateTags(t *testing.T) {
    82  	t.Parallel()
    83  
    84  	Convey("validate build set", t, func() {
    85  		Convey("valid", func() {
    86  			// test gitiles format
    87  			gitiles := fmt.Sprintf("commit/gitiles/chromium.googlesource.com/chromium/src/+/%s", strings.Repeat("a", 40))
    88  			So(validateBuildSet(gitiles), ShouldBeNil)
    89  			// test gerrit format
    90  			So(validateBuildSet("patch/gerrit/chromium-review.googlesource.com/123/456"), ShouldBeNil)
    91  			// test user format
    92  			So(validateBuildSet("myformat/x"), ShouldBeNil)
    93  		})
    94  		Convey("invalid", func() {
    95  			gitiles := fmt.Sprintf("commit/gitiles/chromium.googlesource.com/a/chromium/src/+/%s", strings.Repeat("a", 40))
    96  			So(validateBuildSet(gitiles), ShouldErrLike, `gitiles project must not start with "a/"`)
    97  			gitiles = fmt.Sprintf("commit/gitiles/chromium.googlesource.com/chromium/src.git/+/%s", strings.Repeat("a", 40))
    98  			So(validateBuildSet(gitiles), ShouldErrLike, `gitiles project must not end with ".git"`)
    99  
   100  			So(validateBuildSet("patch/gerrit/chromium-review.googlesource.com/aa/bb"), ShouldErrLike, `does not match regex "^patch/gerrit/([^/]+)/(\d+)/(\d+)$"`)
   101  			So(validateBuildSet(strings.Repeat("a", 2000)), ShouldErrLike, "buildset tag is too long")
   102  		})
   103  	})
   104  
   105  	Convey("validate tags", t, func() {
   106  		Convey("invalid", func() {
   107  			// in general
   108  			So(validateTags([]*pb.StringPair{{Key: "k:1", Value: "v"}}, TagNew), ShouldErrLike, "cannot have a colon")
   109  
   110  			// build address
   111  			So(validateTags([]*pb.StringPair{{Key: "build_address", Value: "v"}}, TagNew), ShouldErrLike, `tag "build_address" is reserved`)
   112  			So(validateTags([]*pb.StringPair{{Key: "build_address", Value: "v"}}, TagAppend), ShouldErrLike, `cannot be added to an existing build`)
   113  
   114  			// buildset
   115  			So(validateTags([]*pb.StringPair{{Key: "buildset", Value: "patch/gerrit/foo"}}, TagNew), ShouldErrLike, `does not match regex "^patch/gerrit/([^/]+)/(\d+)/(\d+)$"`)
   116  
   117  			gitiles1 := fmt.Sprintf("commit/gitiles/chromium.googlesource.com/chromium/src/+/%s", strings.Repeat("a", 40))
   118  			gitiles2 := fmt.Sprintf("commit/gitiles/chromium.googlesource.com/chromium/src/+/%s", strings.Repeat("b", 40))
   119  			So(validateTags([]*pb.StringPair{
   120  				{Key: "buildset", Value: gitiles1},
   121  				{Key: "buildset", Value: gitiles2},
   122  			}, TagNew),
   123  				ShouldBeNil)
   124  			So(validateTags([]*pb.StringPair{
   125  				{Key: "buildset", Value: gitiles1},
   126  				{Key: "buildset", Value: gitiles1},
   127  			}, TagNew),
   128  				ShouldBeNil)
   129  
   130  			// builder
   131  			So(validateTags([]*pb.StringPair{
   132  				{Key: "builder", Value: "1"},
   133  				{Key: "builder", Value: "2"},
   134  			}, TagNew),
   135  				ShouldErrLike,
   136  				`tag "builder:2" conflicts with tag "builder:1"`)
   137  			So(validateTags([]*pb.StringPair{
   138  				{Key: "builder", Value: "1"},
   139  				{Key: "builder", Value: "1"},
   140  			}, TagNew),
   141  				ShouldBeNil)
   142  			So(validateTags([]*pb.StringPair{{Key: "builder", Value: "v"}}, TagAppend), ShouldErrLike, "cannot be added to an existing build")
   143  		})
   144  	})
   145  
   146  	Convey("validate summary_markdown", t, func() {
   147  		Convey("valid", func() {
   148  			So(validateSummaryMarkdown("[this](http://example.org) is a link"), ShouldBeNil)
   149  		})
   150  
   151  		Convey("too big", func() {
   152  			So(validateSummaryMarkdown(strings.Repeat("☕", protoutil.SummaryMarkdownMaxLength)), ShouldErrLike, "too big to accept")
   153  		})
   154  	})
   155  
   156  	Convey("validateCommit", t, func() {
   157  		Convey("nil", func() {
   158  			err := validateCommit(nil)
   159  			So(err, ShouldErrLike, "host is required")
   160  		})
   161  
   162  		Convey("empty", func() {
   163  			cm := &pb.GitilesCommit{}
   164  			err := validateCommit(cm)
   165  			So(err, ShouldErrLike, "host is required")
   166  		})
   167  
   168  		Convey("project", func() {
   169  			cm := &pb.GitilesCommit{
   170  				Host: "host",
   171  			}
   172  			err := validateCommit(cm)
   173  			So(err, ShouldErrLike, "project is required")
   174  		})
   175  
   176  		Convey("id", func() {
   177  			Convey("invalid ID", func() {
   178  				cm := &pb.GitilesCommit{
   179  					Host:    "host",
   180  					Project: "project",
   181  					Id:      "id",
   182  				}
   183  				err := validateCommit(cm)
   184  				// sha1
   185  				So(err, ShouldErrLike, "id must match")
   186  			})
   187  
   188  			Convey("position", func() {
   189  				cm := &pb.GitilesCommit{
   190  					Host:     "host",
   191  					Project:  "project",
   192  					Id:       "id",
   193  					Position: 1,
   194  				}
   195  				err := validateCommit(cm)
   196  				So(err, ShouldErrLike, "position requires ref")
   197  			})
   198  		})
   199  
   200  		Convey("ref", func() {
   201  			Convey("invalid ref", func() {
   202  				cm := &pb.GitilesCommit{
   203  					Host:    "host",
   204  					Project: "project",
   205  					Ref:     "ref",
   206  				}
   207  				err := validateCommit(cm)
   208  				So(err, ShouldErrLike, "ref must match")
   209  			})
   210  
   211  			Convey("valid, but w/ invalid ID", func() {
   212  				cm := &pb.GitilesCommit{
   213  					Host:    "host",
   214  					Project: "project",
   215  					Ref:     "refs/r1",
   216  					Id:      "id",
   217  				}
   218  				err := validateCommit(cm)
   219  				So(err, ShouldErrLike, "id must match")
   220  			})
   221  		})
   222  
   223  		Convey("neither ID nor ref", func() {
   224  			cm := &pb.GitilesCommit{
   225  				Host:    "host",
   226  				Project: "project",
   227  			}
   228  			err := validateCommit(cm)
   229  			So(err, ShouldErrLike, "one of")
   230  		})
   231  
   232  		Convey("valid", func() {
   233  			Convey("id", func() {
   234  				cm := &pb.GitilesCommit{
   235  					Host:    "host",
   236  					Project: "project",
   237  					Id:      "1234567890123456789012345678901234567890",
   238  				}
   239  				err := validateCommit(cm)
   240  				So(err, ShouldBeNil)
   241  			})
   242  
   243  			Convey("ref", func() {
   244  				cm := &pb.GitilesCommit{
   245  					Host:     "host",
   246  					Project:  "project",
   247  					Ref:      "refs/ref",
   248  					Position: 1,
   249  				}
   250  				err := validateCommit(cm)
   251  				So(err, ShouldBeNil)
   252  			})
   253  		})
   254  	})
   255  
   256  	Convey("validateCommitWithRef", t, func() {
   257  		Convey("nil", func() {
   258  			So(validateCommitWithRef(nil), ShouldErrLike, "ref is required")
   259  		})
   260  
   261  		Convey("empty", func() {
   262  			So(validateCommitWithRef(&pb.GitilesCommit{}), ShouldErrLike, "ref is required")
   263  		})
   264  
   265  		Convey("with id", func() {
   266  			cm := &pb.GitilesCommit{
   267  				Host:    "host",
   268  				Project: "project",
   269  				Id:      "id",
   270  			}
   271  			So(validateCommitWithRef(cm), ShouldErrLike, "ref is required")
   272  		})
   273  
   274  		Convey("with ref", func() {
   275  			cm := &pb.GitilesCommit{
   276  				Host:     "host",
   277  				Project:  "project",
   278  				Ref:      "refs/",
   279  				Position: 1,
   280  			}
   281  			So(validateCommitWithRef(cm), ShouldBeNil)
   282  		})
   283  	})
   284  }
   285  
   286  func TestValidateBuildToken(t *testing.T) {
   287  	t.Parallel()
   288  
   289  	Convey("validateBuildToken", t, func() {
   290  		ctx := memory.Use(context.Background())
   291  		ctx, _ = testclock.UseTime(ctx, time.Unix(1444945245, 0))
   292  		ctx = installTestSecret(ctx)
   293  
   294  		tk1, _ := buildtoken.GenerateToken(ctx, 1, pb.TokenBody_BUILD)
   295  		tk2, _ := buildtoken.GenerateToken(ctx, 2, pb.TokenBody_BUILD)
   296  		build := &model.Build{
   297  			ID: 1,
   298  			Proto: &pb.Build{
   299  				Id: 1,
   300  				Builder: &pb.BuilderID{
   301  					Project: "project",
   302  					Bucket:  "bucket",
   303  					Builder: "builder",
   304  				},
   305  				Status: pb.Status_STARTED,
   306  			},
   307  			UpdateToken: tk1,
   308  		}
   309  		So(datastore.Put(ctx, build), ShouldBeNil)
   310  
   311  		Convey("Works", func() {
   312  			ctx = metadata.NewIncomingContext(ctx, metadata.Pairs(buildbucket.BuildbucketTokenHeader, tk1))
   313  			_, err := validateToken(ctx, 1, pb.TokenBody_BUILD)
   314  			So(err, ShouldBeNil)
   315  		})
   316  
   317  		Convey("Fails", func() {
   318  			Convey("if unmatched", func() {
   319  				ctx = metadata.NewIncomingContext(ctx, metadata.Pairs(buildbucket.BuildbucketTokenHeader, tk2))
   320  				_, err := validateToken(ctx, 1, pb.TokenBody_BUILD)
   321  				So(err, ShouldNotBeNil)
   322  			})
   323  			Convey("if missing", func() {
   324  				_, err := validateToken(ctx, 1, pb.TokenBody_BUILD)
   325  				So(err, ShouldNotBeNil)
   326  			})
   327  		})
   328  	})
   329  }
   330  
   331  func TestValidateBuildTaskToken(t *testing.T) {
   332  	t.Parallel()
   333  
   334  	Convey("validateBuildTaskToken", t, func() {
   335  		ctx := memory.Use(context.Background())
   336  		ctx, _ = testclock.UseTime(ctx, time.Unix(1444945245, 0))
   337  		ctx = installTestSecret(ctx)
   338  
   339  		tk1, err := buildtoken.GenerateToken(ctx, 1, pb.TokenBody_TASK)
   340  		So(err, ShouldBeNil)
   341  		tk2, err := buildtoken.GenerateToken(ctx, 2, pb.TokenBody_TASK)
   342  		So(err, ShouldBeNil)
   343  
   344  		build := &model.Build{
   345  			ID: 1,
   346  			Proto: &pb.Build{
   347  				Id: 1,
   348  				Builder: &pb.BuilderID{
   349  					Project: "project",
   350  					Bucket:  "bucket",
   351  					Builder: "builder",
   352  				},
   353  				Status: pb.Status_STARTED,
   354  			},
   355  		}
   356  		So(datastore.Put(ctx, build), ShouldBeNil)
   357  
   358  		Convey("Works", func() {
   359  			ctx = metadata.NewIncomingContext(ctx, metadata.Pairs(buildbucket.BuildbucketTokenHeader, tk1))
   360  			_, err := validateToken(ctx, 1, pb.TokenBody_TASK)
   361  			So(err, ShouldBeNil)
   362  		})
   363  
   364  		Convey("Fails", func() {
   365  			Convey("if unmatched", func() {
   366  				ctx = metadata.NewIncomingContext(ctx, metadata.Pairs(buildbucket.BuildbucketTokenHeader, tk2))
   367  				_, err := validateToken(ctx, 1, pb.TokenBody_TASK)
   368  				So(err, ShouldNotBeNil)
   369  			})
   370  			Convey("if missing", func() {
   371  				_, err := validateToken(ctx, 1, pb.TokenBody_TASK)
   372  				So(err, ShouldNotBeNil)
   373  			})
   374  			Convey("if wrong purpose", func() {
   375  				_, err := validateToken(ctx, 1, pb.TokenBody_BUILD)
   376  				So(err, ShouldNotBeNil)
   377  			})
   378  		})
   379  	})
   380  }