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  }