go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/scheduler/appengine/task/buildbucket/buildbucket_test.go (about) 1 // Copyright 2015 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 buildbucket 16 17 import ( 18 "context" 19 "encoding/base64" 20 "fmt" 21 "math/rand" 22 "net/http" 23 "sort" 24 "strings" 25 "sync/atomic" 26 "testing" 27 "time" 28 29 "github.com/golang/protobuf/jsonpb" 30 "github.com/golang/protobuf/proto" 31 "google.golang.org/api/pubsub/v1" 32 "google.golang.org/grpc/codes" 33 "google.golang.org/grpc/status" 34 "google.golang.org/protobuf/types/known/structpb" 35 36 bbpb "go.chromium.org/luci/buildbucket/proto" 37 "go.chromium.org/luci/common/data/rand/mathrand" 38 "go.chromium.org/luci/config/validation" 39 "go.chromium.org/luci/gae/impl/memory" 40 api "go.chromium.org/luci/scheduler/api/scheduler/v1" 41 "go.chromium.org/luci/scheduler/appengine/engine/policy" 42 "go.chromium.org/luci/scheduler/appengine/internal" 43 "go.chromium.org/luci/scheduler/appengine/messages" 44 "go.chromium.org/luci/scheduler/appengine/task" 45 "go.chromium.org/luci/scheduler/appengine/task/utils/tasktest" 46 47 . "github.com/smartystreets/goconvey/convey" 48 . "go.chromium.org/luci/common/testing/assertions" 49 ) 50 51 var _ task.Manager = (*TaskManager)(nil) 52 53 func TestValidateProtoMessage(t *testing.T) { 54 t.Parallel() 55 56 tm := TaskManager{} 57 c := context.Background() 58 59 Convey("ValidateProtoMessage works", t, func() { 60 ctx := &validation.Context{Context: c} 61 validate := func(msg proto.Message) error { 62 tm.ValidateProtoMessage(ctx, msg, "some-project:some-realm") 63 return ctx.Finalize() 64 } 65 66 Convey("ValidateProtoMessage passes good msg", func() { 67 So(validate(&messages.BuildbucketTask{ 68 Server: "blah.com", 69 Bucket: "bucket", 70 Builder: "builder", 71 Tags: []string{"a:b", "c:d"}, 72 Properties: []string{"a:b", "c:d"}, 73 }), ShouldBeNil) 74 }) 75 76 Convey("ValidateProtoMessage passes good minimal msg", func() { 77 So(validate(&messages.BuildbucketTask{ 78 Server: "blah.com", 79 Builder: "builder", 80 }), ShouldBeNil) 81 }) 82 83 Convey("ValidateProtoMessage wrong type", func() { 84 So(validate(&messages.NoopTask{}), ShouldErrLike, "wrong type") 85 }) 86 87 Convey("ValidateProtoMessage empty", func() { 88 So(validate(tm.ProtoMessageType()), ShouldErrLike, "expecting a non-empty BuildbucketTask") 89 }) 90 91 Convey("ValidateProtoMessage validates URL", func() { 92 call := func(url string) error { 93 ctx = &validation.Context{Context: c} 94 tm.ValidateProtoMessage(ctx, &messages.BuildbucketTask{ 95 Server: url, 96 Bucket: "bucket", 97 Builder: "builder", 98 }, "some-project:some-realm") 99 return ctx.Finalize() 100 } 101 So(call(""), ShouldErrLike, "field 'server' is required") 102 So(call("https://host/not-root"), ShouldErrLike, "field 'server' should be just a host, not a URL") 103 So(call("%%%%"), ShouldErrLike, "field 'server' is not a valid hostname") 104 So(call("blah.com/abc"), ShouldErrLike, "field 'server' is not a valid hostname") 105 }) 106 107 Convey("ValidateProtoMessage needs bucket", func() { 108 tm.ValidateProtoMessage(ctx, &messages.BuildbucketTask{ 109 Server: "blah.com", 110 Builder: "builder", 111 }, "some-project:@legacy") 112 So(ctx.Finalize(), ShouldErrLike, `'bucket' field for jobs in "@legacy" realm is required`) 113 }) 114 115 Convey("ValidateProtoMessage needs builder", func() { 116 So(validate(&messages.BuildbucketTask{ 117 Server: "blah.com", 118 Bucket: "bucket", 119 }), ShouldErrLike, "'builder' field is required") 120 }) 121 122 Convey("ValidateProtoMessage validates properties", func() { 123 So(validate(&messages.BuildbucketTask{ 124 Server: "blah.com", 125 Bucket: "bucket", 126 Builder: "builder", 127 Properties: []string{"not_kv_pair"}, 128 }), ShouldErrLike, "bad property, not a 'key:value' pair") 129 }) 130 131 Convey("ValidateProtoMessage validates tags", func() { 132 So(validate(&messages.BuildbucketTask{ 133 Server: "blah.com", 134 Bucket: "bucket", 135 Builder: "builder", 136 Tags: []string{"not_kv_pair"}, 137 }), ShouldErrLike, "bad tag, not a 'key:value' pair") 138 }) 139 140 Convey("ValidateProtoMessage forbids default tags overwrite", func() { 141 So(validate(&messages.BuildbucketTask{ 142 Server: "blah.com", 143 Bucket: "bucket", 144 Builder: "builder", 145 Tags: []string{"scheduler_job_id:blah"}, 146 }), ShouldErrLike, "tag \"scheduler_job_id\" is reserved") 147 }) 148 }) 149 } 150 151 func fakeController(testSrvURL string) *tasktest.TestController { 152 return &tasktest.TestController{ 153 TaskMessage: &messages.BuildbucketTask{ 154 Server: testSrvURL, 155 Bucket: "test-bucket", 156 Builder: "builder", 157 Tags: []string{"a:from-task-def", "b:from-task-def"}, 158 }, 159 Req: task.Request{ 160 IncomingTriggers: []*internal.Trigger{ 161 { 162 Id: "trigger", 163 Title: "Trigger", 164 Url: "https://trigger.example.com", 165 Payload: &internal.Trigger_Gitiles{ 166 Gitiles: &api.GitilesTrigger{ 167 Repo: "https://chromium.googlesource.com/chromium/src", 168 Ref: "refs/heads/master", 169 Revision: "deadbeef", 170 }, 171 }, 172 }, 173 }, 174 }, 175 Client: http.DefaultClient, 176 SaveCallback: func() error { return nil }, 177 PrepareTopicCallback: func(publisher string) (string, string, error) { 178 if publisher != testSrvURL { 179 panic(fmt.Sprintf("expecting %q, got %q", testSrvURL, publisher)) 180 } 181 return "topic", "auth_token", nil 182 }, 183 } 184 } 185 186 func TestBuilderID(t *testing.T) { 187 t.Parallel() 188 189 var cases = []struct { 190 RealmID string 191 Bucket string 192 Output string 193 Error string 194 }{ 195 {"proj:realm", "", "proj:realm", ""}, 196 {"proj:@legacy", "", "", "is required"}, 197 {"proj:@root", "", "", "is required"}, 198 199 {"proj:realm", "another-proj:buck", "another-proj:buck", ""}, 200 {"proj:realm", "buck", "proj:buck", ""}, 201 {"proj:realm", "abc.def.123", "proj:abc.def.123", ""}, 202 {"proj:@legacy", "buck", "proj:buck", ""}, 203 204 {"proj:realm", "luci.proj.buck", "", `use "buck" instead`}, 205 {"proj:realm", "luci.another-proj.buck", "", `use "another-proj:buck" instead`}, 206 {"proj:realm", "luci.another-proj", "", "need 3 components"}, 207 } 208 209 for _, c := range cases { 210 bid, err := builderID(&messages.BuildbucketTask{ 211 Bucket: c.Bucket, 212 Builder: "some-builder", 213 }, c.RealmID) 214 if c.Error != "" { 215 if err == nil { 216 t.Errorf("Expected to fail for %q %q, but did not", c.Bucket, c.RealmID) 217 } else if !strings.Contains(err.Error(), c.Error) { 218 t.Errorf("Expected to fail with %q, but failed with %q", c.Error, err.Error()) 219 } 220 } else { 221 if err != nil { 222 t.Errorf("Expected to succeed for %q %q, but failed with %q", c.Bucket, c.RealmID, err.Error()) 223 } else if got := fmt.Sprintf("%s:%s", bid.Project, bid.Bucket); got != c.Output { 224 t.Errorf("Expected to get %q, but got %q", c.Output, got) 225 } 226 } 227 } 228 } 229 230 func TestFullFlow(t *testing.T) { 231 t.Parallel() 232 233 Convey("LaunchTask and HandleNotification work", t, func(ctx C) { 234 scheduleRequest := make(chan *bbpb.ScheduleBuildRequest, 1) 235 236 buildStatus := atomic.Value{} 237 buildStatus.Store(bbpb.Status_STARTED) 238 239 srv := BuildbucketFake{ 240 ScheduleBuild: func(req *bbpb.ScheduleBuildRequest) (*bbpb.Build, error) { 241 scheduleRequest <- req 242 return &bbpb.Build{ 243 Id: 9025781602559305888, 244 Status: bbpb.Status_STARTED, 245 }, nil 246 }, 247 GetBuild: func(req *bbpb.GetBuildRequest) (*bbpb.Build, error) { 248 if req.Id != 9025781602559305888 { 249 return nil, status.Errorf(codes.NotFound, "wrong build ID") 250 } 251 return &bbpb.Build{ 252 Id: req.Id, 253 Status: buildStatus.Load().(bbpb.Status), 254 }, nil 255 }, 256 } 257 srv.Start() 258 defer srv.Stop() 259 260 c := memory.Use(context.Background()) 261 c = mathrand.Set(c, rand.New(rand.NewSource(1000))) 262 mgr := TaskManager{} 263 ctl := fakeController(srv.URL()) 264 265 // Launch. 266 So(mgr.LaunchTask(c, ctl), ShouldBeNil) 267 So(ctl.TaskState, ShouldResemble, task.State{ 268 Status: task.StatusRunning, 269 TaskData: []byte(`{"build_id":"9025781602559305888"}`), 270 ViewURL: srv.URL() + "/build/9025781602559305888", 271 }) 272 273 So(<-scheduleRequest, ShouldResembleProto, &bbpb.ScheduleBuildRequest{ 274 RequestId: "1", 275 Builder: &bbpb.BuilderID{ 276 Project: "some-project", 277 Bucket: "test-bucket", 278 Builder: "builder", 279 }, 280 GitilesCommit: &bbpb.GitilesCommit{ 281 Host: "chromium.googlesource.com", 282 Project: "chromium/src", 283 Id: "deadbeef", 284 Ref: "refs/heads/master", 285 }, 286 Properties: structFromJSON(`{ 287 "$recipe_engine/scheduler": { 288 "hostname": "app.example.com", 289 "job": "some-project/some-job", 290 "invocation": "1", 291 "triggers": [ 292 { 293 "id": "trigger", 294 "title": "Trigger", 295 "url": "https://trigger.example.com", 296 "gitiles": { 297 "repo": "https://chromium.googlesource.com/chromium/src", 298 "ref": "refs/heads/master", 299 "revision": "deadbeef" 300 } 301 } 302 ] 303 } 304 }`), 305 Tags: []*bbpb.StringPair{ 306 {Key: "scheduler_invocation_id", Value: "1"}, 307 {Key: "scheduler_job_id", Value: "some-project/some-job"}, 308 {Key: "user_agent", Value: "app"}, 309 {Key: "a", Value: "from-task-def"}, 310 {Key: "b", Value: "from-task-def"}, 311 }, 312 Notify: &bbpb.NotificationConfig{ 313 PubsubTopic: "topic", 314 UserData: []byte("auth_token"), 315 }, 316 }) 317 318 // Added the timer. 319 So(ctl.Timers, ShouldResemble, []tasktest.TimerSpec{ 320 { 321 Delay: 224 * time.Second, // random 322 Name: statusCheckTimerName, 323 }, 324 }) 325 ctl.Timers = nil 326 327 // The timer is called. Checks the state, reschedules itself. 328 So(mgr.HandleTimer(c, ctl, statusCheckTimerName, nil), ShouldBeNil) 329 So(ctl.Timers, ShouldResemble, []tasktest.TimerSpec{ 330 { 331 Delay: 157 * time.Second, // random 332 Name: statusCheckTimerName, 333 }, 334 }) 335 336 // Process finish notification. 337 buildStatus.Store(bbpb.Status_SUCCESS) 338 So(mgr.HandleNotification(c, ctl, &pubsub.PubsubMessage{}), ShouldBeNil) 339 So(ctl.TaskState.Status, ShouldEqual, task.StatusSucceeded) 340 }) 341 } 342 343 func TestAbort(t *testing.T) { 344 t.Parallel() 345 346 Convey("LaunchTask and AbortTask work", t, func(ctx C) { 347 srv := BuildbucketFake{ 348 ScheduleBuild: func(req *bbpb.ScheduleBuildRequest) (*bbpb.Build, error) { 349 return &bbpb.Build{ 350 Id: 9025781602559305888, 351 Status: bbpb.Status_STARTED, 352 }, nil 353 }, 354 CancelBuild: func(req *bbpb.CancelBuildRequest) (*bbpb.Build, error) { 355 if req.Id != 9025781602559305888 { 356 return nil, status.Errorf(codes.NotFound, "wrong build ID") 357 } 358 return &bbpb.Build{ 359 Id: req.Id, 360 Status: bbpb.Status_CANCELED, 361 }, nil 362 }, 363 } 364 srv.Start() 365 defer srv.Stop() 366 367 c := memory.Use(context.Background()) 368 mgr := TaskManager{} 369 ctl := fakeController(srv.URL()) 370 371 // Launch and kill. 372 So(mgr.LaunchTask(c, ctl), ShouldBeNil) 373 So(mgr.AbortTask(c, ctl), ShouldBeNil) 374 }) 375 } 376 377 func TestTriggeredFlow(t *testing.T) { 378 t.Parallel() 379 380 Convey("LaunchTask with GitilesTrigger works", t, func(ctx C) { 381 scheduleRequest := make(chan *bbpb.ScheduleBuildRequest, 1) 382 383 srv := BuildbucketFake{ 384 ScheduleBuild: func(req *bbpb.ScheduleBuildRequest) (*bbpb.Build, error) { 385 scheduleRequest <- req 386 return &bbpb.Build{ 387 Id: 9025781602559305888, 388 Status: bbpb.Status_STARTED, 389 }, nil 390 }, 391 GetBuild: func(req *bbpb.GetBuildRequest) (*bbpb.Build, error) { 392 if req.Id != 9025781602559305888 { 393 return nil, status.Errorf(codes.NotFound, "wrong build ID") 394 } 395 return &bbpb.Build{ 396 Id: req.Id, 397 Status: bbpb.Status_SUCCESS, 398 }, nil 399 }, 400 } 401 srv.Start() 402 defer srv.Stop() 403 404 c := memory.Use(context.Background()) 405 mgr := TaskManager{} 406 ctl := fakeController(srv.URL()) 407 408 schedule := func(triggers []*internal.Trigger) *bbpb.ScheduleBuildRequest { 409 // Prepare the request the same way the engine does using RequestBuilder. 410 req := policy.RequestBuilder{} 411 req.FromTrigger(triggers[len(triggers)-1]) 412 req.IncomingTriggers = triggers 413 ctl.Req = req.Request 414 415 // Launch with triggers, 416 So(mgr.LaunchTask(c, ctl), ShouldBeNil) 417 So(ctl.TaskState, ShouldResemble, task.State{ 418 Status: task.StatusRunning, 419 TaskData: []byte(`{"build_id":"9025781602559305888"}`), 420 ViewURL: srv.URL() + "/build/9025781602559305888", 421 }) 422 423 return <-scheduleRequest 424 } 425 426 Convey("Gitiles triggers", func() { 427 req := schedule([]*internal.Trigger{ 428 { 429 Id: "1", 430 Payload: &internal.Trigger_Gitiles{ 431 Gitiles: &api.GitilesTrigger{ 432 Repo: "https://r.googlesource.com/repo", 433 Ref: "refs/heads/master", 434 Revision: "baadcafe", 435 }, 436 }, 437 }, 438 { 439 Id: "2", 440 Payload: &internal.Trigger_Gitiles{ 441 Gitiles: &api.GitilesTrigger{ 442 Repo: "https://r.googlesource.com/repo", 443 Ref: "refs/heads/master", 444 Revision: "deadbeef", 445 Tags: []string{"extra:tag", "gitiles_ref:refs/heads/master"}, 446 Properties: structFromJSON(`{"extra_prop": "val", "branch": "ignored"}`), 447 }, 448 }, 449 }, 450 }) 451 452 // Used the last trigger to get the commit. 453 So(req.GitilesCommit, ShouldResembleProto, &bbpb.GitilesCommit{ 454 Host: "r.googlesource.com", 455 Project: "repo", 456 Id: "deadbeef", 457 Ref: "refs/heads/master", 458 }) 459 460 // Properties are sanitized. 461 So(structKeys(req.Properties), ShouldResemble, []string{ 462 "$recipe_engine/scheduler", 463 "extra_prop", 464 }) 465 466 // Tags are sanitized too. 467 So(req.Tags, ShouldResembleProto, []*bbpb.StringPair{ 468 {Key: "scheduler_invocation_id", Value: "1"}, 469 {Key: "scheduler_job_id", Value: "some-project/some-job"}, 470 {Key: "user_agent", Value: "app"}, 471 {Key: "a", Value: "from-task-def"}, 472 {Key: "b", Value: "from-task-def"}, 473 {Key: "extra", Value: "tag"}, 474 }) 475 }) 476 477 Convey("Reconstructs gitiles commit from generic trigger", func() { 478 req := schedule([]*internal.Trigger{ 479 { 480 Id: "1", 481 Payload: &internal.Trigger_Buildbucket{ 482 Buildbucket: &api.BuildbucketTrigger{ 483 Properties: structFromJSON(`{ 484 "repository": "https://r.googlesource.com/repo", 485 "branch": "master", 486 "revision": "deadbeef", 487 "extra_prop": "val" 488 }`), 489 Tags: []string{ 490 "buildset:commit/git/deadbeef", 491 "buildset:commit/gitiles/r.googlesource.com/repo/+/deadbeef", 492 "gitiles_ref:ignored", 493 "gitiles_ref:master", 494 "extra:tag", 495 }, 496 }, 497 }, 498 }, 499 }) 500 501 // Reconstructed gitiles commit from properties. 502 So(req.GitilesCommit, ShouldResembleProto, &bbpb.GitilesCommit{ 503 Host: "r.googlesource.com", 504 Project: "repo", 505 Id: "deadbeef", 506 Ref: "refs/heads/master", 507 }) 508 509 // Properties are sanitized. 510 So(structKeys(req.Properties), ShouldResemble, []string{ 511 "$recipe_engine/scheduler", 512 "extra_prop", 513 }) 514 515 // Tags are sanitized too. 516 So(req.Tags, ShouldResembleProto, []*bbpb.StringPair{ 517 {Key: "scheduler_invocation_id", Value: "1"}, 518 {Key: "scheduler_job_id", Value: "some-project/some-job"}, 519 {Key: "user_agent", Value: "app"}, 520 {Key: "a", Value: "from-task-def"}, 521 {Key: "b", Value: "from-task-def"}, 522 {Key: "extra", Value: "tag"}, 523 }) 524 }) 525 526 Convey("Branch is optional when reconstructing", func() { 527 req := schedule([]*internal.Trigger{ 528 { 529 Id: "1", 530 Payload: &internal.Trigger_Buildbucket{ 531 Buildbucket: &api.BuildbucketTrigger{ 532 Properties: structFromJSON(`{ 533 "repository": "https://r.googlesource.com/repo", 534 "revision": "deadbeef" 535 }`), 536 Tags: []string{ 537 "buildset:commit/gitiles/r.googlesource.com/repo/+/deadbeef", 538 }, 539 }, 540 }, 541 }, 542 }) 543 So(req.GitilesCommit, ShouldResembleProto, &bbpb.GitilesCommit{ 544 Host: "r.googlesource.com", 545 Project: "repo", 546 Id: "deadbeef", 547 }) 548 So(countTags(req.Tags, "buildset"), ShouldEqual, 0) 549 So(countTags(req.Tags, "gitiles_ref"), ShouldEqual, 0) 550 }) 551 552 Convey("Properties are ignored if buildset tag is missing", func() { 553 req := schedule([]*internal.Trigger{ 554 { 555 Id: "1", 556 Payload: &internal.Trigger_Buildbucket{ 557 Buildbucket: &api.BuildbucketTrigger{ 558 Properties: structFromJSON(`{ 559 "repository": "https://r.googlesource.com/repo", 560 "branch": "main", 561 "revision": "deadbeef" 562 }`), 563 Tags: []string{ 564 "gitiles_ref:ignored", 565 }, 566 }, 567 }, 568 }, 569 }) 570 So(req.GitilesCommit, ShouldBeNil) 571 So(structKeys(req.Properties), ShouldResemble, []string{ 572 "$recipe_engine/scheduler", 573 }) 574 So(countTags(req.Tags, "buildset"), ShouldEqual, 0) 575 So(countTags(req.Tags, "gitiles_ref"), ShouldEqual, 0) 576 }) 577 578 Convey("Tags are authoritative over properties", func() { 579 req := schedule([]*internal.Trigger{ 580 { 581 Id: "1", 582 Payload: &internal.Trigger_Buildbucket{ 583 Buildbucket: &api.BuildbucketTrigger{ 584 Properties: structFromJSON(`{ 585 "repository": "https://prop.googlesource.com/repo-prop", 586 "branch": "main-prop", 587 "revision": "aaaa" 588 }`), 589 Tags: []string{ 590 "buildset:commit/gitiles/tag.googlesource.com/repo-tag/+/bbbb", 591 "gitiles_ref:main-tag", 592 }, 593 }, 594 }, 595 }, 596 }) 597 So(req.GitilesCommit, ShouldResembleProto, &bbpb.GitilesCommit{ 598 Host: "tag.googlesource.com", 599 Project: "repo-tag", 600 Id: "bbbb", 601 Ref: "refs/heads/main-tag", 602 }) 603 So(structKeys(req.Properties), ShouldResemble, []string{ 604 "$recipe_engine/scheduler", 605 }) 606 So(countTags(req.Tags, "buildset"), ShouldEqual, 0) 607 So(countTags(req.Tags, "gitiles_ref"), ShouldEqual, 0) 608 }) 609 }) 610 } 611 612 func TestPassedTriggers(t *testing.T) { 613 t.Parallel() 614 615 Convey(fmt.Sprintf("Passed to buildbucket triggers are capped at %d", maxTriggersAsSchedulerProperty), t, func(ctx C) { 616 c := memory.Use(context.Background()) 617 ctl := fakeController("doesn't matter") 618 triggers := make([]*internal.Trigger, 0, maxTriggersAsSchedulerProperty+10) 619 add := func(i int) { 620 triggers = append(triggers, &internal.Trigger{ 621 Id: fmt.Sprintf("id=%d", i), 622 Payload: &internal.Trigger_Gitiles{ 623 Gitiles: &api.GitilesTrigger{ 624 Repo: "https://r.googlesource.com/repo", 625 Ref: "refs/heads/master", 626 Revision: fmt.Sprintf("sha1=%d", i), 627 }, 628 }, 629 }) 630 } 631 for i := 0; i < maxTriggersAsSchedulerProperty; i++ { 632 add(i) 633 } 634 635 propertiesString := func() string { 636 ctl.Req = task.Request{IncomingTriggers: triggers} 637 v, err := schedulerProperty(c, ctl) 638 So(err, ShouldBeNil) 639 return v.String() 640 } 641 642 s := propertiesString() 643 So(s, ShouldContainSubstring, "sha1=0") 644 So(s, ShouldContainSubstring, fmt.Sprintf("sha1=%d", maxTriggersAsSchedulerProperty-1)) 645 646 add(maxTriggersAsSchedulerProperty) 647 s = propertiesString() 648 So(s, ShouldContainSubstring, fmt.Sprintf("sha1=%d", maxTriggersAsSchedulerProperty)) 649 So(s, ShouldNotContainSubstring, "sha1=0") 650 }) 651 } 652 653 func TestExamineNotification(t *testing.T) { 654 t.Parallel() 655 656 Convey("Works", t, func() { 657 c := memory.Use(context.Background()) 658 mgr := TaskManager{} 659 660 Convey("v1 builds", func() { 661 tok := mgr.ExamineNotification(c, &pubsub.PubsubMessage{ 662 Attributes: map[string]string{"auth_token": "blah"}, 663 }) 664 So(tok, ShouldEqual, "blah") 665 }) 666 667 Convey("v2 builds", func() { 668 Convey("old pubsub message", func() { 669 call := func(data string) string { 670 return mgr.ExamineNotification(c, &pubsub.PubsubMessage{ 671 Data: data, 672 }) 673 } 674 So(call(base64.StdEncoding.EncodeToString([]byte(`{"user_data": "blah"}`))), ShouldEqual, "blah") 675 So(call(base64.StdEncoding.EncodeToString([]byte(`not json`))), ShouldEqual, "") 676 So(call("not base64"), ShouldEqual, "") 677 }) 678 Convey("new pubsub message", func() { 679 call := func(data string) string { 680 return mgr.ExamineNotification(c, &pubsub.PubsubMessage{ 681 Data: data, 682 Attributes: map[string]string{"version": "v2"}, 683 }) 684 } 685 686 ud := base64.StdEncoding.EncodeToString([]byte("blah")) 687 So(call(base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(`{"userData": "%s"}`, ud)))), ShouldEqual, "blah") 688 So(call(base64.StdEncoding.EncodeToString([]byte(`not json`))), ShouldEqual, "") 689 So(call("not base64"), ShouldEqual, "") 690 }) 691 }) 692 }) 693 } 694 695 func structFromJSON(json string) *structpb.Struct { 696 r := strings.NewReader(json) 697 s := &structpb.Struct{} 698 if err := (&jsonpb.Unmarshaler{}).Unmarshal(r, s); err != nil { 699 panic(err) 700 } 701 return s 702 } 703 704 func structKeys(s *structpb.Struct) []string { 705 keys := make([]string, 0, len(s.Fields)) 706 for k := range s.Fields { 707 keys = append(keys, k) 708 } 709 sort.Strings(keys) 710 return keys 711 } 712 713 func countTags(tags []*bbpb.StringPair, key string) (count int) { 714 for _, tag := range tags { 715 if tag.Key == key { 716 count++ 717 } 718 } 719 return 720 }