go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/internal/projectconfig/config_test.go (about) 1 // Copyright 2016 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 projectconfig 16 17 import ( 18 "testing" 19 20 buildbucketpb "go.chromium.org/luci/buildbucket/proto" 21 . "go.chromium.org/luci/common/testing/assertions" 22 "go.chromium.org/luci/gae/service/datastore" 23 projectconfigpb "go.chromium.org/luci/milo/proto/projectconfig" 24 25 "go.chromium.org/luci/appengine/gaetesting" 26 "go.chromium.org/luci/auth/identity" 27 "go.chromium.org/luci/config" 28 "go.chromium.org/luci/config/cfgclient" 29 memcfg "go.chromium.org/luci/config/impl/memory" 30 "go.chromium.org/luci/server/auth" 31 "go.chromium.org/luci/server/auth/authtest" 32 33 . "github.com/smartystreets/goconvey/convey" 34 ) 35 36 func TestConfig(t *testing.T) { 37 t.Parallel() 38 39 Convey("Test Environment", t, func() { 40 c := gaetesting.TestingContext() 41 datastore.GetTestable(c).Consistent(true) 42 43 Convey("Send update", func() { 44 c := cfgclient.Use(c, memcfg.New(mockedConfigs)) 45 So(UpdateProjects(c), ShouldBeNil) 46 47 Convey("Check created Project entities", func() { 48 foo := &Project{ID: "foo"} 49 So(datastore.Get(c, foo), ShouldBeNil) 50 So(foo.HasConfig, ShouldBeTrue) 51 So(foo.ACL, ShouldResemble, ACL{ 52 Groups: []string{"a", "b"}, 53 Identities: []identity.Identity{"user:a@example.com", "user:b@example.com"}, 54 }) 55 56 bar := &Project{ID: "bar"} 57 So(datastore.Get(c, bar), ShouldBeNil) 58 So(bar.HasConfig, ShouldBeTrue) 59 So(bar.ACL, ShouldResemble, ACL{}) 60 61 baz := &Project{ID: "baz"} 62 So(datastore.Get(c, baz), ShouldBeNil) 63 So(baz.HasConfig, ShouldBeFalse) 64 So(baz.ACL, ShouldResemble, ACL{ 65 Groups: []string{"a"}, 66 }) 67 68 external := &Project{ID: "external"} 69 So(datastore.Get(c, external), ShouldBeNil) 70 So(external.HasConfig, ShouldBeTrue) 71 So(external.ACL, ShouldResemble, ACL{ 72 Identities: []identity.Identity{"user:a@example.com", "user:e@example.com"}, 73 }) 74 }) 75 76 Convey("Check Console config updated", func() { 77 cs, err := GetConsole(c, "foo", "default") 78 So(err, ShouldBeNil) 79 So(cs.ID, ShouldEqual, "default") 80 So(cs.Ordinal, ShouldEqual, 0) 81 So(cs.Def.Header, ShouldBeNil) 82 }) 83 84 Convey("Check Console config updated with header", func() { 85 cs, err := GetConsole(c, "foo", "default_header") 86 So(err, ShouldBeNil) 87 So(cs.ID, ShouldEqual, "default_header") 88 So(cs.Ordinal, ShouldEqual, 1) 89 So(cs.Def.Header.Id, ShouldEqual, "main_header") 90 So(cs.Def.Header.TreeStatusHost, ShouldEqual, "blarg.example.com") 91 }) 92 93 Convey("Check Console config updated with realm", func() { 94 cs, err := GetConsole(c, "foo", "realm_test_console") 95 So(err, ShouldBeNil) 96 So(cs.ID, ShouldEqual, "realm_test_console") 97 So(cs.Ordinal, ShouldEqual, 2) 98 So(cs.Realm, ShouldEqual, "foo:fake_realm") 99 }) 100 101 Convey("Check Console config updated with builder ID", func() { 102 cs, err := GetConsole(c, "foo", "default_header") 103 So(err, ShouldBeNil) 104 So(cs.Def.Builders, ShouldResembleProto, []*projectconfigpb.Builder{ 105 { 106 Id: &buildbucketpb.BuilderID{ 107 Project: "foo", 108 Bucket: "something", 109 Builder: "bar", 110 }, 111 Name: "buildbucket/luci.foo.something/bar", 112 Category: "main|something", 113 ShortName: "s", 114 }, 115 { 116 Id: &buildbucketpb.BuilderID{ 117 Project: "foo", 118 Bucket: "other", 119 Builder: "baz", 120 }, 121 Name: "buildbucket/luci.foo.other/baz", 122 Category: "main|other", 123 ShortName: "o", 124 }, 125 }) 126 }) 127 128 Convey("Check external Console is resolved", func() { 129 cs, err := GetConsole(c, "external", "foo-default") 130 So(err, ShouldBeNil) 131 So(cs.Ordinal, ShouldEqual, 0) 132 So(cs.ID, ShouldEqual, "foo-default") 133 So(cs.Def.Id, ShouldEqual, "foo-default") 134 So(cs.Def.Name, ShouldEqual, "foo default") 135 So(cs.Def.ExternalProject, ShouldEqual, "foo") 136 So(cs.Def.ExternalId, ShouldEqual, "default") 137 So(cs.Builders, ShouldResemble, []string{"buildbucket/luci.foo.something/bar", "buildbucket/luci.foo.other/baz"}) 138 }) 139 140 Convey("Check user can see external consoles they have access to", func() { 141 cUser := auth.WithState(c, &authtest.FakeState{Identity: "user:a@example.com"}) 142 cs, err := GetProjectConsoles(cUser, "external") 143 So(err, ShouldBeNil) 144 145 ids := make([]string, 0, len(cs)) 146 for _, c := range cs { 147 ids = append(ids, c.ID) 148 } 149 So(ids, ShouldResemble, []string{"foo-default"}) 150 }) 151 152 Convey("Check user can't see external consoles they don't have access to", func() { 153 cUser := auth.WithState(c, &authtest.FakeState{Identity: "user:e@example.com"}) 154 cs, err := GetProjectConsoles(cUser, "external") 155 So(err, ShouldBeNil) 156 157 ids := make([]string, 0, len(cs)) 158 for _, c := range cs { 159 ids = append(ids, c.ID) 160 } 161 So(ids, ShouldHaveLength, 0) 162 }) 163 164 Convey("Check second update reorders", func() { 165 c := cfgclient.Use(c, memcfg.New(mockedConfigsUpdate)) 166 So(UpdateProjects(c), ShouldBeNil) 167 168 Convey("Check updated Project entities", func() { 169 foo := &Project{ID: "foo"} 170 So(datastore.Get(c, foo), ShouldBeNil) 171 So(foo.HasConfig, ShouldBeTrue) 172 So(foo.ACL, ShouldResemble, ACL{ 173 Identities: []identity.Identity{"user:a@example.com"}, 174 }) 175 176 bar := &Project{ID: "bar"} 177 So(datastore.Get(c, bar), ShouldBeNil) 178 So(bar.HasConfig, ShouldBeFalse) 179 So(bar.ACL, ShouldResemble, ACL{}) 180 181 So(datastore.Get(c, &Project{ID: "baz"}), ShouldEqual, datastore.ErrNoSuchEntity) 182 }) 183 184 Convey("Check Console config removed", func() { 185 cs, err := GetConsole(c, "foo", "default") 186 So(err, ShouldNotBeNil) 187 So(cs, ShouldBeNil) 188 }) 189 190 Convey("Check builder group configs in correct order", func() { 191 cs, err := GetConsole(c, "foo", "default_header") 192 So(err, ShouldBeNil) 193 So(cs.ID, ShouldEqual, "default_header") 194 So(cs.Ordinal, ShouldEqual, 0) 195 So(cs.Def.Header.Id, ShouldEqual, "main_header") 196 So(cs.Def.Header.TreeStatusHost, ShouldEqual, "blarg.example.com") 197 cs, err = GetConsole(c, "foo", "console.bar") 198 So(err, ShouldBeNil) 199 So(cs.ID, ShouldEqual, "console.bar") 200 So(cs.Ordinal, ShouldEqual, 1) 201 So(cs.Builders, ShouldResemble, []string{"buildbucket/luci.foo.something/bar"}) 202 203 cs, err = GetConsole(c, "foo", "console.baz") 204 So(err, ShouldBeNil) 205 So(cs.ID, ShouldEqual, "console.baz") 206 So(cs.Ordinal, ShouldEqual, 2) 207 So(cs.Builders, ShouldResemble, []string{"buildbucket/luci.foo.other/baz"}) 208 }) 209 210 Convey("Check getting project builder groups in correct order", func() { 211 cUser := auth.WithState(c, &authtest.FakeState{Identity: "user:a@example.com"}) 212 cs, err := GetProjectConsoles(cUser, "foo") 213 So(err, ShouldBeNil) 214 215 ids := make([]string, 0, len(cs)) 216 for _, c := range cs { 217 ids = append(ids, c.ID) 218 } 219 So(ids, ShouldResemble, []string{"default_header", "console.bar", "console.baz"}) 220 }) 221 }) 222 223 Convey("Check removing Milo config only", func() { 224 c := cfgclient.Use(c, memcfg.New(mockedConfigsNoConsole)) 225 So(UpdateProjects(c), ShouldBeNil) 226 227 Convey("Check kept the Project entity", func() { 228 foo := &Project{ID: "foo"} 229 So(datastore.Get(c, foo), ShouldBeNil) 230 So(foo.HasConfig, ShouldBeFalse) 231 So(foo.ACL, ShouldResemble, ACL{ 232 Groups: []string{"a", "b"}, 233 Identities: []identity.Identity{"user:a@example.com", "user:b@example.com"}, 234 }) 235 }) 236 237 Convey("Check removed the console", func() { 238 cs, err := GetConsole(c, "foo", "default") 239 So(err, ShouldNotBeNil) 240 So(cs, ShouldBeNil) 241 }) 242 }) 243 244 Convey("Check applying broken config", func() { 245 c := cfgclient.Use(c, memcfg.New(mockedConfigsBroken)) 246 So(UpdateProjects(c), ShouldNotBeNil) 247 248 Convey("Check kept the Project entity", func() { 249 foo := &Project{ID: "foo"} 250 So(datastore.Get(c, foo), ShouldBeNil) 251 So(foo.HasConfig, ShouldBeTrue) 252 So(foo.ACL, ShouldResemble, ACL{ 253 Groups: []string{"a", "b"}, 254 Identities: []identity.Identity{"user:a@example.com", "user:b@example.com"}, 255 }) 256 }) 257 258 Convey("Check kept the console", func() { 259 _, err := GetConsole(c, "foo", "default") 260 So(err, ShouldBeNil) 261 }) 262 }) 263 }) 264 }) 265 } 266 267 var fooCfg = ` 268 headers: { 269 id: "main_header" 270 tree_status_host: "blarg.example.com" 271 } 272 consoles: { 273 id: "default" 274 name: "default" 275 repo_url: "https://chromium.googlesource.com/foo/bar" 276 refs: "refs/heads/main" 277 manifest_name: "REVISION" 278 builders: { 279 id: { 280 project: "foo" 281 bucket: "something" 282 builder: "bar" 283 } 284 category: "main|something" 285 short_name: "s" 286 } 287 builders: { 288 id: { 289 project: "foo" 290 bucket: "other" 291 builder: "baz" 292 } 293 category: "main|other" 294 short_name: "o" 295 } 296 } 297 consoles: { 298 id: "default_header" 299 repo_url: "https://chromium.googlesource.com/foo/bar" 300 refs: "regexp:refs/heads/also-ok" 301 manifest_name: "REVISION" 302 builders: { 303 id: { 304 project: "foo" 305 bucket: "something" 306 builder: "bar" 307 } 308 name: "buildbucket/luci.foo.something/bar" 309 category: "main|something" 310 short_name: "s" 311 } 312 builders: { 313 name: "buildbucket/luci.foo.other/baz" 314 category: "main|other" 315 short_name: "o" 316 } 317 header_id: "main_header" 318 } 319 consoles: { 320 id: "realm_test_console" 321 name: "realm_test" 322 repo_url: "https://chromium.googlesource.com/foo/bar" 323 refs: "refs/heads/main" 324 realm: "foo:fake_realm" 325 manifest_name: "REVISION" 326 } 327 metadata_config: { 328 test_metadata_properties: { 329 schema: "package.name" 330 display_items: { 331 display_name: "owners" 332 path: "owners.email" 333 } 334 } 335 } 336 ` 337 338 var fooProjectCfg = ` 339 access: "a@example.com" 340 access: "user:a@example.com" 341 access: "user:b@example.com" 342 access: "group:a" 343 access: "group:a" 344 access: "group:b" 345 ` 346 347 var bazProjectCfg = ` 348 access: "group:a" 349 ` 350 351 var fooCfg2 = ` 352 headers: { 353 id: "main_header" 354 tree_status_host: "blarg.example.com" 355 } 356 consoles: { 357 id: "default_header" 358 repo_url: "https://chromium.googlesource.com/foo/bar" 359 refs: "refs/heads/main" 360 builders: { 361 name: "buildbucket/luci.foo.something/bar" 362 category: "main|something" 363 short_name: "s" 364 } 365 builders: { 366 name: "buildbucket/luci.foo.other/baz" 367 category: "main|other" 368 short_name: "o" 369 } 370 header_id: "main_header" 371 } 372 consoles: { 373 id: "console.bar" 374 repo_url: "https://chromium.googlesource.com/foo/bar" 375 refs: "refs/heads/main" 376 builders: { 377 name: "buildbucket/luci.foo.something/bar" 378 category: "main|something" 379 short_name: "s" 380 } 381 } 382 consoles: { 383 id: "console.baz" 384 repo_url: "https://chromium.googlesource.com/foo/bar" 385 refs: "refs/heads/main" 386 builders: { 387 name: "buildbucket/luci.foo.other/baz" 388 category: "main|other" 389 short_name: "o" 390 } 391 } 392 metadata_config: { 393 test_metadata_properties: { 394 schema: "package.name" 395 display_items: { 396 display_name: "owners" 397 path: "owners.email" 398 } 399 } 400 } 401 ` 402 403 var fooProjectCfg2 = ` 404 access: "a@example.com" 405 ` 406 407 var externalConsoleCfg = ` 408 consoles: { 409 id: "foo-default" 410 name: "foo default" 411 external_project: "foo" 412 external_id: "default" 413 } 414 ` 415 416 var externalProjectCfg = ` 417 access: "a@example.com" 418 access: "e@example.com" 419 ` 420 421 var badConsoleCfg = ` 422 consoles: { 423 id: "baz" 424 repo_url: "https://chromium.googlesource.com/foo/bar" 425 refs: "refs/heads/main" 426 manifest_name: "REVISION" 427 builders: { 428 name: "" 429 } 430 builders: { 431 name: "bad/scheme" 432 } 433 builders: { 434 id: { 435 project: "" 436 bucket: "bucket" 437 builder: "builder" 438 } 439 } 440 } 441 ` 442 443 var mockedConfigs = map[config.Set]memcfg.Files{ 444 "projects/foo": { 445 "${appid}.cfg": fooCfg, 446 "project.cfg": fooProjectCfg, 447 }, 448 "projects/bar": { 449 "${appid}.cfg": ``, // empty, but present 450 "project.cfg": ``, 451 }, 452 "projects/baz": { 453 // no Milo config 454 "project.cfg": bazProjectCfg, 455 }, 456 "projects/external": { 457 "${appid}.cfg": externalConsoleCfg, 458 "project.cfg": externalProjectCfg, 459 }, 460 } 461 462 var mockedConfigsUpdate = map[config.Set]memcfg.Files{ 463 "projects/foo": { 464 "${appid}.cfg": fooCfg2, 465 "project.cfg": fooProjectCfg2, 466 }, 467 "projects/bar": { 468 // No milo config any more 469 "project.cfg": ``, 470 }, 471 // No project/baz anymore. 472 } 473 474 // A copy of mockedConfigs with projects/foo and projects/external Milo configs 475 // removed. 476 var mockedConfigsNoConsole = map[config.Set]memcfg.Files{ 477 "projects/foo": { 478 "project.cfg": fooProjectCfg, 479 }, 480 "projects/bar": { 481 "${appid}.cfg": ``, // empty, but present 482 "project.cfg": ``, 483 }, 484 "projects/baz": { 485 // no Milo config 486 "project.cfg": bazProjectCfg, 487 }, 488 "projects/external": { 489 "project.cfg": externalProjectCfg, 490 }, 491 } 492 493 // A copy of mockedConfigs with projects/foo broken. 494 var mockedConfigsBroken = map[config.Set]memcfg.Files{ 495 "projects/foo": { 496 "${appid}.cfg": `broken milo config file`, 497 "project.cfg": fooProjectCfg, 498 }, 499 "projects/bar": { 500 "${appid}.cfg": ``, // empty, but present 501 "project.cfg": ``, 502 }, 503 "projects/baz": { 504 // no Milo config 505 "project.cfg": bazProjectCfg, 506 }, 507 "projects/external": { 508 "${appid}.cfg": externalConsoleCfg, 509 "project.cfg": externalProjectCfg, 510 }, 511 }