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  }