@@ -13,8 +13,10 @@ import org.robolectric.RuntimeEnvironment
1313import org.robolectric.annotation.Config
1414import kotlin.test.Test
1515import kotlin.test.assertEquals
16+ import kotlin.test.assertFalse
1617import kotlin.test.assertNotNull
1718import kotlin.test.assertNull
19+ import kotlin.test.assertTrue
1820
1921@OptIn(ExperimentalCoilApi ::class )
2022@RunWith(RobolectricTestRunner ::class )
@@ -30,52 +32,121 @@ class ETagCacheStrategyTest {
3032 return Options (context = RuntimeEnvironment .getApplication(), extras = extras)
3133 }
3234
33- private val baseCacheResponse = NetworkResponse (
34- code = 200 ,
35- headers = NetworkHeaders .EMPTY ,
36- )
35+ private fun freshCacheResponse (
36+ maxAgeSeconds : Long = 3600,
37+ etag : String? = null,
38+ lastModified : String? = null,
39+ ): NetworkResponse {
40+ val headers = NetworkHeaders .Builder ()
41+ .set(" cache-control" , " public, max-age=$maxAgeSeconds " )
42+ if (etag != null ) headers.set(" etag" , etag)
43+ if (lastModified != null ) headers.set(" last-modified" , lastModified)
44+ return NetworkResponse (
45+ code = 200 ,
46+ responseMillis = System .currentTimeMillis() - 60_000 , // 1 minute ago
47+ headers = headers.build(),
48+ )
49+ }
50+
51+ private fun staleCacheResponse (
52+ maxAgeSeconds : Long = 3600,
53+ etag : String? = null,
54+ lastModified : String? = null,
55+ ): NetworkResponse {
56+ val headers = NetworkHeaders .Builder ()
57+ .set(" cache-control" , " public, max-age=$maxAgeSeconds " )
58+ if (etag != null ) headers.set(" etag" , etag)
59+ if (lastModified != null ) headers.set(" last-modified" , lastModified)
60+ return NetworkResponse (
61+ code = 200 ,
62+ responseMillis = System .currentTimeMillis() - (maxAgeSeconds * 1000 + 60_000 ),
63+ headers = headers.build(),
64+ )
65+ }
3766
3867 private val baseNetworkRequest = NetworkRequest (url = " https://example.com/icon.png" )
3968
69+ // region key disabled
70+
4071 @Test
4172 fun `without key returns cache response` () = runTest {
42- val result = strategy.read(baseCacheResponse , baseNetworkRequest, options(false ))
73+ val result = strategy.read(staleCacheResponse() , baseNetworkRequest, options(false ))
4374 assertNotNull(result.response)
4475 assertNull(result.request)
4576 }
4677
78+ // endregion
79+
80+ // region freshness
81+
4782 @Test
48- fun `with key and cached etag adds If-None-Match` () = runTest {
49- val cached = baseCacheResponse.copy(
83+ fun `with key and fresh cache returns cache response` () = runTest {
84+ val cached = freshCacheResponse(etag = " \" abc123\" " )
85+ val result = strategy.read(cached, baseNetworkRequest, options(true ))
86+ assertNotNull(result.response)
87+ assertNull(result.request)
88+ }
89+
90+ @Test
91+ fun `with key and stale cache revalidates` () = runTest {
92+ val cached = staleCacheResponse(etag = " \" abc123\" " )
93+ val result = strategy.read(cached, baseNetworkRequest, options(true ))
94+ assertNotNull(result.request)
95+ assertNull(result.response)
96+ }
97+
98+ @Test
99+ fun `with key and no cache-control header treats as stale` () = runTest {
100+ val cached = NetworkResponse (
101+ code = 200 ,
102+ responseMillis = System .currentTimeMillis(),
50103 headers = NetworkHeaders .Builder ()
51104 .set(" etag" , " \" abc123\" " )
52- .build()
105+ .build(),
53106 )
54107 val result = strategy.read(cached, baseNetworkRequest, options(true ))
55108 assertNotNull(result.request)
56- assertNull(result.response)
57- assertEquals(" \" abc123\" " , result.request!! .headers[" if-none-match" ])
58109 }
59110
60111 @Test
61- fun `with key and cached last-modified adds If-Modified-Since` () = runTest {
62- val cached = baseCacheResponse.copy(
112+ fun `with key and zero responseMillis treats as stale` () = runTest {
113+ val cached = NetworkResponse (
114+ code = 200 ,
115+ responseMillis = 0L ,
63116 headers = NetworkHeaders .Builder ()
64- .set(" last-modified" , " Wed, 21 Oct 2015 07:28:00 GMT" )
65- .build()
117+ .set(" cache-control" , " public, max-age=3600" )
118+ .set(" etag" , " \" abc123\" " )
119+ .build(),
66120 )
67121 val result = strategy.read(cached, baseNetworkRequest, options(true ))
68122 assertNotNull(result.request)
123+ }
124+
125+ // endregion
126+
127+ // region conditional headers
128+
129+ @Test
130+ fun `stale cache with etag adds If-None-Match` () = runTest {
131+ val cached = staleCacheResponse(etag = " \" abc123\" " )
132+ val result = strategy.read(cached, baseNetworkRequest, options(true ))
133+ assertNotNull(result.request)
134+ assertEquals(" \" abc123\" " , result.request!! .headers[" if-none-match" ])
135+ }
136+
137+ @Test
138+ fun `stale cache with last-modified adds If-Modified-Since` () = runTest {
139+ val cached = staleCacheResponse(lastModified = " Wed, 21 Oct 2015 07:28:00 GMT" )
140+ val result = strategy.read(cached, baseNetworkRequest, options(true ))
141+ assertNotNull(result.request)
69142 assertEquals(" Wed, 21 Oct 2015 07:28:00 GMT" , result.request!! .headers[" if-modified-since" ])
70143 }
71144
72145 @Test
73- fun `with key and both headers adds both conditional headers` () = runTest {
74- val cached = baseCacheResponse.copy(
75- headers = NetworkHeaders .Builder ()
76- .set(" etag" , " \" abc123\" " )
77- .set(" last-modified" , " Wed, 21 Oct 2015 07:28:00 GMT" )
78- .build()
146+ fun `stale cache with both headers adds both conditional headers` () = runTest {
147+ val cached = staleCacheResponse(
148+ etag = " \" abc123\" " ,
149+ lastModified = " Wed, 21 Oct 2015 07:28:00 GMT" ,
79150 )
80151 val result = strategy.read(cached, baseNetworkRequest, options(true ))
81152 assertNotNull(result.request)
@@ -84,11 +155,51 @@ class ETagCacheStrategyTest {
84155 }
85156
86157 @Test
87- fun `with key and no cached headers sends plain request` () = runTest {
88- val result = strategy.read(baseCacheResponse, baseNetworkRequest, options(true ))
158+ fun `stale cache with no etag or last-modified sends plain request` () = runTest {
159+ val cached = staleCacheResponse()
160+ val result = strategy.read(cached, baseNetworkRequest, options(true ))
89161 assertNotNull(result.request)
90- assertNull(result.response)
91162 assertNull(result.request!! .headers[" if-none-match" ])
92163 assertNull(result.request!! .headers[" if-modified-since" ])
93164 }
165+
166+ // endregion
167+
168+ // region isStale
169+
170+ @Test
171+ fun `isStale returns false when within max-age` () {
172+ val response = freshCacheResponse(maxAgeSeconds = 3600 )
173+ assertFalse(ETagCacheStrategy .isStale(response))
174+ }
175+
176+ @Test
177+ fun `isStale returns true when past max-age` () {
178+ val response = staleCacheResponse(maxAgeSeconds = 3600 )
179+ assertTrue(ETagCacheStrategy .isStale(response))
180+ }
181+
182+ @Test
183+ fun `isStale returns true when no cache-control` () {
184+ val response = NetworkResponse (
185+ code = 200 ,
186+ responseMillis = System .currentTimeMillis(),
187+ headers = NetworkHeaders .EMPTY ,
188+ )
189+ assertTrue(ETagCacheStrategy .isStale(response))
190+ }
191+
192+ @Test
193+ fun `isStale returns true when responseMillis is zero` () {
194+ val response = NetworkResponse (
195+ code = 200 ,
196+ responseMillis = 0L ,
197+ headers = NetworkHeaders .Builder ()
198+ .set(" cache-control" , " public, max-age=3600" )
199+ .build(),
200+ )
201+ assertTrue(ETagCacheStrategy .isStale(response))
202+ }
203+
204+ // endregion
94205}
0 commit comments