Skip to content

Commit 0269212

Browse files
AlexanderGHclaude
andcommitted
Cover the contracts the single-chain watcher relies on
Three properties the previous commit depends on had no test of their own. Each was verified to fail against a deliberate break of what it covers. - initialFetchDoesNotTriggerTheWatcher: the initial fetch writes to the cache and publishes, and the watcher subscribes only afterwards, so it must not react to its own write. Fails when the subscription is moved ahead of the fetch. - refetchUsesTheRefetchPolicyRatherThanTheFetchPolicy: the initial responses use the fetch policy and the refetches use the refetch policy, including when they disagree. Pins the refetch request built by the interceptor, which has to clear the noCache set by a NetworkOnly fetch policy. Fails when refetches reuse the original request. - watchExecutesTheInterceptorChainOnce: counts executions of an interceptor installed ahead of the cache. Fails with expected:<1> but was:<2> against the previous two-execution implementation, which is the regression it exists to catch. The synchronisation point of apollographql/apollo-kotlin#3853 and the absence of an initial request in watch(data) were already covered, by storeWriteTriggersWatcher and cacheOnlyFetchPolicy respectively. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 698afb0 commit 0269212

1 file changed

Lines changed: 108 additions & 0 deletions

File tree

tests/normalized-cache/src/commonTest/kotlin/WatcherTest.kt

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ package test
22

33
import app.cash.turbine.test
44
import com.apollographql.apollo.ApolloClient
5+
import com.apollographql.apollo.api.ApolloRequest
56
import com.apollographql.apollo.api.ApolloResponse
7+
import com.apollographql.apollo.api.Operation
68
import com.apollographql.apollo.api.composeJsonResponse
79
import com.apollographql.apollo.exception.ApolloNetworkException
810
import com.apollographql.apollo.exception.CacheMissException
11+
import com.apollographql.apollo.interceptor.ApolloInterceptor
12+
import com.apollographql.apollo.interceptor.ApolloInterceptorChain
913
import com.apollographql.apollo.testing.QueueTestNetworkTransport
1014
import com.apollographql.apollo.testing.enqueueTestNetworkError
1115
import com.apollographql.apollo.testing.enqueueTestResponse
@@ -33,6 +37,7 @@ import kotlinx.coroutines.TimeoutCancellationException
3337
import kotlinx.coroutines.cancelAndJoin
3438
import kotlinx.coroutines.channels.Channel
3539
import kotlinx.coroutines.delay
40+
import kotlinx.coroutines.flow.Flow
3641
import kotlinx.coroutines.flow.first
3742
import kotlinx.coroutines.launch
3843
import kotlinx.coroutines.withContext
@@ -373,6 +378,109 @@ class WatcherTest {
373378
job.cancel()
374379
}
375380

381+
/**
382+
* The initial fetch writes its response to the cache and publishes the changed keys. The watcher
383+
* subscribes only once those responses are in, so it must not react to its own write.
384+
*/
385+
@Test
386+
fun initialFetchDoesNotTriggerTheWatcher() = runTest(before = { setUp() }) {
387+
val query = EpisodeHeroNameWithIdQuery(Episode.EMPIRE)
388+
val channel = Channel<EpisodeHeroNameWithIdQuery.Data?>()
389+
390+
// The cache starts empty, so the initial fetch does change it
391+
apolloClient.enqueueTestResponse(query, episodeHeroNameWithIdData)
392+
val job = launch {
393+
apolloClient.query(query).watch().collect {
394+
channel.send(it.data)
395+
}
396+
}
397+
398+
// Cache miss is emitted first (null data)
399+
assertNull(channel.awaitElement())
400+
assertEquals(channel.awaitElement()?.hero?.name, "R2-D2")
401+
402+
channel.assertEmpty()
403+
404+
job.cancel()
405+
}
406+
407+
/**
408+
* The initial responses use the fetch policy while the refetches use the refetch policy, including
409+
* when the two disagree: a NetworkOnly watch still refetches from the cache under the default
410+
* CacheOnly refetch policy rather than going back to the network.
411+
*/
412+
@Test
413+
fun refetchUsesTheRefetchPolicyRatherThanTheFetchPolicy() = runTest(before = { setUp() }) {
414+
val query = EpisodeHeroNameWithIdQuery(Episode.EMPIRE)
415+
val channel = Channel<EpisodeHeroNameWithIdQuery.Data?>()
416+
417+
apolloClient.enqueueTestResponse(query, episodeHeroNameWithIdData)
418+
val job = launch {
419+
apolloClient.query(query)
420+
.fetchPolicy(FetchPolicy.NetworkOnly)
421+
.watch().collect {
422+
channel.send(it.data)
423+
}
424+
}
425+
426+
assertEquals(channel.awaitElement()?.hero?.name, "R2-D2")
427+
428+
// Write "Artoo" out of band. Only one response was enqueued, so a refetch that went to the
429+
// network would fail instead of reading the cache.
430+
cacheManager.writeOperation(
431+
query,
432+
EpisodeHeroNameWithIdQuery.Data(EpisodeHeroNameWithIdQuery.Hero("Droid", "2001", "Artoo")),
433+
publish = true,
434+
)
435+
436+
assertEquals(channel.awaitElement()?.hero?.name, "Artoo")
437+
438+
job.cancel()
439+
}
440+
441+
/**
442+
* The initial responses and the cache subscription come from a single execution of the interceptor
443+
* chain, so interceptors sitting ahead of the cache run once rather than once per execution.
444+
*/
445+
@Test
446+
fun watchExecutesTheInterceptorChainOnce() = runTest {
447+
var executions = 0
448+
val countingInterceptor = object : ApolloInterceptor {
449+
override fun <D : Operation.Data> intercept(
450+
request: ApolloRequest<D>,
451+
chain: ApolloInterceptorChain,
452+
): Flow<ApolloResponse<D>> {
453+
executions++
454+
return chain.proceed(request)
455+
}
456+
}
457+
val cacheManager =
458+
CacheManager(MemoryCacheFactory(), cacheKeyGenerator = IdCacheKeyGenerator(), cacheResolver = IdCacheResolver())
459+
val apolloClient = ApolloClient.Builder()
460+
.networkTransport(QueueTestNetworkTransport())
461+
.cacheManager(cacheManager)
462+
.addInterceptor(countingInterceptor)
463+
.build()
464+
465+
val query = EpisodeHeroNameWithIdQuery(Episode.EMPIRE)
466+
val channel = Channel<EpisodeHeroNameWithIdQuery.Data?>()
467+
apolloClient.enqueueTestResponse(query, episodeHeroNameWithIdData)
468+
val job = launch {
469+
apolloClient.query(query).watch().collect {
470+
channel.send(it.data)
471+
}
472+
}
473+
474+
// Cache miss is emitted first (null data)
475+
assertNull(channel.awaitElement())
476+
assertEquals(channel.awaitElement()?.hero?.name, "R2-D2")
477+
478+
assertEquals(1, executions)
479+
480+
job.cancel()
481+
apolloClient.close()
482+
}
483+
376484
/**
377485
* A test to test refetching with a NetworkOnly refetchPolicy. On every change, the watcher should get new information
378486
* from the network

0 commit comments

Comments
 (0)