android/app/src/androidTest/java/com/nextcloud/client/jobs/BackgroundJobManagerTest.kt

419 lines
14 KiB
Kotlin

/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.client.jobs
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.test.annotation.UiThreadTest
import androidx.work.Data
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.nextcloud.client.account.User
import com.nextcloud.client.core.Clock
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Suite
import org.mockito.ArgumentMatcher
import org.mockito.kotlin.KArgumentCaptor
import org.mockito.kotlin.any
import org.mockito.kotlin.argThat
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import java.util.Date
import java.util.UUID
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
/**
* When using IDE to run enire Suite, make sure tests are run using Android Instrumentation Test
* runner. By default IDE runs normal JUnit - this is AS problem. One must configure the
* test run manually.
*/
@RunWith(Suite::class)
@Suite.SuiteClasses(
BackgroundJobManagerTest.Manager::class,
BackgroundJobManagerTest.ContentObserver::class,
BackgroundJobManagerTest.PeriodicContactsBackup::class,
BackgroundJobManagerTest.ImmediateContactsBackup::class,
BackgroundJobManagerTest.ImmediateContactsImport::class,
BackgroundJobManagerTest.Tags::class
)
class BackgroundJobManagerTest {
/**
* Used to help with ambiguous type inference
*/
class IsOneTimeWorkRequest : ArgumentMatcher<OneTimeWorkRequest> {
override fun matches(argument: OneTimeWorkRequest?): Boolean = true
}
/**
* Used to help with ambiguous type inference
*/
class IsPeriodicWorkRequest : ArgumentMatcher<PeriodicWorkRequest> {
override fun matches(argument: PeriodicWorkRequest?): Boolean = true
}
abstract class Fixture {
companion object {
internal const val USER_ACCOUNT_NAME = "user@nextcloud"
internal val TIMESTAMP = System.currentTimeMillis()
}
internal lateinit var user: User
internal lateinit var workManager: WorkManager
internal lateinit var clock: Clock
internal lateinit var backgroundJobManager: BackgroundJobManagerImpl
@Before
fun setUpFixture() {
user = mock()
whenever(user.accountName).thenReturn(USER_ACCOUNT_NAME)
workManager = mock()
clock = mock()
whenever(clock.currentTime).thenReturn(TIMESTAMP)
whenever(clock.currentDate).thenReturn(Date(TIMESTAMP))
backgroundJobManager = BackgroundJobManagerImpl(workManager, clock, mock())
}
fun assertHasRequiredTags(tags: Set<String>, jobName: String, user: User? = null) {
assertTrue("""'all' tag is mandatory""", tags.contains("*"))
assertTrue("name tag is mandatory", tags.contains(BackgroundJobManagerImpl.formatNameTag(jobName, user)))
assertTrue("timestamp tag is mandatory", tags.contains(BackgroundJobManagerImpl.formatTimeTag(TIMESTAMP)))
if (user != null) {
assertTrue("user tag is mandatory", tags.contains(BackgroundJobManagerImpl.formatUserTag(user)))
}
}
fun buildWorkInfo(index: Long): WorkInfo = WorkInfo(
id = UUID.randomUUID(),
state = WorkInfo.State.RUNNING,
outputData = Data.Builder().build(),
tags = setOf(BackgroundJobManagerImpl.formatTimeTag(1581820284000)),
progress = Data.Builder().build(),
runAttemptCount = 1,
generation = 0
)
}
class Manager : Fixture() {
class SyncObserver<T> : Observer<T> {
val latch = CountDownLatch(1)
var value: T? = null
override fun onChanged(t: T) {
value = t
latch.countDown()
}
fun getValue(timeout: Long = 3, timeUnit: TimeUnit = TimeUnit.SECONDS): T? {
val result = latch.await(timeout, timeUnit)
if (!result) {
throw TimeoutException()
}
return value
}
}
@Test
@UiThreadTest
fun get_all_job_info() {
// GIVEN
// work manager has 2 registered workers
val platformWorkInfo = listOf(
buildWorkInfo(0),
buildWorkInfo(1),
buildWorkInfo(2)
)
val lv = MutableLiveData<List<WorkInfo>>()
lv.value = platformWorkInfo
whenever(workManager.getWorkInfosByTagLiveData(eq("*"))).thenReturn(lv)
// WHEN
// job info for all jobs is requested
val jobs = backgroundJobManager.jobs
// THEN
// live data with job info is returned
// live data contains 2 job info instances
// job info is sorted by timestamp from newest to oldest
assertNotNull(jobs)
val observer = SyncObserver<List<JobInfo>>()
jobs.observeForever(observer)
val jobInfo = observer.getValue()
assertNotNull(jobInfo)
assertEquals(platformWorkInfo.size, jobInfo?.size)
jobInfo?.let {
assertEquals(platformWorkInfo[2].id, it[0].id)
assertEquals(platformWorkInfo[1].id, it[1].id)
assertEquals(platformWorkInfo[0].id, it[2].id)
}
}
@Test
fun cancel_all_jobs() {
// WHEN
// all jobs are cancelled
backgroundJobManager.cancelAllJobs()
// THEN
// all jobs with * tag are cancelled
verify(workManager).cancelAllWorkByTag(BackgroundJobManagerImpl.TAG_ALL)
}
}
class ContentObserver : Fixture() {
private lateinit var request: OneTimeWorkRequest
@Before
fun setUp() {
val requestCaptor: KArgumentCaptor<OneTimeWorkRequest> = argumentCaptor()
backgroundJobManager.scheduleContentObserverJob()
verify(workManager).enqueueUniqueWork(
any(),
any(),
requestCaptor.capture()
)
assertEquals(1, requestCaptor.allValues.size)
request = requestCaptor.firstValue
}
@Test
fun job_is_unique_and_replaces_previous_job() {
verify(workManager).enqueueUniqueWork(
eq(BackgroundJobManagerImpl.JOB_CONTENT_OBSERVER),
eq(ExistingWorkPolicy.APPEND),
argThat(IsOneTimeWorkRequest())
)
}
@Test
fun job_request_has_mandatory_tags() {
assertHasRequiredTags(request.tags, BackgroundJobManagerImpl.JOB_CONTENT_OBSERVER)
}
}
class PeriodicContactsBackup : Fixture() {
private lateinit var request: PeriodicWorkRequest
@Before
fun setUp() {
val requestCaptor: KArgumentCaptor<PeriodicWorkRequest> = argumentCaptor()
backgroundJobManager.schedulePeriodicContactsBackup(user)
verify(workManager).enqueueUniquePeriodicWork(
any(),
any(),
requestCaptor.capture()
)
assertEquals(1, requestCaptor.allValues.size)
request = requestCaptor.firstValue
}
@Test
fun job_is_unique_for_user() {
verify(workManager).enqueueUniquePeriodicWork(
eq(BackgroundJobManagerImpl.JOB_PERIODIC_CONTACTS_BACKUP),
eq(ExistingPeriodicWorkPolicy.KEEP),
argThat(IsPeriodicWorkRequest())
)
}
@Test
fun job_request_has_mandatory_tags() {
assertHasRequiredTags(request.tags, BackgroundJobManagerImpl.JOB_PERIODIC_CONTACTS_BACKUP, user)
}
}
class ImmediateContactsBackup : Fixture() {
private lateinit var workInfo: MutableLiveData<WorkInfo>
private lateinit var jobInfo: LiveData<JobInfo?>
private lateinit var request: OneTimeWorkRequest
@Before
fun setUp() {
val requestCaptor: KArgumentCaptor<OneTimeWorkRequest> = argumentCaptor()
workInfo = MutableLiveData()
whenever(workManager.getWorkInfoByIdLiveData(any())).thenReturn(workInfo)
jobInfo = backgroundJobManager.startImmediateContactsBackup(user)
verify(workManager).enqueueUniqueWork(
any(),
any(),
requestCaptor.capture()
)
assertEquals(1, requestCaptor.allValues.size)
request = requestCaptor.firstValue
}
@Test
fun job_is_unique_for_user() {
verify(workManager).enqueueUniqueWork(
eq(BackgroundJobManagerImpl.JOB_IMMEDIATE_CONTACTS_BACKUP),
eq(ExistingWorkPolicy.KEEP),
argThat(IsOneTimeWorkRequest())
)
}
@Test
fun job_request_has_mandatory_tags() {
assertHasRequiredTags(request.tags, BackgroundJobManagerImpl.JOB_IMMEDIATE_CONTACTS_BACKUP, user)
}
@Test
@UiThreadTest
fun job_info_is_obtained_from_work_info() {
// GIVEN
// work info is available
workInfo.value = buildWorkInfo(0)
// WHEN
// job info has listener
jobInfo.observeForever {}
// THEN
// converted value is available
assertNotNull(jobInfo.value)
assertEquals(workInfo.value?.id, jobInfo.value?.id)
}
}
class ImmediateContactsImport : Fixture() {
private lateinit var workInfo: MutableLiveData<WorkInfo>
private lateinit var jobInfo: LiveData<JobInfo?>
private lateinit var request: OneTimeWorkRequest
@Before
fun setUp() {
val requestCaptor: KArgumentCaptor<OneTimeWorkRequest> = argumentCaptor()
workInfo = MutableLiveData()
whenever(workManager.getWorkInfoByIdLiveData(any())).thenReturn(workInfo)
jobInfo = backgroundJobManager.startImmediateContactsImport(
contactsAccountName = "name",
contactsAccountType = "type",
vCardFilePath = "/path/to/vcard/file",
selectedContacts = intArrayOf(1, 2, 3)
)
verify(workManager).enqueueUniqueWork(
any(),
any(),
requestCaptor.capture()
)
assertEquals(1, requestCaptor.allValues.size)
request = requestCaptor.firstValue
}
@Test
fun job_is_unique() {
verify(workManager).enqueueUniqueWork(
eq(BackgroundJobManagerImpl.JOB_IMMEDIATE_CONTACTS_IMPORT),
eq(ExistingWorkPolicy.KEEP),
argThat(IsOneTimeWorkRequest())
)
}
@Test
fun job_request_has_mandatory_tags() {
assertHasRequiredTags(request.tags, BackgroundJobManagerImpl.JOB_IMMEDIATE_CONTACTS_IMPORT)
}
@Test
@UiThreadTest
fun job_info_is_obtained_from_work_info() {
// GIVEN
// work info is available
workInfo.value = buildWorkInfo(0)
// WHEN
// job info has listener
jobInfo.observeForever {}
// THEN
// converted value is available
assertNotNull(jobInfo.value)
assertEquals(workInfo.value?.id, jobInfo.value?.id)
}
}
class Tags {
@Test
fun split_tag_key_and_value() {
// GIVEN
// valid tag
// tag has colons in value part
val tag = "${BackgroundJobManagerImpl.TAG_PREFIX_NAME}:value:with:colons and spaces"
// WHEN
// tag is parsed
val parsedTag = BackgroundJobManagerImpl.parseTag(tag)
// THEN
// key-value pair is returned
// key is first
// value with colons is second
assertNotNull(parsedTag)
assertEquals(BackgroundJobManagerImpl.TAG_PREFIX_NAME, parsedTag?.first)
assertEquals("value:with:colons and spaces", parsedTag?.second)
}
@Test
fun tags_with_invalid_prefixes_are_rejected() {
// GIVEN
// tag prefix is not on allowed prefixes list
val tag = "invalidprefix:value"
BackgroundJobManagerImpl.PREFIXES.forEach {
assertFalse(tag.startsWith(it))
}
// WHEN
// tag is parsed
val parsedTag = BackgroundJobManagerImpl.parseTag(tag)
// THEN
// tag is rejected
assertNull(parsedTag)
}
@Test
fun strings_without_colon_are_rejected() {
// GIVEN
// strings that are not tags
val tags = listOf(
BackgroundJobManagerImpl.TAG_ALL,
BackgroundJobManagerImpl.TAG_PREFIX_NAME,
"simplestring",
""
)
tags.forEach {
// WHEN
// string is parsed
val parsedTag = BackgroundJobManagerImpl.parseTag(it)
// THEN
// tag is rejected
assertNull(parsedTag)
}
}
}
}