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  }