go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/tasks/notification_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 tasks
    16  
    17  import (
    18  	"context"
    19  	"sort"
    20  	"testing"
    21  
    22  	"google.golang.org/protobuf/encoding/protojson"
    23  	"google.golang.org/protobuf/proto"
    24  	"google.golang.org/protobuf/types/known/structpb"
    25  
    26  	"go.chromium.org/luci/common/retry/transient"
    27  	"go.chromium.org/luci/gae/filter/txndefer"
    28  	"go.chromium.org/luci/gae/impl/memory"
    29  	"go.chromium.org/luci/gae/service/datastore"
    30  	"go.chromium.org/luci/server/auth"
    31  	"go.chromium.org/luci/server/auth/authtest"
    32  	"go.chromium.org/luci/server/tq"
    33  	"go.chromium.org/luci/server/tq/tqtesting"
    34  
    35  	"go.chromium.org/luci/buildbucket/appengine/internal/clients"
    36  	"go.chromium.org/luci/buildbucket/appengine/internal/compression"
    37  	"go.chromium.org/luci/buildbucket/appengine/model"
    38  	taskdefs "go.chromium.org/luci/buildbucket/appengine/tasks/defs"
    39  	pb "go.chromium.org/luci/buildbucket/proto"
    40  
    41  	. "github.com/smartystreets/goconvey/convey"
    42  
    43  	. "go.chromium.org/luci/common/testing/assertions"
    44  )
    45  
    46  func TestNotification(t *testing.T) {
    47  	t.Parallel()
    48  
    49  	Convey("notifyPubsub", t, func() {
    50  		ctx := auth.WithState(memory.Use(context.Background()), &authtest.FakeState{})
    51  		ctx = txndefer.FilterRDS(ctx)
    52  		ctx, sch := tq.TestingContext(ctx, nil)
    53  		datastore.GetTestable(ctx).AutoIndex(true)
    54  		datastore.GetTestable(ctx).Consistent(true)
    55  
    56  		sortTasksByClassName := func(tasks tqtesting.TaskList) {
    57  			sort.Slice(tasks, func(i, j int) bool {
    58  				return tasks[i].Class < tasks[j].Class
    59  			})
    60  		}
    61  
    62  		Convey("w/o callback", func() {
    63  			txErr := datastore.RunInTransaction(ctx, func(ctx context.Context) error {
    64  				return NotifyPubSub(ctx, &model.Build{
    65  					ID: 123,
    66  					Proto: &pb.Build{
    67  						Builder: &pb.BuilderID{
    68  							Project: "project",
    69  						},
    70  					},
    71  				})
    72  			}, nil)
    73  			So(txErr, ShouldBeNil)
    74  			tasks := sch.Tasks()
    75  			sortTasksByClassName(tasks)
    76  			So(tasks, ShouldHaveLength, 2)
    77  			So(tasks[0].Payload.(*taskdefs.NotifyPubSub).GetBuildId(), ShouldEqual, 123)
    78  			So(tasks[0].Payload.(*taskdefs.NotifyPubSub).GetCallback(), ShouldBeFalse)
    79  			So(tasks[1].Payload.(*taskdefs.NotifyPubSubGoProxy).GetBuildId(), ShouldEqual, 123)
    80  			So(tasks[1].Payload.(*taskdefs.NotifyPubSubGoProxy).GetProject(), ShouldEqual, "project")
    81  		})
    82  
    83  		Convey("w/ callback", func() {
    84  			txErr := datastore.RunInTransaction(ctx, func(ctx context.Context) error {
    85  				cb := model.PubSubCallback{
    86  					AuthToken: "token",
    87  					Topic:     "topic",
    88  					UserData:  []byte("user_data"),
    89  				}
    90  				return NotifyPubSub(ctx, &model.Build{
    91  					ID:             123,
    92  					PubSubCallback: cb,
    93  					Proto: &pb.Build{
    94  						Builder: &pb.BuilderID{
    95  							Project: "project",
    96  						},
    97  					},
    98  				})
    99  			}, nil)
   100  			So(txErr, ShouldBeNil)
   101  			tasks := sch.Tasks()
   102  			sortTasksByClassName(tasks)
   103  			So(tasks, ShouldHaveLength, 3)
   104  
   105  			n1 := tasks[0].Payload.(*taskdefs.NotifyPubSub)
   106  			n2 := tasks[1].Payload.(*taskdefs.NotifyPubSubGo)
   107  			So(n1.GetBuildId(), ShouldEqual, 123)
   108  			So(n1.GetCallback(), ShouldBeFalse)
   109  			So(n2.GetBuildId(), ShouldEqual, 123)
   110  			So(n2.GetCallback(), ShouldBeTrue)
   111  			So(n2.GetTopic().GetName(), ShouldEqual, "topic")
   112  
   113  			So(tasks[2].Payload.(*taskdefs.NotifyPubSubGoProxy).GetBuildId(), ShouldEqual, 123)
   114  			So(tasks[2].Payload.(*taskdefs.NotifyPubSubGoProxy).GetProject(), ShouldEqual, "project")
   115  		})
   116  
   117  		Convey("w/ callback (Python)", func() {
   118  			txErr := datastore.RunInTransaction(ctx, func(ctx context.Context) error {
   119  				cb := model.PubSubCallback{
   120  					AuthToken: "token",
   121  					Topic:     "projects/flutter-dashboard/topics/luci-builds",
   122  					UserData:  []byte("user_data"),
   123  				}
   124  				return NotifyPubSub(ctx, &model.Build{
   125  					ID:             123,
   126  					PubSubCallback: cb,
   127  					Proto: &pb.Build{
   128  						Builder: &pb.BuilderID{
   129  							Project: "project",
   130  						},
   131  					},
   132  				})
   133  			}, nil)
   134  			So(txErr, ShouldBeNil)
   135  			tasks := sch.Tasks()
   136  			sortTasksByClassName(tasks)
   137  			So(tasks, ShouldHaveLength, 3)
   138  
   139  			n1 := tasks[0].Payload.(*taskdefs.NotifyPubSub)
   140  			n2 := tasks[1].Payload.(*taskdefs.NotifyPubSub)
   141  			So(n1.GetBuildId(), ShouldEqual, 123)
   142  			So(n2.GetBuildId(), ShouldEqual, 123)
   143  			// One w/ callback and one w/o callback.
   144  			So(n1.GetCallback() != n2.GetCallback(), ShouldBeTrue)
   145  
   146  			So(tasks[2].Payload.(*taskdefs.NotifyPubSubGoProxy).GetBuildId(), ShouldEqual, 123)
   147  			So(tasks[2].Payload.(*taskdefs.NotifyPubSubGoProxy).GetProject(), ShouldEqual, "project")
   148  		})
   149  	})
   150  
   151  	Convey("EnqueueNotifyPubSubGo", t, func() {
   152  		ctx := auth.WithState(memory.Use(context.Background()), &authtest.FakeState{})
   153  		ctx = txndefer.FilterRDS(ctx)
   154  		ctx, sch := tq.TestingContext(ctx, nil)
   155  		datastore.GetTestable(ctx).AutoIndex(true)
   156  		datastore.GetTestable(ctx).Consistent(true)
   157  		So(datastore.Put(ctx, &model.Project{
   158  			ID: "project_with_external_topics",
   159  			CommonConfig: &pb.BuildbucketCfg_CommonConfig{
   160  				BuildsNotificationTopics: []*pb.BuildbucketCfg_Topic{
   161  					{
   162  						Name: "projects/my-cloud-project/topics/my-topic",
   163  					},
   164  				},
   165  			},
   166  		}), ShouldBeNil)
   167  
   168  		Convey("no project entity", func() {
   169  			txErr := EnqueueNotifyPubSubGo(ctx, 123, "project_no_external_topics")
   170  			So(txErr, ShouldBeNil)
   171  			tasks := sch.Tasks()
   172  			So(tasks, ShouldHaveLength, 1)
   173  			So(tasks[0].Payload, ShouldResembleProto, &taskdefs.NotifyPubSubGo{
   174  				BuildId: 123,
   175  			})
   176  		})
   177  
   178  		Convey("empty project.common_config", func() {
   179  			So(datastore.Put(ctx, &model.Project{
   180  				ID:           "project_empty",
   181  				CommonConfig: &pb.BuildbucketCfg_CommonConfig{},
   182  			}), ShouldBeNil)
   183  			txErr := EnqueueNotifyPubSubGo(ctx, 123, "project_empty")
   184  			So(txErr, ShouldBeNil)
   185  			tasks := sch.Tasks()
   186  			So(tasks, ShouldHaveLength, 1)
   187  			So(tasks[0].Payload, ShouldResembleProto, &taskdefs.NotifyPubSubGo{
   188  				BuildId: 123,
   189  			})
   190  		})
   191  
   192  		Convey("has external topics", func() {
   193  			txErr := EnqueueNotifyPubSubGo(ctx, 123, "project_with_external_topics")
   194  			So(txErr, ShouldBeNil)
   195  			tasks := sch.Tasks()
   196  			So(tasks, ShouldHaveLength, 2)
   197  			taskGo0 := tasks[0].Payload.(*taskdefs.NotifyPubSubGo)
   198  			taskGo1 := tasks[1].Payload.(*taskdefs.NotifyPubSubGo)
   199  			So(taskGo0.BuildId, ShouldEqual, 123)
   200  			So(taskGo1.BuildId, ShouldEqual, 123)
   201  			So(taskGo0.Topic.GetName()+taskGo1.Topic.GetName(), ShouldEqual, "projects/my-cloud-project/topics/my-topic")
   202  		})
   203  	})
   204  
   205  	Convey("PublishBuildsV2Notification", t, func() {
   206  		ctx := auth.WithState(memory.Use(context.Background()), &authtest.FakeState{})
   207  		ctx = txndefer.FilterRDS(ctx)
   208  		ctx, sch := tq.TestingContext(ctx, nil)
   209  		datastore.GetTestable(ctx).AutoIndex(true)
   210  		datastore.GetTestable(ctx).Consistent(true)
   211  
   212  		b := &model.Build{
   213  			ID: 123,
   214  			Proto: &pb.Build{
   215  				Id: 123,
   216  				Builder: &pb.BuilderID{
   217  					Project: "project",
   218  					Bucket:  "bucket",
   219  					Builder: "builder",
   220  				},
   221  				Status: pb.Status_CANCELED,
   222  			},
   223  		}
   224  		bk := datastore.KeyForObj(ctx, b)
   225  		bsBytes, err := proto.Marshal(&pb.Build{
   226  			Steps: []*pb.Step{
   227  				{
   228  					Name:            "step",
   229  					SummaryMarkdown: "summary",
   230  					Logs: []*pb.Log{{
   231  						Name:    "log1",
   232  						Url:     "url",
   233  						ViewUrl: "view_url",
   234  					},
   235  					},
   236  				},
   237  			},
   238  		})
   239  		So(err, ShouldBeNil)
   240  		bs := &model.BuildSteps{ID: 1, Build: bk, Bytes: bsBytes}
   241  		bi := &model.BuildInfra{
   242  			ID:    1,
   243  			Build: bk,
   244  			Proto: &pb.BuildInfra{
   245  				Buildbucket: &pb.BuildInfra_Buildbucket{
   246  					Hostname: "hostname",
   247  				},
   248  			},
   249  		}
   250  		bo := &model.BuildOutputProperties{
   251  			Build: bk,
   252  			Proto: &structpb.Struct{
   253  				Fields: map[string]*structpb.Value{
   254  					"output": {
   255  						Kind: &structpb.Value_StringValue{
   256  							StringValue: "output value",
   257  						},
   258  					},
   259  				},
   260  			},
   261  		}
   262  		binpProp := &model.BuildInputProperties{
   263  			Build: bk,
   264  			Proto: &structpb.Struct{
   265  				Fields: map[string]*structpb.Value{
   266  					"input": {
   267  						Kind: &structpb.Value_StringValue{
   268  							StringValue: "input value",
   269  						},
   270  					},
   271  				},
   272  			},
   273  		}
   274  		So(datastore.Put(ctx, b, bi, bs, bo, binpProp), ShouldBeNil)
   275  
   276  		Convey("build not exist", func() {
   277  			err := PublishBuildsV2Notification(ctx, 999, nil, false)
   278  			So(err, ShouldBeNil)
   279  			tasks := sch.Tasks()
   280  			So(tasks, ShouldHaveLength, 0)
   281  		})
   282  
   283  		Convey("To internal topic", func() {
   284  
   285  			Convey("success", func() {
   286  				err := PublishBuildsV2Notification(ctx, 123, nil, false)
   287  				So(err, ShouldBeNil)
   288  
   289  				tasks := sch.Tasks()
   290  				So(tasks, ShouldHaveLength, 1)
   291  				So(tasks[0].Message.Attributes["project"], ShouldEqual, "project")
   292  				So(tasks[0].Message.Attributes["is_completed"], ShouldEqual, "true")
   293  				So(tasks[0].Payload.(*pb.BuildsV2PubSub).GetBuild(), ShouldResembleProto, &pb.Build{
   294  					Id: 123,
   295  					Builder: &pb.BuilderID{
   296  						Project: "project",
   297  						Bucket:  "bucket",
   298  						Builder: "builder",
   299  					},
   300  					Status: pb.Status_CANCELED,
   301  					Infra: &pb.BuildInfra{
   302  						Buildbucket: &pb.BuildInfra_Buildbucket{
   303  							Hostname: "hostname",
   304  						},
   305  					},
   306  					Input:  &pb.Build_Input{},
   307  					Output: &pb.Build_Output{},
   308  				})
   309  				So(tasks[0].Payload.(*pb.BuildsV2PubSub).GetBuildLargeFields(), ShouldNotBeNil)
   310  				bLargeBytes := tasks[0].Payload.(*pb.BuildsV2PubSub).GetBuildLargeFields()
   311  				buildLarge, err := zlibUncompressBuild(bLargeBytes)
   312  				So(err, ShouldBeNil)
   313  				So(buildLarge, ShouldResembleProto, &pb.Build{
   314  					Steps: []*pb.Step{
   315  						{
   316  							Name:            "step",
   317  							SummaryMarkdown: "summary",
   318  							Logs: []*pb.Log{{
   319  								Name:    "log1",
   320  								Url:     "url",
   321  								ViewUrl: "view_url",
   322  							},
   323  							},
   324  						},
   325  					},
   326  					Input: &pb.Build_Input{
   327  						Properties: &structpb.Struct{
   328  							Fields: map[string]*structpb.Value{
   329  								"input": {
   330  									Kind: &structpb.Value_StringValue{
   331  										StringValue: "input value",
   332  									},
   333  								},
   334  							},
   335  						},
   336  					},
   337  					Output: &pb.Build_Output{
   338  						Properties: &structpb.Struct{
   339  							Fields: map[string]*structpb.Value{
   340  								"output": {
   341  									Kind: &structpb.Value_StringValue{
   342  										StringValue: "output value",
   343  									},
   344  								},
   345  							},
   346  						},
   347  					},
   348  				})
   349  			})
   350  
   351  			Convey("success - no large fields", func() {
   352  				b := &model.Build{
   353  					ID: 456,
   354  					Proto: &pb.Build{
   355  						Id: 456,
   356  						Builder: &pb.BuilderID{
   357  							Project: "project",
   358  							Bucket:  "bucket",
   359  							Builder: "builder",
   360  						},
   361  						Status: pb.Status_CANCELED,
   362  					},
   363  				}
   364  				bk := datastore.KeyForObj(ctx, b)
   365  				bi := &model.BuildInfra{
   366  					ID:    1,
   367  					Build: bk,
   368  					Proto: &pb.BuildInfra{
   369  						Buildbucket: &pb.BuildInfra_Buildbucket{
   370  							Hostname: "hostname",
   371  						},
   372  					},
   373  				}
   374  				So(datastore.Put(ctx, b, bi), ShouldBeNil)
   375  
   376  				err := PublishBuildsV2Notification(ctx, 456, nil, false)
   377  				So(err, ShouldBeNil)
   378  
   379  				tasks := sch.Tasks()
   380  				So(tasks, ShouldHaveLength, 1)
   381  				So(tasks[0].Payload.(*pb.BuildsV2PubSub).GetBuild(), ShouldResembleProto, &pb.Build{
   382  					Id: 456,
   383  					Builder: &pb.BuilderID{
   384  						Project: "project",
   385  						Bucket:  "bucket",
   386  						Builder: "builder",
   387  					},
   388  					Status: pb.Status_CANCELED,
   389  					Infra: &pb.BuildInfra{
   390  						Buildbucket: &pb.BuildInfra_Buildbucket{
   391  							Hostname: "hostname",
   392  						},
   393  					},
   394  					Input:  &pb.Build_Input{},
   395  					Output: &pb.Build_Output{},
   396  				})
   397  				So(tasks[0].Payload.(*pb.BuildsV2PubSub).GetBuildLargeFields(), ShouldNotBeNil)
   398  				bLargeBytes := tasks[0].Payload.(*pb.BuildsV2PubSub).GetBuildLargeFields()
   399  				buildLarge, err := zlibUncompressBuild(bLargeBytes)
   400  				So(err, ShouldBeNil)
   401  				So(buildLarge, ShouldResembleProto, &pb.Build{
   402  					Input:  &pb.Build_Input{},
   403  					Output: &pb.Build_Output{},
   404  				})
   405  			})
   406  		})
   407  
   408  		Convey("To external topic (non callback)", func() {
   409  			ctx, psserver, psclient, err := clients.SetupTestPubsub(ctx, "my-cloud-project")
   410  			So(err, ShouldBeNil)
   411  			defer func() {
   412  				psclient.Close()
   413  				psserver.Close()
   414  			}()
   415  			_, err = psclient.CreateTopic(ctx, "my-topic")
   416  			So(err, ShouldBeNil)
   417  
   418  			Convey("success (zlib compression)", func() {
   419  				err := PublishBuildsV2Notification(ctx, 123, &pb.BuildbucketCfg_Topic{Name: "projects/my-cloud-project/topics/my-topic"}, false)
   420  				So(err, ShouldBeNil)
   421  
   422  				tasks := sch.Tasks()
   423  				So(tasks, ShouldHaveLength, 0)
   424  				So(psserver.Messages(), ShouldHaveLength, 1)
   425  				publishedMsg := psserver.Messages()[0]
   426  
   427  				So(publishedMsg.Attributes["project"], ShouldEqual, "project")
   428  				So(publishedMsg.Attributes["bucket"], ShouldEqual, "bucket")
   429  				So(publishedMsg.Attributes["builder"], ShouldEqual, "builder")
   430  				So(publishedMsg.Attributes["is_completed"], ShouldEqual, "true")
   431  				buildMsg := &pb.BuildsV2PubSub{}
   432  				err = protojson.Unmarshal(publishedMsg.Data, buildMsg)
   433  				So(err, ShouldBeNil)
   434  				So(buildMsg.Build, ShouldResembleProto, &pb.Build{
   435  					Id: 123,
   436  					Builder: &pb.BuilderID{
   437  						Project: "project",
   438  						Bucket:  "bucket",
   439  						Builder: "builder",
   440  					},
   441  					Status: pb.Status_CANCELED,
   442  					Infra: &pb.BuildInfra{
   443  						Buildbucket: &pb.BuildInfra_Buildbucket{
   444  							Hostname: "hostname",
   445  						},
   446  					},
   447  					Input:  &pb.Build_Input{},
   448  					Output: &pb.Build_Output{},
   449  				})
   450  				So(buildMsg.BuildLargeFields, ShouldNotBeNil)
   451  				buildLarge, err := zlibUncompressBuild(buildMsg.BuildLargeFields)
   452  				So(err, ShouldBeNil)
   453  				So(buildLarge, ShouldResembleProto, &pb.Build{
   454  					Steps: []*pb.Step{
   455  						{
   456  							Name:            "step",
   457  							SummaryMarkdown: "summary",
   458  							Logs: []*pb.Log{{
   459  								Name:    "log1",
   460  								Url:     "url",
   461  								ViewUrl: "view_url",
   462  							},
   463  							},
   464  						},
   465  					},
   466  					Input: &pb.Build_Input{
   467  						Properties: &structpb.Struct{
   468  							Fields: map[string]*structpb.Value{
   469  								"input": {
   470  									Kind: &structpb.Value_StringValue{
   471  										StringValue: "input value",
   472  									},
   473  								},
   474  							},
   475  						},
   476  					},
   477  					Output: &pb.Build_Output{
   478  						Properties: &structpb.Struct{
   479  							Fields: map[string]*structpb.Value{
   480  								"output": {
   481  									Kind: &structpb.Value_StringValue{
   482  										StringValue: "output value",
   483  									},
   484  								},
   485  							},
   486  						},
   487  					},
   488  				})
   489  			})
   490  
   491  			Convey("success (zstd compression)", func() {
   492  				err := PublishBuildsV2Notification(ctx, 123, &pb.BuildbucketCfg_Topic{
   493  					Name:        "projects/my-cloud-project/topics/my-topic",
   494  					Compression: pb.Compression_ZSTD,
   495  				}, false)
   496  				So(err, ShouldBeNil)
   497  
   498  				tasks := sch.Tasks()
   499  				So(tasks, ShouldHaveLength, 0)
   500  				So(psserver.Messages(), ShouldHaveLength, 1)
   501  				publishedMsg := psserver.Messages()[0]
   502  
   503  				So(publishedMsg.Attributes["project"], ShouldEqual, "project")
   504  				So(publishedMsg.Attributes["bucket"], ShouldEqual, "bucket")
   505  				So(publishedMsg.Attributes["builder"], ShouldEqual, "builder")
   506  				So(publishedMsg.Attributes["is_completed"], ShouldEqual, "true")
   507  				buildMsg := &pb.BuildsV2PubSub{}
   508  				err = protojson.Unmarshal(publishedMsg.Data, buildMsg)
   509  				So(err, ShouldBeNil)
   510  				So(buildMsg.Build, ShouldResembleProto, &pb.Build{
   511  					Id: 123,
   512  					Builder: &pb.BuilderID{
   513  						Project: "project",
   514  						Bucket:  "bucket",
   515  						Builder: "builder",
   516  					},
   517  					Status: pb.Status_CANCELED,
   518  					Infra: &pb.BuildInfra{
   519  						Buildbucket: &pb.BuildInfra_Buildbucket{
   520  							Hostname: "hostname",
   521  						},
   522  					},
   523  					Input:  &pb.Build_Input{},
   524  					Output: &pb.Build_Output{},
   525  				})
   526  				So(buildMsg.BuildLargeFields, ShouldNotBeNil)
   527  				So(buildMsg.Compression, ShouldEqual, pb.Compression_ZSTD)
   528  				buildLarge, err := zstdUncompressBuild(buildMsg.BuildLargeFields)
   529  				So(err, ShouldBeNil)
   530  				So(buildLarge, ShouldResembleProto, &pb.Build{
   531  					Steps: []*pb.Step{
   532  						{
   533  							Name:            "step",
   534  							SummaryMarkdown: "summary",
   535  							Logs: []*pb.Log{{
   536  								Name:    "log1",
   537  								Url:     "url",
   538  								ViewUrl: "view_url",
   539  							},
   540  							},
   541  						},
   542  					},
   543  					Input: &pb.Build_Input{
   544  						Properties: &structpb.Struct{
   545  							Fields: map[string]*structpb.Value{
   546  								"input": {
   547  									Kind: &structpb.Value_StringValue{
   548  										StringValue: "input value",
   549  									},
   550  								},
   551  							},
   552  						},
   553  					},
   554  					Output: &pb.Build_Output{
   555  						Properties: &structpb.Struct{
   556  							Fields: map[string]*structpb.Value{
   557  								"output": {
   558  									Kind: &structpb.Value_StringValue{
   559  										StringValue: "output value",
   560  									},
   561  								},
   562  							},
   563  						},
   564  					},
   565  				})
   566  			})
   567  
   568  			Convey("non-exist topic", func() {
   569  				err := PublishBuildsV2Notification(ctx, 123, &pb.BuildbucketCfg_Topic{
   570  					Name: "projects/my-cloud-project/topics/non-exist-topic",
   571  				}, false)
   572  				So(err, ShouldNotBeNil)
   573  				So(transient.Tag.In(err), ShouldBeTrue)
   574  			})
   575  		})
   576  
   577  		Convey("To external topic (callback)", func() {
   578  			ctx, psserver, psclient, err := clients.SetupTestPubsub(ctx, "my-cloud-project")
   579  			So(err, ShouldBeNil)
   580  			defer func() {
   581  				psclient.Close()
   582  				psserver.Close()
   583  			}()
   584  			_, err = psclient.CreateTopic(ctx, "callback-topic")
   585  			So(err, ShouldBeNil)
   586  
   587  			So(datastore.Put(ctx, &model.Build{
   588  				ID:        999,
   589  				Project:   "project",
   590  				BucketID:  "bucket",
   591  				BuilderID: "builder",
   592  				Proto: &pb.Build{
   593  					Id: 999,
   594  					Builder: &pb.BuilderID{
   595  						Project: "project",
   596  						Bucket:  "bucket",
   597  						Builder: "builder",
   598  					},
   599  				},
   600  				PubSubCallback: model.PubSubCallback{
   601  					Topic:    "projects/my-cloud-project/topics/callback-topic",
   602  					UserData: []byte("userdata"),
   603  				},
   604  			}), ShouldBeNil)
   605  
   606  			err = PublishBuildsV2Notification(ctx, 999, &pb.BuildbucketCfg_Topic{Name: "projects/my-cloud-project/topics/callback-topic"}, true)
   607  			So(err, ShouldBeNil)
   608  
   609  			tasks := sch.Tasks()
   610  			So(tasks, ShouldHaveLength, 0)
   611  			So(psserver.Messages(), ShouldHaveLength, 1)
   612  			publishedMsg := psserver.Messages()[0]
   613  
   614  			So(publishedMsg.Attributes["project"], ShouldEqual, "project")
   615  			So(publishedMsg.Attributes["bucket"], ShouldEqual, "bucket")
   616  			So(publishedMsg.Attributes["builder"], ShouldEqual, "builder")
   617  			So(publishedMsg.Attributes["is_completed"], ShouldEqual, "false")
   618  			psCallbackMsg := &pb.PubSubCallBack{}
   619  			err = protojson.Unmarshal(publishedMsg.Data, psCallbackMsg)
   620  			So(err, ShouldBeNil)
   621  
   622  			buildLarge, err := zlibUncompressBuild(psCallbackMsg.BuildPubsub.BuildLargeFields)
   623  			So(err, ShouldBeNil)
   624  			So(buildLarge, ShouldResembleProto, &pb.Build{Input: &pb.Build_Input{}, Output: &pb.Build_Output{}})
   625  			So(psCallbackMsg.BuildPubsub.Build, ShouldResembleProto, &pb.Build{
   626  				Id: 999,
   627  				Builder: &pb.BuilderID{
   628  					Project: "project",
   629  					Bucket:  "bucket",
   630  					Builder: "builder",
   631  				},
   632  				Input:  &pb.Build_Input{},
   633  				Output: &pb.Build_Output{},
   634  			})
   635  			So(err, ShouldBeNil)
   636  			So(psCallbackMsg.UserData, ShouldResemble, []byte("userdata"))
   637  		})
   638  	})
   639  }
   640  
   641  func zlibUncompressBuild(compressed []byte) (*pb.Build, error) {
   642  	originalData, err := compression.ZlibDecompress(compressed)
   643  	if err != nil {
   644  		return nil, err
   645  	}
   646  	b := &pb.Build{}
   647  	if err := proto.Unmarshal(originalData, b); err != nil {
   648  		return nil, err
   649  	}
   650  	return b, nil
   651  }
   652  
   653  func zstdUncompressBuild(compressed []byte) (*pb.Build, error) {
   654  	originalData, err := compression.ZstdDecompress(compressed, nil)
   655  	if err != nil {
   656  		return nil, err
   657  	}
   658  	b := &pb.Build{}
   659  	if err := proto.Unmarshal(originalData, b); err != nil {
   660  		return nil, err
   661  	}
   662  	return b, nil
   663  }