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 }