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  }