go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/config_service/internal/rules/project_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 rules 16 17 import ( 18 "testing" 19 20 "go.chromium.org/luci/config" 21 "go.chromium.org/luci/config/validation" 22 23 "go.chromium.org/luci/config_service/internal/common" 24 "go.chromium.org/luci/config_service/testutil" 25 26 . "github.com/smartystreets/goconvey/convey" 27 . "go.chromium.org/luci/common/testing/assertions" 28 ) 29 30 func TestValidateProjectsCfg(t *testing.T) { 31 t.Parallel() 32 33 Convey("Validate projects.cfg", t, func() { 34 ctx := testutil.SetupContext() 35 vctx := &validation.Context{Context: ctx} 36 cs := config.MustServiceSet(testutil.AppID) 37 path := common.ProjRegistryFilePath 38 39 Convey("valid", func() { 40 content := []byte(`teams { 41 name: "Example Team" 42 maintenance_contact: "team-maintenance@example.com" 43 escalation_contact: "team-escalation@google.com" 44 } 45 projects { 46 id: "example-proj" 47 owned_by: "Example Team" 48 gitiles_location { 49 repo: "https://example.googlesource.com/example" 50 ref: "refs/heads/main" 51 path: "infra/config/generated" 52 } 53 identity_config { 54 service_account_email: "example-sa@example.com" 55 } 56 }`) 57 So(validateProjectsCfg(vctx, string(cs), path, content), ShouldBeNil) 58 So(vctx.Finalize(), ShouldBeNil) 59 }) 60 61 Convey("invalid proto", func() { 62 content := []byte(`bad config`) 63 So(validateProjectsCfg(vctx, string(cs), path, content), ShouldBeNil) 64 So(vctx.Finalize(), ShouldErrLike, `in "projects.cfg"`, "invalid projects proto:") 65 }) 66 67 Convey("missing maintenance contact", func() { 68 content := []byte(`teams { 69 name: "Example Team" 70 escalation_contact: "team-escalation@google.com" 71 }`) 72 So(validateProjectsCfg(vctx, string(cs), path, content), ShouldBeNil) 73 So(vctx.Finalize(), ShouldErrLike, `(teams #0): maintenance_contact is required`) 74 }) 75 Convey("invalid maintenance contact", func() { 76 content := []byte(`teams { 77 name: "Example Team" 78 maintenance_contact: "bad email" 79 escalation_contact: "team-escalation@google.com" 80 }`) 81 So(validateProjectsCfg(vctx, string(cs), path, content), ShouldBeNil) 82 So(vctx.Finalize(), ShouldErrLike, `(teams #0 / maintenance_contact #0): invalid email address`) 83 }) 84 85 Convey("warn on missing escalation contact", func() { 86 content := []byte(`teams { 87 name: "Example Team" 88 maintenance_contact: "team-maintenance@example.com" 89 }`) 90 So(validateProjectsCfg(vctx, string(cs), path, content), ShouldBeNil) 91 vErr := vctx.Finalize().(*validation.Error) 92 So(vErr.WithSeverity(validation.Warning), ShouldErrLike, `(teams #0): escalation_contact is recommended`) 93 }) 94 Convey("invalid escalation contact", func() { 95 content := []byte(`teams { 96 name: "Example Team" 97 maintenance_contact: "team-maintenance@example.com" 98 escalation_contact: "bad email" 99 }`) 100 So(validateProjectsCfg(vctx, string(cs), path, content), ShouldBeNil) 101 So(vctx.Finalize(), ShouldErrLike, `(teams #0 / escalation_contact #0): invalid email address`) 102 }) 103 104 Convey("not sorted teams", func() { 105 content := []byte(`teams { 106 name: "Example Team B" 107 maintenance_contact: "team-b-maintenance@example.com" 108 escalation_contact: "team-b-escalation@google.com" 109 } 110 teams { 111 name: "Example Team A" 112 maintenance_contact: "team-a-maintenance@example.com" 113 escalation_contact: "team-a-escalation@google.com" 114 }`) 115 So(validateProjectsCfg(vctx, string(cs), path, content), ShouldBeNil) 116 So(vctx.Finalize(), ShouldErrLike, `teams are not sorted by id. First offending id: "Example Team A"`) 117 }) 118 119 Convey("invalid project ID", func() { 120 content := []byte(`teams { 121 name: "Example Team" 122 maintenance_contact: "team-maintenance@example.com" 123 escalation_contact: "team-escalation@google.com" 124 } 125 projects { 126 id: "bad/project" 127 owned_by: "Example Team" 128 gitiles_location { 129 repo: "https://example.googlesource.com/example" 130 ref: "refs/heads/main" 131 path: "infra/config/generated" 132 } 133 identity_config { 134 service_account_email: "example-sa@example.com" 135 } 136 }`) 137 So(validateProjectsCfg(vctx, string(cs), path, content), ShouldBeNil) 138 So(vctx.Finalize(), ShouldErrLike, `(projects #0 / id): invalid id:`) 139 }) 140 141 Convey("invalid gitiles location", func() { 142 content := []byte(`teams { 143 name: "Example Team" 144 maintenance_contact: "team-maintenance@example.com" 145 escalation_contact: "team-escalation@google.com" 146 } 147 projects { 148 id: "example-proj" 149 owned_by: "Example Team" 150 gitiles_location { 151 repo: "http://example.googlesource.com/another-example" 152 ref: "refs/heads/main" 153 path: "infra/config/generated" 154 } 155 identity_config { 156 service_account_email: "example-sa@example.com" 157 } 158 }`) 159 So(validateProjectsCfg(vctx, string(cs), path, content), ShouldBeNil) 160 So(vctx.Finalize(), ShouldErrLike, `(projects #0 / gitiles_location): repo: only https scheme is supported`) 161 }) 162 163 Convey("empty owned_by", func() { 164 content := []byte(`projects { 165 id: "example-proj" 166 owned_by: "" 167 gitiles_location { 168 repo: "https://example.googlesource.com/another-example" 169 ref: "refs/heads/main" 170 path: "infra/config/generated" 171 } 172 identity_config { 173 service_account_email: "example-sa@example.com" 174 } 175 }`) 176 So(validateProjectsCfg(vctx, string(cs), path, content), ShouldBeNil) 177 So(vctx.Finalize(), ShouldErrLike, `(projects #0 / owned_by): not specified`) 178 }) 179 Convey("unknown owned_by", func() { 180 content := []byte(`projects { 181 id: "example-proj" 182 owned_by: "Example Team" 183 gitiles_location { 184 repo: "https://example.googlesource.com/another-example" 185 ref: "refs/heads/main" 186 path: "infra/config/generated" 187 } 188 identity_config { 189 service_account_email: "example-sa@example.com" 190 } 191 }`) 192 So(validateProjectsCfg(vctx, string(cs), path, content), ShouldBeNil) 193 So(vctx.Finalize(), ShouldErrLike, `(projects #0 / owned_by): unknown team "Example Team`) 194 }) 195 }) 196 } 197 198 func TestValidateProjectMetadata(t *testing.T) { 199 t.Parallel() 200 201 Convey("Validate project.cfg", t, func() { 202 ctx := testutil.SetupContext() 203 vctx := &validation.Context{Context: ctx} 204 cs := config.MustServiceSet(testutil.AppID) 205 path := common.ProjMetadataFilePath 206 207 Convey("valid", func() { 208 content := []byte(` 209 name: "example-proj" 210 access: "group:all" 211 `) 212 So(validateProjectMetadata(vctx, string(cs), path, content), ShouldBeNil) 213 So(vctx.Finalize(), ShouldBeNil) 214 }) 215 216 Convey("invalid proto", func() { 217 content := []byte(`bad config`) 218 So(validateProjectMetadata(vctx, string(cs), path, content), ShouldBeNil) 219 So(vctx.Finalize(), ShouldErrLike, `in "project.cfg"`, "invalid project proto:") 220 }) 221 222 Convey("missing name", func() { 223 content := []byte(` 224 name: "" 225 access: "group:all" 226 `) 227 So(validateProjectMetadata(vctx, string(cs), path, content), ShouldBeNil) 228 So(vctx.Finalize(), ShouldErrLike, "name is not specified") 229 }) 230 231 Convey("invalid access group", func() { 232 content := []byte(` 233 name: "example-proj" 234 access: "group:goo!" 235 `) 236 So(validateProjectMetadata(vctx, string(cs), path, content), ShouldBeNil) 237 So(vctx.Finalize(), ShouldErrLike, `(access #0): invalid auth group`) 238 }) 239 }) 240 }