Skip to content

Commit 32e3f1e

Browse files
PM-9081: Should cancel the job not the scope when managing autofill requests (#3389)
1 parent d7032b8 commit 32e3f1e

File tree

2 files changed

+118
-13
lines changed

2 files changed

+118
-13
lines changed

app/src/main/java/com/x8bit/bitwarden/data/autofill/processor/AutofillProcessorImpl.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
1818
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
1919
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
2020
import kotlinx.coroutines.CoroutineScope
21-
import kotlinx.coroutines.cancel
21+
import kotlinx.coroutines.Job
2222
import kotlinx.coroutines.launch
2323

2424
/**
@@ -41,14 +41,19 @@ class AutofillProcessorImpl(
4141
*/
4242
private val scope: CoroutineScope = CoroutineScope(dispatcherManager.unconfined)
4343

44+
/**
45+
* The job being used to process the fill request.
46+
*/
47+
private var job: Job = Job().apply { complete() }
48+
4449
override fun processFillRequest(
4550
autofillAppInfo: AutofillAppInfo,
4651
cancellationSignal: CancellationSignal,
4752
fillCallback: FillCallback,
4853
request: FillRequest,
4954
) {
5055
// Set the listener so that any long running work is cancelled when it is no longer needed.
51-
cancellationSignal.setOnCancelListener { scope.cancel() }
56+
cancellationSignal.setOnCancelListener { job.cancel() }
5257
// Process the OS data and handle invoking the callback with the result.
5358
process(
5459
autofillAppInfo = autofillAppInfo,
@@ -113,7 +118,8 @@ class AutofillProcessorImpl(
113118
)
114119
when (autofillRequest) {
115120
is AutofillRequest.Fillable -> {
116-
scope.launch {
121+
job.cancel()
122+
job = scope.launch {
117123
// Fulfill the [autofillRequest].
118124
val filledData = filledDataBuilder.build(
119125
autofillRequest = autofillRequest,

app/src/test/java/com/x8bit/bitwarden/data/autofill/processor/AutofillProcessorTest.kt

Lines changed: 109 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,40 +21,41 @@ import com.x8bit.bitwarden.data.autofill.model.FilledData
2121
import com.x8bit.bitwarden.data.autofill.parser.AutofillParser
2222
import com.x8bit.bitwarden.data.autofill.util.createAutofillSavedItemIntentSender
2323
import com.x8bit.bitwarden.data.autofill.util.toAutofillSaveItem
24+
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
2425
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
25-
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
2626
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
2727
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
2828
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
29+
import io.mockk.clearMocks
2930
import io.mockk.coEvery
3031
import io.mockk.coVerify
3132
import io.mockk.every
3233
import io.mockk.just
3334
import io.mockk.mockk
3435
import io.mockk.mockkStatic
3536
import io.mockk.runs
37+
import io.mockk.slot
3638
import io.mockk.unmockkStatic
3739
import io.mockk.verify
38-
import kotlinx.coroutines.ExperimentalCoroutinesApi
39-
import kotlinx.coroutines.test.UnconfinedTestDispatcher
40+
import kotlinx.coroutines.test.StandardTestDispatcher
4041
import kotlinx.coroutines.test.runTest
4142
import org.junit.jupiter.api.AfterEach
4243
import org.junit.jupiter.api.BeforeEach
4344
import org.junit.jupiter.api.Test
4445

45-
@OptIn(ExperimentalCoroutinesApi::class)
4646
class AutofillProcessorTest {
4747
private lateinit var autofillProcessor: AutofillProcessor
4848

49-
private val dispatcherManager: DispatcherManager = mockk()
49+
private val testDispatcher = StandardTestDispatcher()
50+
private val dispatcherManager: FakeDispatcherManager =
51+
FakeDispatcherManager(unconfined = testDispatcher)
5052
private val cancellationSignal: CancellationSignal = mockk()
5153
private val filledDataBuilder: FilledDataBuilder = mockk()
5254
private val fillResponseBuilder: FillResponseBuilder = mockk()
5355
private val parser: AutofillParser = mockk()
5456
private val policyManager: PolicyManager = mockk()
5557
private val saveInfoBuilder: SaveInfoBuilder = mockk()
5658
private val settingsRepository: SettingsRepository = mockk()
57-
private val testDispatcher = UnconfinedTestDispatcher()
5859

5960
private val appInfo: AutofillAppInfo = AutofillAppInfo(
6061
context = mockk(),
@@ -67,7 +68,6 @@ class AutofillProcessorTest {
6768
fun setup() {
6869
mockkStatic(::createAutofillSavedItemIntentSender)
6970
mockkStatic(AutofillRequest.Fillable::toAutofillSaveItem)
70-
every { dispatcherManager.unconfined } returns testDispatcher
7171

7272
autofillProcessor = AutofillProcessorImpl(
7373
dispatcherManager = dispatcherManager,
@@ -82,9 +82,6 @@ class AutofillProcessorTest {
8282

8383
@AfterEach
8484
fun teardown() {
85-
verify(exactly = 1) {
86-
dispatcherManager.unconfined
87-
}
8885
unmockkStatic(::createAutofillSavedItemIntentSender)
8986
unmockkStatic(AutofillRequest.Fillable::toAutofillSaveItem)
9087
}
@@ -179,6 +176,8 @@ class AutofillProcessorTest {
179176
request = fillRequest,
180177
)
181178

179+
testDispatcher.scheduler.runCurrent()
180+
182181
// Verify
183182
coVerify(exactly = 1) {
184183
filledDataBuilder.build(
@@ -386,6 +385,106 @@ class AutofillProcessorTest {
386385
saveCallback.onSuccess()
387386
}
388387
}
388+
389+
@Suppress("MaxLineLength")
390+
@Test
391+
fun `processFillRequest should allow additional requests to be invoked after cancellation signal is triggered`() {
392+
// Setup
393+
val autofillPartition: AutofillPartition = mockk()
394+
val autofillRequest: AutofillRequest.Fillable = mockk {
395+
every { packageName } returns PACKAGE_NAME
396+
every { partition } returns autofillPartition
397+
}
398+
val fillRequest: FillRequest = mockk()
399+
val saveInfo: SaveInfo = mockk()
400+
val filledData = FilledData(
401+
filledPartitions = listOf(mockk()),
402+
ignoreAutofillIds = emptyList(),
403+
originalPartition = mockk(),
404+
uri = null,
405+
vaultItemInlinePresentationSpec = null,
406+
isVaultLocked = false,
407+
)
408+
val fillResponse: FillResponse = mockk()
409+
val cancellationSignalListener = slot<CancellationSignal.OnCancelListener>()
410+
every {
411+
cancellationSignal.setOnCancelListener(capture(cancellationSignalListener))
412+
} just runs
413+
every {
414+
parser.parse(autofillAppInfo = appInfo, fillRequest = fillRequest)
415+
} returns autofillRequest
416+
coEvery { filledDataBuilder.build(autofillRequest = autofillRequest) } returns filledData
417+
every {
418+
saveInfoBuilder.build(
419+
autofillAppInfo = appInfo,
420+
autofillPartition = autofillPartition,
421+
fillRequest = fillRequest,
422+
packageName = PACKAGE_NAME,
423+
)
424+
} returns saveInfo
425+
every {
426+
fillResponseBuilder.build(
427+
autofillAppInfo = appInfo,
428+
filledData = filledData,
429+
saveInfo = saveInfo,
430+
)
431+
} returns fillResponse
432+
every { fillCallback.onSuccess(fillResponse) } just runs
433+
434+
// Test
435+
autofillProcessor.processFillRequest(
436+
autofillAppInfo = appInfo,
437+
cancellationSignal = cancellationSignal,
438+
fillCallback = fillCallback,
439+
request = fillRequest,
440+
)
441+
442+
// Cancel the job and validate that nothing runs
443+
cancellationSignalListener.captured.onCancel()
444+
testDispatcher.scheduler.runCurrent()
445+
verify(exactly = 1) {
446+
// These run as they are not part of the coroutine
447+
cancellationSignal.setOnCancelListener(any())
448+
parser.parse(autofillAppInfo = appInfo, fillRequest = fillRequest)
449+
}
450+
coVerify(exactly = 0) {
451+
filledDataBuilder.build(autofillRequest = autofillRequest)
452+
}
453+
verify(exactly = 0) {
454+
fillResponseBuilder.build(
455+
autofillAppInfo = appInfo,
456+
filledData = filledData,
457+
saveInfo = saveInfo,
458+
)
459+
fillCallback.onSuccess(fillResponse)
460+
}
461+
clearMocks(cancellationSignal, parser, answers = false)
462+
463+
// Test again after cancelling
464+
autofillProcessor.processFillRequest(
465+
autofillAppInfo = appInfo,
466+
cancellationSignal = cancellationSignal,
467+
fillCallback = fillCallback,
468+
request = fillRequest,
469+
)
470+
471+
testDispatcher.scheduler.runCurrent()
472+
473+
// Verify
474+
verify(exactly = 1) {
475+
cancellationSignal.setOnCancelListener(any())
476+
parser.parse(autofillAppInfo = appInfo, fillRequest = fillRequest)
477+
fillResponseBuilder.build(
478+
autofillAppInfo = appInfo,
479+
filledData = filledData,
480+
saveInfo = saveInfo,
481+
)
482+
fillCallback.onSuccess(fillResponse)
483+
}
484+
coVerify(exactly = 1) {
485+
filledDataBuilder.build(autofillRequest = autofillRequest)
486+
}
487+
}
389488
}
390489

391490
private const val PACKAGE_NAME: String = "com.google"

0 commit comments

Comments
 (0)