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(×tamppb.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(×tamppb.Timestamp{Seconds: 6789})) 201 }) 202 }