go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/auth/internal/disk_cache_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 internal 16 17 import ( 18 "context" 19 "io/ioutil" 20 "os" 21 "path/filepath" 22 "testing" 23 "time" 24 25 "golang.org/x/oauth2" 26 27 "go.chromium.org/luci/common/clock" 28 "go.chromium.org/luci/common/clock/testclock" 29 30 . "github.com/smartystreets/goconvey/convey" 31 ) 32 33 func TestDiskTokenCache(t *testing.T) { 34 t.Parallel() 35 36 tmp, err := ioutil.TempDir("", "disk_token_cache") 37 if err != nil { 38 panic(err) 39 } 40 defer os.RemoveAll(tmp) 41 42 ctx := context.Background() 43 ctx, tc := testclock.UseTime(ctx, testclock.TestRecentTimeUTC.Local()) 44 45 Convey("DiskTokenCache works", t, func() { 46 // testCacheSemantics is in proc_cache_test.go. 47 testCacheSemantics(ctx, &DiskTokenCache{ 48 Context: ctx, 49 SecretsDir: tmp, 50 }) 51 }) 52 53 Convey("Retains unknown cacheFileEntry fields", t, func() { 54 cacheFile := filepath.Join(tmp, "tokens.json") 55 testData := ` 56 { 57 "cache": [ 58 { 59 "key": {"key": "a"}, 60 "token": { 61 "access_token": "abc" 62 }, 63 "email": "a@example.com", 64 "random_stuff": {"abc": {"def": "zzz"}}, 65 "abc": "def" 66 } 67 ], 68 "last_update": "2021-02-08T23:18:00.463912Z" 69 } 70 ` 71 So(os.WriteFile(cacheFile, []byte(testData), 0600), ShouldBeNil) 72 73 cache := &DiskTokenCache{ 74 Context: ctx, 75 SecretsDir: tmp, 76 } 77 78 tok, err := cache.GetToken(&CacheKey{Key: "a"}) 79 So(err, ShouldBeNil) 80 So(tok, ShouldResemble, &Token{ 81 Token: oauth2.Token{AccessToken: "abc"}, 82 Email: "a@example.com", 83 }) 84 So(cache.PutToken(&CacheKey{Key: "a"}, &Token{ 85 Token: oauth2.Token{ 86 AccessToken: "def", 87 Expiry: clock.Now(ctx).Add(time.Hour).UTC(), 88 }, 89 Email: "a@example.com", 90 }), ShouldBeNil) 91 92 // Check "random_stuff" and "abc" were preserved by the update. 93 blob, err := os.ReadFile(cacheFile) 94 So(err, ShouldBeNil) 95 So(string(blob), ShouldEqual, `{ 96 "cache": [ 97 { 98 "key": { 99 "key": "a" 100 }, 101 "token": { 102 "access_token": "def", 103 "expiry": "2016-02-03T05:05:06.000000007Z" 104 }, 105 "id_token": "", 106 "email": "a@example.com", 107 "last_update": "2016-02-03T04:05:06.000000007Z", 108 "abc": "def", 109 "random_stuff": { 110 "abc": { 111 "def": "zzz" 112 } 113 } 114 } 115 ], 116 "last_update": "2016-02-03T04:05:06.000000007Z" 117 }`) 118 }) 119 120 Convey("Merges creds.json and tokens.json", t, func() { 121 oldCacheFile := filepath.Join(tmp, "creds.json") 122 oldCacheFileData := ` 123 { 124 "cache": [ 125 { 126 "key": {"key": "a"}, 127 "token": { 128 "access_token": "abc", 129 "expiry": "2016-02-03T07:00:00Z" 130 }, 131 "email": "a@example.com", 132 "last_update": "2016-02-03T05:00:00Z" 133 }, 134 { 135 "key": {"key": "b"}, 136 "token": { 137 "access_token": "def", 138 "expiry": "2016-02-03T07:00:00Z" 139 }, 140 "email": "a@example.com", 141 "last_update": "2016-02-03T05:00:00Z" 142 } 143 ], 144 "last_update": "2016-02-03T05:00:00Z" 145 } 146 ` 147 148 newCacheFile := filepath.Join(tmp, "tokens.json") 149 newCacheFileData := ` 150 { 151 "cache": [ 152 { 153 "key": {"key": "a"}, 154 "token": { 155 "access_token": "better-abc", 156 "expiry": "2016-02-03T07:00:00Z" 157 }, 158 "email": "a@example.com", 159 "last_update": "2016-02-03T06:00:00Z", 160 "extra": "zzz" 161 }, 162 { 163 "key": {"key": "c"}, 164 "token": { 165 "access_token": "zzz", 166 "expiry": "2016-02-03T07:00:00Z" 167 }, 168 "email": "a@example.com", 169 "last_update": "2016-02-03T06:00:00Z" 170 } 171 ], 172 "last_update": "2016-02-03T06:00:00Z" 173 } 174 ` 175 176 So(os.WriteFile(oldCacheFile, []byte(oldCacheFileData), 0600), ShouldBeNil) 177 So(os.WriteFile(newCacheFile, []byte(newCacheFileData), 0600), ShouldBeNil) 178 179 cache := &DiskTokenCache{ 180 Context: ctx, 181 SecretsDir: tmp, 182 } 183 184 tok, err := cache.GetToken(&CacheKey{Key: "a"}) 185 So(err, ShouldBeNil) 186 So(tok.Token.AccessToken, ShouldEqual, "better-abc") 187 So(cache.PutToken(&CacheKey{Key: "a"}, &Token{ 188 Token: oauth2.Token{ 189 AccessToken: "xyz", 190 Expiry: clock.Now(ctx).Add(time.Hour).UTC(), 191 }, 192 Email: "a@example.com", 193 }), ShouldBeNil) 194 195 updatedOld, err := os.ReadFile(oldCacheFile) 196 So(err, ShouldBeNil) 197 So(string(updatedOld), ShouldEqual, `{ 198 "cache": [ 199 { 200 "key": { 201 "key": "a" 202 }, 203 "token": { 204 "access_token": "xyz", 205 "expiry": "2016-02-03T05:05:06.000000007Z" 206 }, 207 "id_token": "", 208 "email": "a@example.com", 209 "last_update": "2016-02-03T04:05:06.000000007Z", 210 "extra": "zzz" 211 }, 212 { 213 "key": { 214 "key": "c" 215 }, 216 "token": { 217 "access_token": "zzz", 218 "expiry": "2016-02-03T07:00:00Z" 219 }, 220 "id_token": "", 221 "email": "a@example.com", 222 "last_update": "2016-02-03T06:00:00Z" 223 }, 224 { 225 "key": { 226 "key": "b" 227 }, 228 "token": { 229 "access_token": "def", 230 "expiry": "2016-02-03T07:00:00Z" 231 }, 232 "id_token": "", 233 "email": "a@example.com", 234 "last_update": "2016-02-03T05:00:00Z" 235 } 236 ], 237 "last_update": "2016-02-03T05:00:00Z" 238 }`) 239 240 // tokens.json is almost identical, except last_update is newer. 241 updatedNew, err := os.ReadFile(newCacheFile) 242 So(err, ShouldBeNil) 243 So(string(updatedNew), ShouldEqual, `{ 244 "cache": [ 245 { 246 "key": { 247 "key": "a" 248 }, 249 "token": { 250 "access_token": "xyz", 251 "expiry": "2016-02-03T05:05:06.000000007Z" 252 }, 253 "id_token": "", 254 "email": "a@example.com", 255 "last_update": "2016-02-03T04:05:06.000000007Z", 256 "extra": "zzz" 257 }, 258 { 259 "key": { 260 "key": "c" 261 }, 262 "token": { 263 "access_token": "zzz", 264 "expiry": "2016-02-03T07:00:00Z" 265 }, 266 "id_token": "", 267 "email": "a@example.com", 268 "last_update": "2016-02-03T06:00:00Z" 269 }, 270 { 271 "key": { 272 "key": "b" 273 }, 274 "token": { 275 "access_token": "def", 276 "expiry": "2016-02-03T07:00:00Z" 277 }, 278 "id_token": "", 279 "email": "a@example.com", 280 "last_update": "2016-02-03T05:00:00Z" 281 } 282 ], 283 "last_update": "2016-02-03T04:05:06.000000007Z" 284 }`) 285 }) 286 287 // TODO(vadimsh): This test is flaky on Windows, there's non zero probability 288 // that all 15 attempts (see testCacheInParallel) will hit "Access is denied" 289 // error. This can be "fixed" by increasing number of attempts or sleeping 290 // more between attempts. Both increase test runtime. 291 SkipConvey("DiskTokenCache works (parallel)", t, func() { 292 // testCacheInParallel is in proc_cache_test.go. 293 // 294 // Use real clock here to test real-world interaction when retrying disk 295 // writes. 296 ctx := context.Background() 297 testCacheInParallel(ctx, &DiskTokenCache{ 298 Context: ctx, 299 SecretsDir: tmp, 300 }) 301 }) 302 303 Convey("Cleans up old tokens", t, func() { 304 cache := &DiskTokenCache{ 305 Context: ctx, 306 SecretsDir: tmp, 307 } 308 309 cache.PutToken(&CacheKey{Key: "a"}, &Token{ 310 Token: oauth2.Token{ 311 AccessToken: "abc", 312 Expiry: clock.Now(ctx), 313 }, 314 }) 315 cache.PutToken(&CacheKey{Key: "b"}, &Token{ 316 Token: oauth2.Token{ 317 AccessToken: "abc", 318 RefreshToken: "def", 319 Expiry: clock.Now(ctx), 320 }, 321 }) 322 323 // GCAccessTokenMaxAge later, "a" is gone while the cache is updated. 324 tc.Add(GCAccessTokenMaxAge) 325 unused := &Token{ 326 Token: oauth2.Token{ 327 AccessToken: "zzz", 328 Expiry: clock.Now(ctx).Add(365 * 24 * time.Hour), 329 }, 330 } 331 cache.PutToken(&CacheKey{Key: "unused"}, unused) 332 333 t, err := cache.GetToken(&CacheKey{Key: "a"}) 334 So(err, ShouldBeNil) 335 So(t, ShouldBeNil) 336 337 // "b" is still there. 338 t, err = cache.GetToken(&CacheKey{Key: "b"}) 339 So(err, ShouldBeNil) 340 So(t.RefreshToken, ShouldEqual, "def") 341 342 // Some time later "b" is also removed. 343 tc.Add(GCRefreshTokenMaxAge) 344 cache.PutToken(&CacheKey{Key: "unused"}, unused) 345 346 t, err = cache.GetToken(&CacheKey{Key: "b"}) 347 So(err, ShouldBeNil) 348 So(t, ShouldBeNil) 349 }) 350 }