github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/avatars/fullcaching_test.go (about) 1 package avatars 2 3 import ( 4 "fmt" 5 "io" 6 "net/http" 7 "os" 8 "runtime" 9 "strings" 10 "testing" 11 "time" 12 13 "github.com/keybase/client/go/kbhttp" 14 "github.com/keybase/client/go/libkb" 15 "github.com/keybase/client/go/protocol/keybase1" 16 "github.com/keybase/clockwork" 17 "github.com/stretchr/testify/require" 18 ) 19 20 func TestAvatarsFullCaching(t *testing.T) { 21 tc := libkb.SetupTest(t, "TestAvatarsFullCaching", 1) 22 defer tc.Cleanup() 23 clock := clockwork.NewFakeClock() 24 tc.G.SetClock(clock) 25 26 testSrv := kbhttp.NewSrv(tc.G.GetLog(), kbhttp.NewRandomPortRangeListenerSource(7000, 8000)) 27 require.NoError(t, testSrv.Start()) 28 testSrv.HandleFunc("/p", func(w http.ResponseWriter, req *http.Request) { 29 fmt.Fprintf(w, "hi") 30 }) 31 testSrv.HandleFunc("/p2", func(w http.ResponseWriter, req *http.Request) { 32 fmt.Fprintf(w, "hi2") 33 }) 34 testSrv.HandleFunc("/p3", func(w http.ResponseWriter, req *http.Request) { 35 fmt.Fprintf(w, "hi3") 36 }) 37 38 cb := make(chan struct{}, 5) 39 a, _ := testSrv.Addr() 40 testSrvAddr := fmt.Sprintf("http://%s/p", a) 41 tc.G.API = newAvatarMockAPI(makeHandler(testSrvAddr, cb)) 42 m := libkb.NewMetaContextForTest(tc) 43 source := NewFullCachingSource(tc.G, time.Hour, 1) 44 source.populateSuccessCh = make(chan struct{}, 5) 45 source.tempDir = os.TempDir() 46 source.StartBackgroundTasks(m) 47 defer source.StopBackgroundTasks(m) 48 49 t.Logf("first blood") 50 res, err := source.LoadUsers(m, []string{"mike"}, []keybase1.AvatarFormat{"square"}) 51 require.NoError(t, err) 52 require.Equal(t, testSrvAddr, res.Picmap["mike"]["square"].String()) 53 select { 54 case <-cb: 55 case <-time.After(20 * time.Second): 56 require.Fail(t, "no API call") 57 } 58 select { 59 case <-source.populateSuccessCh: 60 case <-time.After(20 * time.Second): 61 require.Fail(t, "no populate") 62 } 63 64 t.Log("cache hit") 65 66 convertPath := func(path string) string { 67 path = strings.TrimPrefix(path, "file://") 68 if runtime.GOOS == "windows" { 69 path = strings.ReplaceAll(path, `/`, `\`) 70 path = path[1:] 71 } 72 return path 73 } 74 75 getFile := func(path string) string { 76 path = convertPath(path) 77 file, err := os.Open(path) 78 require.NoError(t, err) 79 defer file.Close() 80 dat, err := io.ReadAll(file) 81 require.NoError(t, err) 82 return string(dat) 83 } 84 res, err = source.LoadUsers(m, []string{"mike"}, []keybase1.AvatarFormat{"square"}) 85 require.NoError(t, err) 86 select { 87 case <-cb: 88 require.Fail(t, "no API call") 89 default: 90 } 91 select { 92 case <-source.populateSuccessCh: 93 require.Fail(t, "no populate") 94 default: 95 } 96 mikePath := res.Picmap["mike"]["square"].String() 97 require.NotEqual(t, testSrvAddr, mikePath) 98 require.True(t, strings.HasPrefix(mikePath, "file://")) 99 require.Equal(t, "hi", getFile(mikePath)) 100 101 t.Log("stale") 102 testSrvAddr = fmt.Sprintf("http://%s/p2", a) 103 tc.G.API = newAvatarMockAPI(makeHandler(testSrvAddr, cb)) 104 clock.Advance(2 * time.Hour) 105 res, err = source.LoadUsers(m, []string{"mike"}, []keybase1.AvatarFormat{"square"}) 106 require.NoError(t, err) 107 select { 108 case <-cb: 109 case <-time.After(20 * time.Second): 110 require.Fail(t, "no API call") 111 } 112 select { 113 case <-source.populateSuccessCh: 114 case <-time.After(20 * time.Second): 115 require.Fail(t, "no populate") 116 } 117 mikePath2 := res.Picmap["mike"]["square"].String() 118 require.Equal(t, mikePath, mikePath2) 119 res, err = source.LoadUsers(m, []string{"mike"}, []keybase1.AvatarFormat{"square"}) 120 require.NoError(t, err) 121 select { 122 case <-cb: 123 require.Fail(t, "no API call") 124 default: 125 } 126 select { 127 case <-source.populateSuccessCh: 128 require.Fail(t, "no populate") 129 default: 130 } 131 mikePath2 = res.Picmap["mike"]["square"].String() 132 require.Equal(t, mikePath2, mikePath) 133 require.Equal(t, "hi2", getFile(mikePath2)) 134 135 // load a second user to validate we clear when the LRU is full 136 res, err = source.LoadUsers(m, []string{"josh"}, []keybase1.AvatarFormat{"square"}) 137 require.NoError(t, err) 138 require.Equal(t, testSrvAddr, res.Picmap["josh"]["square"].String()) 139 select { 140 case <-cb: 141 case <-time.After(20 * time.Second): 142 require.Fail(t, "no API call") 143 } 144 select { 145 case <-source.populateSuccessCh: 146 case <-time.After(20 * time.Second): 147 require.Fail(t, "no populate") 148 } 149 150 res, err = source.LoadUsers(m, []string{"josh"}, []keybase1.AvatarFormat{"square"}) 151 require.NoError(t, err) 152 select { 153 case <-cb: 154 require.Fail(t, "no API call") 155 default: 156 } 157 select { 158 case <-source.populateSuccessCh: 159 require.Fail(t, "no populate") 160 default: 161 } 162 joshPath := res.Picmap["josh"]["square"].String() 163 require.NotEqual(t, testSrvAddr, mikePath2) 164 require.True(t, strings.HasPrefix(joshPath, "file://")) 165 require.Equal(t, "hi2", getFile(joshPath)) 166 167 // mike was evicted 168 _, err = os.Stat(convertPath(mikePath2)) 169 require.Error(t, err) 170 require.True(t, os.IsNotExist(err)) 171 172 err = source.ClearCacheForName(m, "josh", []keybase1.AvatarFormat{"square"}) 173 require.NoError(t, err) 174 175 _, err = os.Stat(convertPath(joshPath)) 176 require.Error(t, err) 177 require.True(t, os.IsNotExist(err)) 178 }