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

     1  // Copyright 2023 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  	"testing"
    20  
    21  	"google.golang.org/genproto/googleapis/rpc/status"
    22  	"google.golang.org/protobuf/types/known/emptypb"
    23  
    24  	"go.chromium.org/luci/gae/filter/txndefer"
    25  	"go.chromium.org/luci/gae/impl/memory"
    26  	"go.chromium.org/luci/gae/service/datastore"
    27  	"go.chromium.org/luci/server/auth"
    28  	"go.chromium.org/luci/server/auth/authtest"
    29  
    30  	"go.chromium.org/luci/buildbucket/appengine/model"
    31  	"go.chromium.org/luci/buildbucket/appengine/rpc/testutil"
    32  	"go.chromium.org/luci/buildbucket/bbperms"
    33  	pb "go.chromium.org/luci/buildbucket/proto"
    34  
    35  	. "github.com/smartystreets/goconvey/convey"
    36  	. "go.chromium.org/luci/common/testing/assertions"
    37  )
    38  
    39  func TestValidateSetBuilderHealthRequest(t *testing.T) {
    40  	t.Parallel()
    41  	Convey("validateSetBuilderHealthRequest", t, func() {
    42  		ctx := memory.Use(context.Background())
    43  		testutil.PutBucket(ctx, "project", "bucket", nil)
    44  
    45  		Convey("empty req", func() {
    46  			req := &pb.SetBuilderHealthRequest{}
    47  			err := validateRequest(ctx, req, nil, nil)
    48  			So(err, ShouldBeNil)
    49  		})
    50  
    51  		Convey("ok req", func() {
    52  			ctx = auth.WithState(ctx, &authtest.FakeState{
    53  				Identity: "user:someone@example.com",
    54  				IdentityPermissions: []authtest.RealmPermission{
    55  					{Realm: "project:bucket", Permission: bbperms.BuildersSetHealth},
    56  					{Realm: "builder:builder1", Permission: bbperms.BuildersSetHealth},
    57  					{Realm: "builder:builder2", Permission: bbperms.BuildersSetHealth},
    58  					{Realm: "builder:builder3", Permission: bbperms.BuildersSetHealth},
    59  				},
    60  			})
    61  			req := &pb.SetBuilderHealthRequest{
    62  				Health: []*pb.SetBuilderHealthRequest_BuilderHealth{
    63  					{
    64  						Id: &pb.BuilderID{
    65  							Bucket:  "bucket",
    66  							Project: "project",
    67  							Builder: "builder1",
    68  						},
    69  						Health: &pb.HealthStatus{HealthScore: 10},
    70  					},
    71  					{
    72  						Id: &pb.BuilderID{
    73  							Bucket:  "bucket",
    74  							Project: "project",
    75  							Builder: "builder2",
    76  						},
    77  						Health: &pb.HealthStatus{HealthScore: 0},
    78  					},
    79  					{
    80  						Id: &pb.BuilderID{
    81  							Bucket:  "bucket",
    82  							Project: "project",
    83  							Builder: "builder3",
    84  						},
    85  						Health: &pb.HealthStatus{HealthScore: 4},
    86  					},
    87  				},
    88  			}
    89  			resp := make([]*pb.SetBuilderHealthResponse_Response, 3)
    90  			err := validateRequest(ctx, req, nil, resp)
    91  			So(err, ShouldBeNil)
    92  			So(resp, ShouldResembleProto, []*pb.SetBuilderHealthResponse_Response{
    93  				nil, nil, nil,
    94  			})
    95  		})
    96  
    97  		Convey("miltiple entries", func() {
    98  			ctx = auth.WithState(ctx, &authtest.FakeState{
    99  				Identity: "user:someone@example.com",
   100  				IdentityPermissions: []authtest.RealmPermission{
   101  					{Realm: "project:bucket", Permission: bbperms.BuildersSetHealth},
   102  					{Realm: "builder:builder", Permission: bbperms.BuildersSetHealth},
   103  				},
   104  			})
   105  			req := &pb.SetBuilderHealthRequest{
   106  				Health: []*pb.SetBuilderHealthRequest_BuilderHealth{
   107  					{
   108  						Id: &pb.BuilderID{
   109  							Bucket:  "bucket",
   110  							Project: "project",
   111  							Builder: "builder",
   112  						},
   113  						Health: &pb.HealthStatus{HealthScore: 10},
   114  					},
   115  					{
   116  						Id: &pb.BuilderID{
   117  							Bucket:  "bucket",
   118  							Project: "project",
   119  							Builder: "builder",
   120  						},
   121  						Health: &pb.HealthStatus{HealthScore: 0},
   122  					},
   123  					{
   124  						Id: &pb.BuilderID{
   125  							Bucket:  "bucket",
   126  							Project: "project",
   127  							Builder: "builder",
   128  						},
   129  						Health: &pb.HealthStatus{HealthScore: 4},
   130  					},
   131  				},
   132  			}
   133  			resp := make([]*pb.SetBuilderHealthResponse_Response, 3)
   134  			err := validateRequest(ctx, req, nil, resp)
   135  			So(err, ShouldNotBeNil)
   136  			So(err.Error(), ShouldEqual, "The following builder has multiple entries: project/bucket/builder")
   137  		})
   138  
   139  		Convey("bad health score", func() {
   140  			errs := map[int]error{}
   141  			ctx = auth.WithState(ctx, &authtest.FakeState{
   142  				Identity: "user:someone@example.com",
   143  				IdentityPermissions: []authtest.RealmPermission{
   144  					{Realm: "project:bucket", Permission: bbperms.BuildersSetHealth},
   145  					{Realm: "builder:builder", Permission: bbperms.BuildersSetHealth},
   146  				},
   147  			})
   148  			req := &pb.SetBuilderHealthRequest{
   149  				Health: []*pb.SetBuilderHealthRequest_BuilderHealth{
   150  					{
   151  						Id: &pb.BuilderID{
   152  							Bucket:  "bucket",
   153  							Project: "project",
   154  							Builder: "builder",
   155  						},
   156  						Health: &pb.HealthStatus{HealthScore: 11},
   157  					},
   158  				},
   159  			}
   160  			resp := make([]*pb.SetBuilderHealthResponse_Response, 1)
   161  			err := validateRequest(ctx, req, errs, resp)
   162  			So(err, ShouldBeNil)
   163  			So(resp, ShouldResembleProto, []*pb.SetBuilderHealthResponse_Response{
   164  				{
   165  					Response: &pb.SetBuilderHealthResponse_Response_Error{
   166  						Error: &status.Status{
   167  							Code:    3,
   168  							Message: "Builder: project/bucket/builder: HealthScore should be between 0 and 10",
   169  						},
   170  					},
   171  				},
   172  			})
   173  		})
   174  
   175  		Convey("builderID not present, health is", func() {
   176  			errs := map[int]error{}
   177  			ctx = auth.WithState(ctx, &authtest.FakeState{
   178  				Identity: "user:someone@example.com",
   179  				IdentityPermissions: []authtest.RealmPermission{
   180  					{Realm: "project:bucket", Permission: bbperms.BuildersSetHealth},
   181  					{Realm: "builder:builder", Permission: bbperms.BuildersSetHealth},
   182  				},
   183  			})
   184  			req := &pb.SetBuilderHealthRequest{
   185  				Health: []*pb.SetBuilderHealthRequest_BuilderHealth{
   186  					{
   187  						Health: &pb.HealthStatus{HealthScore: 11},
   188  					},
   189  				},
   190  			}
   191  			err := validateRequest(ctx, req, errs, nil)
   192  			So(err, ShouldNotBeNil)
   193  			So(err.Error(), ShouldContainSubstring, ".health[0].id: required")
   194  		})
   195  	})
   196  }
   197  
   198  func TestSetBuilderHealth(t *testing.T) {
   199  	t.Parallel()
   200  
   201  	Convey("requests", t, func() {
   202  		ctx := memory.Use(context.Background())
   203  		srv := &Builders{}
   204  		testutil.PutBucket(ctx, "chrome", "cq", nil)
   205  		testutil.PutBucket(ctx, "chromeos", "cq", nil)
   206  
   207  		Convey("bad request; no perms", func() {
   208  			req := &pb.SetBuilderHealthRequest{
   209  				Health: []*pb.SetBuilderHealthRequest_BuilderHealth{
   210  					{
   211  						Id: &pb.BuilderID{
   212  							Project: "project",
   213  							Bucket:  "bucket",
   214  							Builder: "builder",
   215  						},
   216  						Health: &pb.HealthStatus{HealthScore: 12},
   217  					},
   218  					{
   219  						Id: &pb.BuilderID{
   220  							Project: "project2",
   221  							Bucket:  "bucket2",
   222  							Builder: "builder2",
   223  						},
   224  						Health: &pb.HealthStatus{HealthScore: 13},
   225  					},
   226  				},
   227  			}
   228  			resp, err := srv.SetBuilderHealth(ctx, req)
   229  			So(err, ShouldBeNil)
   230  			So(resp, ShouldResembleProto, &pb.SetBuilderHealthResponse{
   231  				Responses: []*pb.SetBuilderHealthResponse_Response{
   232  					{
   233  						Response: &pb.SetBuilderHealthResponse_Response_Error{
   234  							Error: &status.Status{
   235  								Code:    7,
   236  								Message: "Builder: project/bucket/builder: attaching a status: rpc error: code = NotFound desc = requested resource not found or \"anonymous:anonymous\" does not have permission to view it",
   237  							},
   238  						},
   239  					},
   240  					{
   241  						Response: &pb.SetBuilderHealthResponse_Response_Error{
   242  							Error: &status.Status{
   243  								Code:    7,
   244  								Message: "Builder: project2/bucket2/builder2: attaching a status: rpc error: code = NotFound desc = requested resource not found or \"anonymous:anonymous\" does not have permission to view it",
   245  							},
   246  						},
   247  					},
   248  				},
   249  			})
   250  		})
   251  
   252  		Convey("bad request; has perms", func() {
   253  			So(datastore.Put(ctx, &model.Builder{
   254  				ID:     "amd-cq",
   255  				Parent: model.BucketKey(ctx, "chrome", "cq"),
   256  			}), ShouldBeNil)
   257  			ctx = auth.WithState(ctx, &authtest.FakeState{
   258  				Identity: "user:someone@example.com",
   259  				IdentityPermissions: []authtest.RealmPermission{
   260  					{Realm: "chrome:cq", Permission: bbperms.BuildersSetHealth},
   261  					{Realm: "builder:amd-cq", Permission: bbperms.BuildersSetHealth},
   262  				},
   263  			})
   264  			req := &pb.SetBuilderHealthRequest{
   265  				Health: []*pb.SetBuilderHealthRequest_BuilderHealth{
   266  					{
   267  						Id: &pb.BuilderID{
   268  							Project: "chrome",
   269  							Bucket:  "cq",
   270  							Builder: "amd-cq",
   271  						},
   272  						Health: &pb.HealthStatus{HealthScore: 12},
   273  					},
   274  					{
   275  						Id:     &pb.BuilderID{},
   276  						Health: &pb.HealthStatus{HealthScore: 13},
   277  					},
   278  				},
   279  			}
   280  			_, err := srv.SetBuilderHealth(ctx, req)
   281  			So(err.Error(), ShouldContainSubstring, ".health[1].id.project: required (and 2 other errors)")
   282  		})
   283  
   284  		Convey("bad req; one no perm, one validation err, one no builder saved", func() {
   285  			So(datastore.Put(ctx, &model.Builder{
   286  				ID:     "amd-cq",
   287  				Parent: model.BucketKey(ctx, "chrome", "cq"),
   288  			}), ShouldBeNil)
   289  			So(datastore.Put(ctx, &model.Builder{
   290  				ID:     "amd-cq",
   291  				Parent: model.BucketKey(ctx, "chromeos", "cq"),
   292  			}), ShouldBeNil)
   293  			ctx = auth.WithState(ctx, &authtest.FakeState{
   294  				Identity: "user:someone@example.com",
   295  				IdentityPermissions: []authtest.RealmPermission{
   296  					{Realm: "chrome:cq", Permission: bbperms.BuildersSetHealth},
   297  					{Realm: "builder:amd-cq", Permission: bbperms.BuildersSetHealth},
   298  					{Realm: "builder:amd-cq-2", Permission: bbperms.BuildersSetHealth},
   299  				},
   300  			})
   301  			req := &pb.SetBuilderHealthRequest{
   302  				Health: []*pb.SetBuilderHealthRequest_BuilderHealth{
   303  					{
   304  						Id: &pb.BuilderID{
   305  							Project: "chrome",
   306  							Bucket:  "cq",
   307  							Builder: "amd-cq",
   308  						},
   309  						Health: &pb.HealthStatus{HealthScore: 12},
   310  					},
   311  					{
   312  						Id: &pb.BuilderID{
   313  							Project: "chrome",
   314  							Bucket:  "cq",
   315  							Builder: "amd-cq-2",
   316  						},
   317  						Health: &pb.HealthStatus{HealthScore: 8},
   318  					},
   319  					{
   320  						Id: &pb.BuilderID{
   321  							Project: "chromeos",
   322  							Bucket:  "cq",
   323  							Builder: "amd-cq",
   324  						},
   325  						Health: &pb.HealthStatus{HealthScore: 12},
   326  					},
   327  				},
   328  			}
   329  			resp, err := srv.SetBuilderHealth(ctx, req)
   330  			So(err, ShouldBeNil)
   331  			So(resp, ShouldResembleProto, &pb.SetBuilderHealthResponse{
   332  				Responses: []*pb.SetBuilderHealthResponse_Response{
   333  					{
   334  						Response: &pb.SetBuilderHealthResponse_Response_Error{
   335  							Error: &status.Status{
   336  								Message: "Builder: chrome/cq/amd-cq: HealthScore should be between 0 and 10",
   337  								Code:    3,
   338  							},
   339  						},
   340  					},
   341  					{
   342  						Response: &pb.SetBuilderHealthResponse_Response_Error{
   343  							Error: &status.Status{
   344  								Message: "attaching a status: rpc error: code = Internal desc = failed to get builder amd-cq-2: datastore: no such entity",
   345  								Code:    13,
   346  							},
   347  						},
   348  					},
   349  					{
   350  						Response: &pb.SetBuilderHealthResponse_Response_Error{
   351  							Error: &status.Status{
   352  								Message: "Builder: chromeos/cq/amd-cq: attaching a status: rpc error: code = NotFound desc = requested resource not found or \"user:someone@example.com\" does not have permission to view it",
   353  								Code:    7,
   354  							},
   355  						},
   356  					},
   357  				},
   358  			})
   359  		})
   360  	})
   361  
   362  	Convey("existing entities", t, func() {
   363  		ctx := memory.UseWithAppID(context.Background(), "fake-cr-buildbucket")
   364  		datastore.GetTestable(ctx).AutoIndex(true)
   365  		datastore.GetTestable(ctx).Consistent(true)
   366  		ctx = txndefer.FilterRDS(ctx)
   367  		srv := &Builders{}
   368  		testutil.PutBucket(ctx, "chrome", "cq", nil)
   369  
   370  		Convey("builders exists; update is normal", func() {
   371  			bktKey := model.BucketKey(ctx, "chrome", "cq")
   372  
   373  			bldrToPut1 := &model.Builder{
   374  				ID:     "amd-cq",
   375  				Parent: bktKey,
   376  				Config: &pb.BuilderConfig{
   377  					BuilderHealthMetricsLinks: &pb.BuilderConfig_BuilderHealthLinks{
   378  						DataLinks: map[string]string{
   379  							"user": "data-link-for-amd-cq",
   380  						},
   381  						DocLinks: map[string]string{
   382  							"user": "doc-link-for-amd-cq",
   383  						},
   384  					},
   385  				},
   386  			}
   387  			bldrToPut2 := &model.Builder{
   388  				ID:     "amd-cq-2",
   389  				Parent: bktKey,
   390  				Config: &pb.BuilderConfig{
   391  					BuilderHealthMetricsLinks: &pb.BuilderConfig_BuilderHealthLinks{
   392  						DataLinks: map[string]string{
   393  							"user": "data-link-for-amd-cq-2",
   394  						},
   395  						DocLinks: map[string]string{
   396  							"user": "doc-link-for-amd-cq-2",
   397  						},
   398  					},
   399  				},
   400  			}
   401  			bldrToPut3 := &model.Builder{
   402  				ID:     "amd-cq-3",
   403  				Parent: bktKey,
   404  			}
   405  			So(datastore.Put(ctx, bldrToPut1, bldrToPut2, bldrToPut3), ShouldBeNil)
   406  			ctx = auth.WithState(ctx, &authtest.FakeState{
   407  				Identity: "user:someone@example.com",
   408  				IdentityPermissions: []authtest.RealmPermission{
   409  					{Realm: "chrome:cq", Permission: bbperms.BuildersSetHealth},
   410  					{Realm: "builder:amd-cq", Permission: bbperms.BuildersSetHealth},
   411  					{Realm: "builder:amd-cq-2", Permission: bbperms.BuildersSetHealth},
   412  					{Realm: "builder:amd-cq-3", Permission: bbperms.BuildersSetHealth},
   413  				},
   414  			})
   415  			req := &pb.SetBuilderHealthRequest{
   416  				Health: []*pb.SetBuilderHealthRequest_BuilderHealth{
   417  					{
   418  						Id: &pb.BuilderID{
   419  							Project: "chrome",
   420  							Bucket:  "cq",
   421  							Builder: "amd-cq",
   422  						},
   423  						Health: &pb.HealthStatus{
   424  							HealthScore: 9,
   425  							DataLinks: map[string]string{
   426  								"user": "data-link-for-amd-cq-from-req",
   427  							},
   428  							DocLinks: map[string]string{
   429  								"user": "doc-link-for-amd-cq-from-req",
   430  							},
   431  						},
   432  					},
   433  					{
   434  						Id: &pb.BuilderID{
   435  							Project: "chrome",
   436  							Bucket:  "cq",
   437  							Builder: "amd-cq-2",
   438  						},
   439  						Health: &pb.HealthStatus{
   440  							HealthScore: 8,
   441  						},
   442  					},
   443  					{
   444  						Id: &pb.BuilderID{
   445  							Project: "chrome",
   446  							Bucket:  "cq",
   447  							Builder: "amd-cq-3",
   448  						},
   449  						Health: &pb.HealthStatus{
   450  							HealthScore: 2,
   451  						},
   452  					},
   453  				},
   454  			}
   455  			_, err := srv.SetBuilderHealth(ctx, req)
   456  			So(err, ShouldBeNil)
   457  			expectedBuilder1 := &model.Builder{ID: "amd-cq", Parent: bktKey}
   458  			expectedBuilder2 := &model.Builder{ID: "amd-cq-2", Parent: bktKey}
   459  			expectedBuilder3 := &model.Builder{ID: "amd-cq-3", Parent: bktKey}
   460  			So(datastore.Get(ctx, expectedBuilder1, expectedBuilder2, expectedBuilder3), ShouldBeNil)
   461  			So(expectedBuilder1.Metadata.Health.HealthScore, ShouldEqual, 9)
   462  			So(expectedBuilder1.Metadata.Health.Reporter, ShouldEqual, "someone@example.com")
   463  			So(expectedBuilder1.Metadata.Health.ReportedTime, ShouldNotBeNil)
   464  			So(expectedBuilder1.Metadata.Health.DataLinks, ShouldResemble, map[string]string{
   465  				"user": "data-link-for-amd-cq-from-req",
   466  			})
   467  			So(expectedBuilder1.Metadata.Health.DocLinks, ShouldResemble, map[string]string{
   468  				"user": "doc-link-for-amd-cq-from-req",
   469  			})
   470  			So(expectedBuilder2.Metadata.Health.HealthScore, ShouldEqual, 8)
   471  			So(expectedBuilder2.Metadata.Health.Reporter, ShouldEqual, "someone@example.com")
   472  			So(expectedBuilder2.Metadata.Health.ReportedTime, ShouldNotBeNil)
   473  			So(expectedBuilder2.Metadata.Health.DataLinks, ShouldResemble, map[string]string{
   474  				"user": "data-link-for-amd-cq-2",
   475  			})
   476  			So(expectedBuilder2.Metadata.Health.DocLinks, ShouldResemble, map[string]string{
   477  				"user": "doc-link-for-amd-cq-2",
   478  			})
   479  			So(expectedBuilder3.Metadata.Health.HealthScore, ShouldEqual, 2)
   480  			So(expectedBuilder3.Metadata.Health.Reporter, ShouldEqual, "someone@example.com")
   481  			So(expectedBuilder3.Metadata.Health.ReportedTime, ShouldNotBeNil)
   482  		})
   483  
   484  		Convey("one builder does not exist", func() {
   485  			bktKey := model.BucketKey(ctx, "chrome", "cq")
   486  			So(datastore.Put(ctx, &model.Builder{
   487  				ID:     "amd-cq",
   488  				Parent: bktKey,
   489  			}), ShouldBeNil)
   490  			ctx = auth.WithState(ctx, &authtest.FakeState{
   491  				Identity: "user:someone@example.com",
   492  				IdentityPermissions: []authtest.RealmPermission{
   493  					{Realm: "chrome:cq", Permission: bbperms.BuildersSetHealth},
   494  					{Realm: "builder:amd-cq", Permission: bbperms.BuildersSetHealth},
   495  				},
   496  			})
   497  			req := &pb.SetBuilderHealthRequest{
   498  				Health: []*pb.SetBuilderHealthRequest_BuilderHealth{
   499  					{
   500  						Id: &pb.BuilderID{
   501  							Project: "chrome",
   502  							Bucket:  "cq",
   503  							Builder: "amd-cq",
   504  						},
   505  						Health: &pb.HealthStatus{
   506  							HealthScore: 9,
   507  						},
   508  					},
   509  					{
   510  						Id: &pb.BuilderID{
   511  							Project: "chrome",
   512  							Bucket:  "cq",
   513  							Builder: "amd-cq-2",
   514  						},
   515  						Health: &pb.HealthStatus{
   516  							HealthScore: 8,
   517  						},
   518  					},
   519  				},
   520  			}
   521  			resp, err := srv.SetBuilderHealth(ctx, req)
   522  			So(err, ShouldBeNil)
   523  			So(resp, ShouldResembleProto, &pb.SetBuilderHealthResponse{
   524  				Responses: []*pb.SetBuilderHealthResponse_Response{
   525  					{
   526  						Response: &pb.SetBuilderHealthResponse_Response_Result{
   527  							Result: &emptypb.Empty{},
   528  						},
   529  					},
   530  					{
   531  						Response: &pb.SetBuilderHealthResponse_Response_Error{
   532  							Error: &status.Status{
   533  								Code:    13,
   534  								Message: "attaching a status: rpc error: code = Internal desc = failed to get builder amd-cq-2: datastore: no such entity",
   535  							},
   536  						},
   537  					},
   538  				},
   539  			})
   540  			expectedBuilder1 := &model.Builder{ID: "amd-cq", Parent: bktKey}
   541  			So(datastore.Get(ctx, expectedBuilder1), ShouldBeNil)
   542  			So(expectedBuilder1.Metadata.Health.HealthScore, ShouldEqual, 9)
   543  		})
   544  
   545  		Convey("multiple requests for same builder", func() {
   546  			So(datastore.Put(ctx, &model.Builder{
   547  				ID:     "amd-cq",
   548  				Parent: model.BucketKey(ctx, "chrome", "cq"),
   549  			}), ShouldBeNil)
   550  			ctx = auth.WithState(ctx, &authtest.FakeState{
   551  				Identity: "user:someone@example.com",
   552  				IdentityPermissions: []authtest.RealmPermission{
   553  					{Realm: "chrome:cq", Permission: bbperms.BuildersSetHealth},
   554  					{Realm: "builder:amd-cq", Permission: bbperms.BuildersSetHealth},
   555  				},
   556  			})
   557  			req := &pb.SetBuilderHealthRequest{
   558  				Health: []*pb.SetBuilderHealthRequest_BuilderHealth{
   559  					{
   560  						Id: &pb.BuilderID{
   561  							Project: "chrome",
   562  							Bucket:  "cq",
   563  							Builder: "amd-cq",
   564  						},
   565  						Health: &pb.HealthStatus{HealthScore: 12},
   566  					},
   567  					{
   568  						Id: &pb.BuilderID{
   569  							Project: "chrome",
   570  							Bucket:  "cq",
   571  							Builder: "amd-cq",
   572  						},
   573  						Health: &pb.HealthStatus{HealthScore: 13},
   574  					},
   575  				},
   576  			}
   577  			_, err := srv.SetBuilderHealth(ctx, req)
   578  			So(err.Error(), ShouldContainSubstring, "The following builder has multiple entries: chrome/cq/amd-cq")
   579  		})
   580  	})
   581  
   582  	Convey("links", t, func() {
   583  		ctx := memory.UseWithAppID(context.Background(), "fake-cr-buildbucket")
   584  		datastore.GetTestable(ctx).AutoIndex(true)
   585  		datastore.GetTestable(ctx).Consistent(true)
   586  		ctx = txndefer.FilterRDS(ctx)
   587  		srv := &Builders{}
   588  		testutil.PutBucket(ctx, "chrome", "cq", nil)
   589  		bktKey := model.BucketKey(ctx, "chrome", "cq")
   590  		So(datastore.Put(ctx, &model.Builder{
   591  			ID:     "amd-cq",
   592  			Parent: bktKey,
   593  			Config: &pb.BuilderConfig{
   594  				BuilderHealthMetricsLinks: &pb.BuilderConfig_BuilderHealthLinks{
   595  					DataLinks: map[string]string{
   596  						"google.com":   "go/somelink",
   597  						"chromium.org": "some_public_link.com",
   598  					},
   599  					DocLinks: map[string]string{
   600  						"google.com":   "go/some_doc_link",
   601  						"chromium.org": "some_public_doc_link.com",
   602  					},
   603  				},
   604  			},
   605  		}), ShouldBeNil)
   606  
   607  		Convey("links from cfg", func() {
   608  			ctx = auth.WithState(ctx, &authtest.FakeState{
   609  				Identity: "user:someone@google.com",
   610  				IdentityPermissions: []authtest.RealmPermission{
   611  					{Realm: "chrome:cq", Permission: bbperms.BuildersSetHealth},
   612  					{Realm: "builder:amd-cq", Permission: bbperms.BuildersSetHealth},
   613  				},
   614  			})
   615  			req := &pb.SetBuilderHealthRequest{
   616  				Health: []*pb.SetBuilderHealthRequest_BuilderHealth{
   617  					{
   618  						Id: &pb.BuilderID{
   619  							Project: "chrome",
   620  							Bucket:  "cq",
   621  							Builder: "amd-cq",
   622  						},
   623  						Health: &pb.HealthStatus{
   624  							HealthScore: 9,
   625  						},
   626  					},
   627  				},
   628  			}
   629  			_, err := srv.SetBuilderHealth(ctx, req)
   630  			So(err, ShouldBeNil)
   631  			expectedBuilder := &model.Builder{ID: "amd-cq", Parent: bktKey}
   632  			So(datastore.Get(ctx, expectedBuilder), ShouldBeNil)
   633  			So(expectedBuilder.Metadata.Health.DataLinks, ShouldResemble, map[string]string{
   634  				"google.com":   "go/somelink",
   635  				"chromium.org": "some_public_link.com",
   636  			})
   637  			So(expectedBuilder.Metadata.Health.DocLinks, ShouldResemble, map[string]string{
   638  				"google.com":   "go/some_doc_link",
   639  				"chromium.org": "some_public_doc_link.com",
   640  			})
   641  		})
   642  	})
   643  }