go.chromium.org/luci@v0.0.0-20250314024836-d9a61d0730e6/tokenserver/appengine/impl/utils/policy/policy_test.go (about)

     1  // Copyright 2017 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 policy
    16  
    17  import (
    18  	"context"
    19  	"math/rand"
    20  	"testing"
    21  	"time"
    22  
    23  	"google.golang.org/protobuf/types/known/timestamppb"
    24  
    25  	"go.chromium.org/luci/appengine/gaetesting"
    26  	"go.chromium.org/luci/common/clock/testclock"
    27  	"go.chromium.org/luci/common/data/rand/mathrand"
    28  	"go.chromium.org/luci/common/testing/ftt"
    29  	"go.chromium.org/luci/common/testing/truth/assert"
    30  	"go.chromium.org/luci/common/testing/truth/should"
    31  	"go.chromium.org/luci/config/validation"
    32  	"go.chromium.org/luci/gae/filter/count"
    33  )
    34  
    35  // queryableForm implements Queryable.
    36  type queryableForm struct {
    37  	rev    string
    38  	bundle ConfigBundle
    39  }
    40  
    41  func (q *queryableForm) ConfigRevision() string {
    42  	return q.rev
    43  }
    44  
    45  func TestImportConfigs(t *testing.T) {
    46  	t.Parallel()
    47  
    48  	ftt.Run("Happy path", t, func(t *ftt.Test) {
    49  		base := gaetesting.TestingContext()
    50  		c := prepareServiceConfig(base, map[string]string{
    51  			"config_1.cfg": "seconds: 12345",
    52  			"config_2.cfg": "seconds: 67890",
    53  		})
    54  
    55  		fetchCalls := 0
    56  		prepareCalls := 0
    57  
    58  		p := Policy{
    59  			Name: "testing",
    60  			Fetch: func(c context.Context, f ConfigFetcher) (ConfigBundle, error) {
    61  				fetchCalls++
    62  				var ts1, ts2 timestamppb.Timestamp
    63  				assert.Loosely(t, f.FetchTextProto(c, "config_1.cfg", &ts1), should.BeNil)
    64  				assert.Loosely(t, f.FetchTextProto(c, "config_2.cfg", &ts2), should.BeNil)
    65  				return ConfigBundle{"config_1.cfg": &ts1, "config_2.cfg": &ts2}, nil
    66  			},
    67  			Prepare: func(c context.Context, cfg ConfigBundle, revision string) (Queryable, error) {
    68  				prepareCalls++
    69  				return &queryableForm{revision, cfg}, nil
    70  			},
    71  		}
    72  
    73  		// Fetched for the first time.
    74  		rev, err := p.ImportConfigs(c)
    75  		assert.Loosely(t, err, should.BeNil)
    76  		assert.Loosely(t, rev, should.Equal("dc17888617ce2d87e0e33c1ad12034d51127fda3"))
    77  		assert.Loosely(t, fetchCalls, should.Equal(1))
    78  		assert.Loosely(t, prepareCalls, should.Equal(1))
    79  
    80  		// No change in configs -> early exit.
    81  		rev, err = p.ImportConfigs(c)
    82  		assert.Loosely(t, err, should.BeNil)
    83  		assert.Loosely(t, rev, should.Equal("dc17888617ce2d87e0e33c1ad12034d51127fda3"))
    84  		assert.Loosely(t, fetchCalls, should.Equal(2))
    85  		assert.Loosely(t, prepareCalls, should.Equal(1))
    86  
    87  		// New configs appear.
    88  		c = prepareServiceConfig(base, map[string]string{
    89  			"config_1.cfg": "seconds: 11111",
    90  			"config_2.cfg": "seconds: 22222",
    91  		})
    92  		rev, err = p.ImportConfigs(c)
    93  		assert.Loosely(t, err, should.BeNil)
    94  		assert.Loosely(t, rev, should.Equal("6bc6fb514320d908db4955ff8769f59e2cd0ae09"))
    95  		assert.Loosely(t, fetchCalls, should.Equal(3))
    96  		assert.Loosely(t, prepareCalls, should.Equal(2))
    97  	})
    98  
    99  	ftt.Run("Validation errors", t, func(t *ftt.Test) {
   100  		base := gaetesting.TestingContext()
   101  		c := prepareServiceConfig(base, map[string]string{
   102  			"config.cfg": "seconds: 12345",
   103  		})
   104  
   105  		p := Policy{
   106  			Name: "testing",
   107  			Fetch: func(c context.Context, f ConfigFetcher) (ConfigBundle, error) {
   108  				var ts timestamppb.Timestamp
   109  				assert.Loosely(t, f.FetchTextProto(c, "config.cfg", &ts), should.BeNil)
   110  				return ConfigBundle{"config.cfg": &ts}, nil
   111  			},
   112  			Validate: func(v *validation.Context, cfg ConfigBundle) {
   113  				assert.Loosely(t, cfg["config.cfg"], should.NotBeNil)
   114  				v.SetFile("config.cfg")
   115  				v.Errorf("validation error")
   116  			},
   117  			Prepare: func(c context.Context, cfg ConfigBundle, revision string) (Queryable, error) {
   118  				panic("must not be called")
   119  			},
   120  		}
   121  
   122  		_, err := p.ImportConfigs(c)
   123  		assert.Loosely(t, err, should.ErrLike(`in "config.cfg": validation error`))
   124  	})
   125  }
   126  
   127  func TestQueryable(t *testing.T) {
   128  	t.Parallel()
   129  
   130  	ftt.Run("Works", t, func(t *ftt.Test) {
   131  		base, tc := testclock.UseTime(gaetesting.TestingContext(), testclock.TestTimeUTC)
   132  		base = mathrand.Set(base, rand.New(rand.NewSource(2)))
   133  		base, counter := count.FilterRDS(base)
   134  
   135  		p := Policy{
   136  			Name: "testing",
   137  			Fetch: func(c context.Context, f ConfigFetcher) (ConfigBundle, error) {
   138  				var ts timestamppb.Timestamp
   139  				assert.Loosely(t, f.FetchTextProto(c, "config.cfg", &ts), should.BeNil)
   140  				return ConfigBundle{"config.cfg": &ts}, nil
   141  			},
   142  			Prepare: func(c context.Context, cfg ConfigBundle, revision string) (Queryable, error) {
   143  				return &queryableForm{revision, cfg}, nil
   144  			},
   145  		}
   146  
   147  		// No imported configs yet.
   148  		q, err := p.Queryable(base)
   149  		assert.Loosely(t, err, should.Equal(ErrNoPolicy))
   150  		assert.Loosely(t, q, should.BeNil)
   151  
   152  		c1 := prepareServiceConfig(base, map[string]string{
   153  			"config.cfg": "seconds: 12345",
   154  		})
   155  		rev1, err := p.ImportConfigs(c1)
   156  		assert.Loosely(t, err, should.BeNil)
   157  
   158  		q, err = p.Queryable(base)
   159  		assert.Loosely(t, err, should.BeNil)
   160  		assert.Loosely(t, q.ConfigRevision(), should.Equal(rev1))
   161  		qf := q.(*queryableForm)
   162  		assert.Loosely(t, qf.bundle["config.cfg"], should.Match(&timestamppb.Timestamp{Seconds: 12345}))
   163  
   164  		assert.Loosely(t, counter.GetMulti.Total(), should.Equal(4))
   165  
   166  		// 3 min later using exact same config, no datastore calls.
   167  		tc.Add(3 * time.Minute)
   168  		q, err = p.Queryable(base)
   169  		assert.Loosely(t, err, should.BeNil)
   170  		assert.Loosely(t, q.(*queryableForm), should.Equal(qf))
   171  		assert.Loosely(t, counter.GetMulti.Total(), should.Equal(4))
   172  
   173  		// 2 min after that, the datastore is rechecked, since the local cache has
   174  		// expired. No new configs there, exact same 'q' is returned.
   175  		tc.Add(2 * time.Minute)
   176  		q, err = p.Queryable(base)
   177  		assert.Loosely(t, err, should.BeNil)
   178  		assert.Loosely(t, q.(*queryableForm), should.Equal(qf))
   179  		assert.Loosely(t, counter.GetMulti.Total(), should.Equal(5)) // datastore call!
   180  
   181  		// Config has been updated.
   182  		c2 := prepareServiceConfig(base, map[string]string{
   183  			"config.cfg": "seconds: 6789",
   184  		})
   185  		rev2, err := p.ImportConfigs(c2)
   186  		assert.Loosely(t, err, should.BeNil)
   187  
   188  		// Currently cached config is still fresh though, so it is still used.
   189  		q, err = p.Queryable(base)
   190  		assert.Loosely(t, err, should.BeNil)
   191  		assert.Loosely(t, q.ConfigRevision(), should.Equal(rev1))
   192  
   193  		// 5 minutes later the cached copy expires and new one is fetched from
   194  		// the datastore.
   195  		tc.Add(5 * time.Minute)
   196  		q, err = p.Queryable(base)
   197  		assert.Loosely(t, err, should.BeNil)
   198  		assert.Loosely(t, q.ConfigRevision(), should.Equal(rev2))
   199  		qf = q.(*queryableForm)
   200  		assert.Loosely(t, qf.bundle["config.cfg"], should.Match(&timestamppb.Timestamp{Seconds: 6789}))
   201  	})
   202  }