go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/config/project_config_test.go (about) 1 // Copyright 2022 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 config 16 17 import ( 18 "context" 19 "testing" 20 "time" 21 22 "google.golang.org/protobuf/encoding/prototext" 23 24 "go.chromium.org/luci/common/clock/testclock" 25 "go.chromium.org/luci/config" 26 "go.chromium.org/luci/config/cfgclient" 27 cfgmem "go.chromium.org/luci/config/impl/memory" 28 "go.chromium.org/luci/gae/impl/memory" 29 "go.chromium.org/luci/gae/service/datastore" 30 "go.chromium.org/luci/server/caching" 31 32 configpb "go.chromium.org/luci/resultdb/proto/config" 33 34 . "github.com/smartystreets/goconvey/convey" 35 . "go.chromium.org/luci/common/testing/assertions" 36 ) 37 38 var textPBMultiline = prototext.MarshalOptions{ 39 Multiline: true, 40 } 41 42 func TestProjectConfig(t *testing.T) { 43 t.Parallel() 44 45 Convey("SetTestProjectConfig updates context config", t, func() { 46 projectA := CreatePlaceholderProjectConfig() 47 configs := make(map[string]*configpb.ProjectConfig) 48 configs["a"] = projectA 49 50 ctx := memory.Use(context.Background()) 51 So(SetTestProjectConfig(ctx, configs), ShouldBeNil) 52 53 cfg, err := Projects(ctx) 54 55 So(err, ShouldBeNil) 56 So(len(cfg), ShouldEqual, 1) 57 So(cfg["a"], ShouldResembleProto, projectA) 58 }) 59 60 Convey("With mocks", t, func() { 61 projectA := CreatePlaceholderProjectConfig() 62 projectB := CreatePlaceholderProjectConfig() 63 So(len(projectB.GcsAllowList), ShouldEqual, 1) 64 projectB.GcsAllowList[0].Users = []string{"user:b@test.com"} 65 66 configs := map[config.Set]cfgmem.Files{ 67 "projects/a": {"${appid}.cfg": textPBMultiline.Format(projectA)}, 68 "projects/b": {"${appid}.cfg": textPBMultiline.Format(projectB)}, 69 } 70 71 ctx := memory.Use(context.Background()) 72 ctx, tc := testclock.UseTime(ctx, testclock.TestTimeUTC) 73 ctx = cfgclient.Use(ctx, cfgmem.New(configs)) 74 ctx = caching.WithEmptyProcessCache(ctx) 75 76 Convey("Update works", func() { 77 // Initial update. 78 err := UpdateProjects(ctx) 79 So(err, ShouldBeNil) 80 datastore.GetTestable(ctx).CatchupIndexes() 81 82 // Get works. 83 projects, err := Projects(ctx) 84 So(err, ShouldBeNil) 85 So(len(projects), ShouldEqual, 2) 86 So(projects["a"], ShouldResembleProto, projectA) 87 So(projects["b"], ShouldResembleProto, projectB) 88 89 // Noop update. 90 err = UpdateProjects(ctx) 91 So(err, ShouldBeNil) 92 datastore.GetTestable(ctx).CatchupIndexes() 93 94 // Real update. 95 projectC := CreatePlaceholderProjectConfig() 96 newProjectB := CreatePlaceholderProjectConfig() 97 So(len(newProjectB.GcsAllowList), ShouldEqual, 1) 98 newProjectB.GcsAllowList[0].Users = []string{"user:newb@test.com"} 99 delete(configs, "projects/a") 100 configs["projects/b"]["${appid}.cfg"] = textPBMultiline.Format(newProjectB) 101 configs["projects/c"] = cfgmem.Files{ 102 "${appid}.cfg": textPBMultiline.Format(projectC), 103 } 104 err = UpdateProjects(ctx) 105 So(err, ShouldBeNil) 106 datastore.GetTestable(ctx).CatchupIndexes() 107 108 // Fetch returns the new value right away. 109 projects, err = fetchProjects(ctx) 110 So(err, ShouldBeNil) 111 So(len(projects), ShouldEqual, 2) 112 So(projects["b"], ShouldResembleProto, newProjectB) 113 So(projects["c"], ShouldResembleProto, projectC) 114 115 // Get still uses in-memory cached copy. 116 projects, err = Projects(ctx) 117 So(err, ShouldBeNil) 118 So(len(projects), ShouldEqual, 2) 119 So(projects["a"], ShouldResembleProto, projectA) 120 So(projects["b"], ShouldResembleProto, projectB) 121 122 // Time passes, in-memory cached copy expires. 123 tc.Add(2 * time.Minute) 124 125 // Get returns the new value now too. 126 projects, err = Projects(ctx) 127 So(err, ShouldBeNil) 128 So(len(projects), ShouldEqual, 2) 129 So(projects["b"], ShouldResembleProto, newProjectB) 130 So(projects["c"], ShouldResembleProto, projectC) 131 132 // Time passes, in-memory cached copy expires. 133 tc.Add(2 * time.Minute) 134 135 // Get returns the same value. 136 projects, err = Projects(ctx) 137 So(err, ShouldBeNil) 138 So(len(projects), ShouldEqual, 2) 139 So(projects["b"], ShouldResembleProto, newProjectB) 140 So(projects["c"], ShouldResembleProto, projectC) 141 }) 142 143 Convey("Validation works", func() { 144 configs["projects/b"]["${appid}.cfg"] = `bad data` 145 err := UpdateProjects(ctx) 146 datastore.GetTestable(ctx).CatchupIndexes() 147 So(err, ShouldErrLike, "validation errors") 148 149 // Validation for project A passed and project is 150 // available, validation for project B failed 151 // as is not available. 152 projects, err := Projects(ctx) 153 So(err, ShouldBeNil) 154 So(len(projects), ShouldEqual, 1) 155 So(projects["a"], ShouldResembleProto, projectA) 156 }) 157 158 Convey("Update retains existing config if new config is invalid", func() { 159 // Initial update. 160 err := UpdateProjects(ctx) 161 So(err, ShouldBeNil) 162 datastore.GetTestable(ctx).CatchupIndexes() 163 164 // Get works. 165 projects, err := Projects(ctx) 166 So(err, ShouldBeNil) 167 So(len(projects), ShouldEqual, 2) 168 So(projects["a"], ShouldResembleProto, projectA) 169 So(projects["b"], ShouldResembleProto, projectB) 170 171 // Attempt to update with an invalid config for project B. 172 newProjectA := CreatePlaceholderProjectConfig() 173 So(len(newProjectA.GcsAllowList), ShouldEqual, 1) 174 newProjectA.GcsAllowList[0].Users = []string{"user:newa@test.com"} 175 newProjectB := CreatePlaceholderProjectConfig() 176 So(len(newProjectB.GcsAllowList), ShouldEqual, 1) 177 newProjectB.GcsAllowList[0].Users = []string{""} 178 configs["projects/a"]["${appid}.cfg"] = textPBMultiline.Format(newProjectA) 179 configs["projects/b"]["${appid}.cfg"] = textPBMultiline.Format(newProjectB) 180 err = UpdateProjects(ctx) 181 So(err, ShouldErrLike, "validation errors") 182 datastore.GetTestable(ctx).CatchupIndexes() 183 184 // Time passes, in-memory cached copy expires. 185 tc.Add(2 * time.Minute) 186 187 // Get returns the new configuration A and the old 188 // configuration for B. This ensures an attempt to push an invalid 189 // config does not result in a service outage for that project. 190 projects, err = Projects(ctx) 191 So(err, ShouldBeNil) 192 So(len(projects), ShouldEqual, 2) 193 So(projects["a"], ShouldResembleProto, newProjectA) 194 So(projects["b"], ShouldResembleProto, projectB) 195 }) 196 }) 197 } 198 199 func TestProject(t *testing.T) { 200 t.Parallel() 201 202 Convey("Project", t, func() { 203 pjChromium := CreatePlaceholderProjectConfig() 204 configs := map[string]*configpb.ProjectConfig{ 205 "chromium": pjChromium, 206 } 207 208 ctx := memory.Use(context.Background()) 209 So(SetTestProjectConfig(ctx, configs), ShouldBeNil) 210 211 Convey("success", func() { 212 pj, err := Project(ctx, "chromium") 213 So(err, ShouldBeNil) 214 So(pj, ShouldResembleProto, pjChromium) 215 }) 216 217 Convey("not found", func() { 218 pj, err := Project(ctx, "random") 219 So(err, ShouldErrLike, ErrNotFoundProjectConfig) 220 So(pj, ShouldBeNil) 221 }) 222 }) 223 }