Unit tests for CheckStatusManager

This commit is contained in:
Aidan Follestad 2018-12-01 19:58:09 -08:00
commit 14a86568e6
7 changed files with 474 additions and 46 deletions

View file

@ -5,17 +5,16 @@
*/
package com.afollestad.nocknock.engine.statuscheck
import android.app.Application
import android.app.job.JobInfo.NETWORK_TYPE_ANY
import android.app.job.JobScheduler
import android.app.job.JobScheduler.RESULT_SUCCESS
import android.os.PersistableBundle
import com.afollestad.nocknock.data.ServerModel
import com.afollestad.nocknock.data.ServerStatus.ERROR
import com.afollestad.nocknock.data.ServerStatus.OK
import com.afollestad.nocknock.engine.R
import com.afollestad.nocknock.engine.db.ServerModelStore
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_SITE_ID
import com.afollestad.nocknock.utilities.providers.BundleProvider
import com.afollestad.nocknock.utilities.providers.JobInfoProvider
import com.afollestad.nocknock.utilities.providers.StringProvider
import okhttp3.OkHttpClient
import okhttp3.Request
@ -38,7 +37,7 @@ interface CheckStatusManager {
fun scheduleCheck(
site: ServerModel,
rightNow: Boolean = false,
cancelPrevious: Boolean = false,
cancelPrevious: Boolean = rightNow,
fromFinishingJob: Boolean = false
)
@ -48,10 +47,11 @@ interface CheckStatusManager {
}
class RealCheckStatusManager @Inject constructor(
private val app: Application,
private val jobScheduler: JobScheduler,
private val okHttpClient: OkHttpClient,
private val stringProvider: StringProvider,
private val bundleProvider: BundleProvider,
private val jobInfoProvider: JobInfoProvider,
private val siteStore: ServerModelStore
) : CheckStatusManager {
@ -90,25 +90,21 @@ class RealCheckStatusManager @Inject constructor(
}
log("Requesting a check job for site to be scheduled: $site")
val extras = PersistableBundle().apply {
val extras = bundleProvider.createPersistable {
putInt(KEY_SITE_ID, site.id)
}
val jobInfo = jobInfoProvider.createCheckJob(
id = site.id,
onlyUnmeteredNetwork = false,
delayMs = if (rightNow) {
1
} else {
site.checkInterval
},
extras = extras,
target = CheckStatusJob::class.java
)
// Note: we don't use the periodic feature of JobScheduler because it requires a
// minimum of 15 minutes between each execution which may not be what's requested by the
// user of the app.
val jobInfo = jobInfo(app, site.id, CheckStatusJob::class.java) {
setRequiredNetworkType(NETWORK_TYPE_ANY)
if (rightNow) {
log(">> Job for site ${site.id} should be executed now")
setMinimumLatency(1)
} else {
log(">> Job for site ${site.id} should be in ${site.checkInterval}ms")
setMinimumLatency(site.checkInterval)
}
setExtras(extras)
setPersisted(true)
}
val dispatchResult = jobScheduler.schedule(jobInfo)
if (dispatchResult != RESULT_SUCCESS) {
log("Failed to schedule a check job for site: ${site.id}")

View file

@ -1,25 +0,0 @@
/*
* Licensed under Apache-2.0
*
* Designed and developed by Aidan Follestad (@afollestad)
*/
package com.afollestad.nocknock.engine.statuscheck
import android.app.job.JobInfo
import android.app.job.JobService
import android.content.ComponentName
import android.content.Context
typealias JobInfoBuilder = JobInfo.Builder
fun jobInfo(
context: Context,
id: Int,
target: Class<out JobService>,
exec: JobInfoBuilder.() -> JobInfoBuilder
): JobInfo {
val component = ComponentName(context, target)
val builder = JobInfo.Builder(id, component)
exec(builder)
return builder.build()
}

View file

@ -0,0 +1,291 @@
/*
* Licensed under Apache-2.0
*
* Designed and developed by Aidan Follestad (@afollestad)
*/
package com.afollestad.nocknock.engine
import android.app.job.JobInfo
import android.app.job.JobScheduler
import com.afollestad.nocknock.data.ServerModel
import com.afollestad.nocknock.data.ServerStatus.ERROR
import com.afollestad.nocknock.data.ServerStatus.OK
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
import com.afollestad.nocknock.engine.db.ServerModelStore
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_SITE_ID
import com.afollestad.nocknock.engine.statuscheck.RealCheckStatusManager
import com.afollestad.nocknock.utilities.providers.StringProvider
import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.argumentCaptor
import com.nhaarman.mockitokotlin2.doAnswer
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.never
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
import com.nhaarman.mockitokotlin2.whenever
import kotlinx.coroutines.runBlocking
import okhttp3.Call
import okhttp3.OkHttpClient
import okhttp3.Protocol.HTTP_2
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody
import org.junit.Test
import java.net.SocketTimeoutException
class CheckStatusManagerTest {
private val timeoutError = "Oh no, a timeout"
private val jobScheduler = mock<JobScheduler>()
private val okHttpClient = mock<OkHttpClient>()
private val stringProvider = mock<StringProvider> {
on { get(R.string.timeout) } doReturn timeoutError
}
private val bundleProvider = testBundleProvider()
private val jobInfoProvider = testJobInfoProvider()
private val store = mock<ServerModelStore>()
private val manager = RealCheckStatusManager(
jobScheduler,
okHttpClient,
stringProvider,
bundleProvider,
jobInfoProvider,
store
)
@Test fun ensureScheduledChecks_noEnabledSites() = runBlocking {
val model1 = fakeModel().copy(disabled = true)
whenever(store.get()).doReturn(listOf(model1))
manager.ensureScheduledChecks()
verifyNoMoreInteractions(jobScheduler)
}
@Test fun ensureScheduledChecks_sitesAlreadyHaveJobs() = runBlocking<Unit> {
val model1 = fakeModel()
val job1 = fakeJob(model1.id)
whenever(store.get()).doReturn(listOf(model1))
whenever(jobScheduler.allPendingJobs).doReturn(listOf(job1))
manager.ensureScheduledChecks()
verify(jobScheduler, never()).schedule(any())
}
@Test fun ensureScheduledChecks() = runBlocking {
val model1 = fakeModel()
whenever(store.get()).doReturn(listOf(model1))
whenever(jobScheduler.allPendingJobs).doReturn(listOf<JobInfo>())
manager.ensureScheduledChecks()
val jobCaptor = argumentCaptor<JobInfo>()
verify(jobScheduler).schedule(jobCaptor.capture())
val jobInfo = jobCaptor.allValues.single()
assertThat(jobInfo.id).isEqualTo(model1.id)
assertThat(jobInfo.extras.getInt(KEY_SITE_ID)).isEqualTo(model1.id)
}
@Test fun scheduleCheck_rightNow() {
val model1 = fakeModel()
whenever(jobScheduler.allPendingJobs).doReturn(listOf<JobInfo>())
manager.scheduleCheck(
site = model1,
rightNow = true
)
val jobCaptor = argumentCaptor<JobInfo>()
verify(jobScheduler).schedule(jobCaptor.capture())
verify(jobScheduler).cancel(model1.id)
val jobInfo = jobCaptor.allValues.single()
assertThat(jobInfo.id).isEqualTo(model1.id)
assertThat(jobInfo.extras.getInt(KEY_SITE_ID)).isEqualTo(model1.id)
}
@Test(expected = IllegalStateException::class)
fun scheduleCheck_notFromFinishingJob_haveExistingJob() {
val model1 = fakeModel()
val job1 = fakeJob(model1.id)
whenever(jobScheduler.allPendingJobs).doReturn(listOf(job1))
manager.scheduleCheck(
site = model1,
fromFinishingJob = false
)
}
@Test fun scheduleCheck_fromFinishingJob_haveExistingJob() {
val model1 = fakeModel()
val job1 = fakeJob(model1.id)
whenever(jobScheduler.allPendingJobs).doReturn(listOf(job1))
manager.scheduleCheck(
site = model1,
fromFinishingJob = true
)
val jobCaptor = argumentCaptor<JobInfo>()
verify(jobScheduler).schedule(jobCaptor.capture())
verify(jobScheduler, never()).cancel(model1.id)
val jobInfo = jobCaptor.allValues.single()
assertThat(jobInfo.id).isEqualTo(model1.id)
assertThat(jobInfo.extras.getInt(KEY_SITE_ID)).isEqualTo(model1.id)
}
@Test fun scheduleCheck() {
val model1 = fakeModel()
whenever(jobScheduler.allPendingJobs).doReturn(listOf<JobInfo>())
manager.scheduleCheck(
site = model1,
fromFinishingJob = true
)
val jobCaptor = argumentCaptor<JobInfo>()
verify(jobScheduler).schedule(jobCaptor.capture())
verify(jobScheduler, never()).cancel(model1.id)
val jobInfo = jobCaptor.allValues.single()
assertThat(jobInfo.id).isEqualTo(model1.id)
assertThat(jobInfo.extras.getInt(KEY_SITE_ID)).isEqualTo(model1.id)
}
@Test fun cancelCheck() {
val model1 = fakeModel()
manager.cancelCheck(model1)
verify(jobScheduler).cancel(model1.id)
}
@Test fun performCheck_httpNotSuccess() = runBlocking {
val response = fakeResponse(500, "Internal Server Error", "Hello World")
val call = mock<Call> {
on { execute() } doReturn response
}
whenever(okHttpClient.newCall(any())).doReturn(call)
val model1 = fakeModel()
val result = manager.performCheck(model1)
assertThat(result.model).isEqualTo(
model1.copy(
status = ERROR,
reason = "Response 500 - Hello World"
)
)
}
@Test fun performCheck_socketTimeout() = runBlocking {
val error = SocketTimeoutException("Oh no!")
val call = mock<Call> {
on { execute() } doAnswer { throw error }
}
whenever(okHttpClient.newCall(any())).doReturn(call)
val model1 = fakeModel()
val result = manager.performCheck(model1)
assertThat(result.model).isEqualTo(
model1.copy(
status = ERROR,
reason = timeoutError
)
)
}
@Test fun performCheck_exception() = runBlocking {
val error = Exception("Oh no!")
val call = mock<Call> {
on { execute() } doAnswer { throw error }
}
whenever(okHttpClient.newCall(any())).doReturn(call)
val model1 = fakeModel()
val result = manager.performCheck(model1)
assertThat(result.model).isEqualTo(
model1.copy(
status = ERROR,
reason = "Oh no!"
)
)
}
@Test fun performCheck_success() = runBlocking {
val response = fakeResponse(200, "OK", "Hello World")
val call = mock<Call> {
on { execute() } doReturn response
}
whenever(okHttpClient.newCall(any())).doReturn(call)
val model1 = fakeModel()
val result = manager.performCheck(model1)
assertThat(result.model).isEqualTo(
model1.copy(
status = OK,
reason = null
)
)
}
@Test fun performCheck_401_butStillSuccess() = runBlocking {
val response = fakeResponse(401, "Unauthorized", "Hello World")
val call = mock<Call> {
on { execute() } doReturn response
}
whenever(okHttpClient.newCall(any())).doReturn(call)
val model1 = fakeModel()
val result = manager.performCheck(model1)
assertThat(result.model).isEqualTo(
model1.copy(
status = OK,
reason = null
)
)
}
private fun fakeResponse(
code: Int,
message: String,
body: String?
): Response {
val responseBody = if (body != null) {
ResponseBody.create(null, body)
} else {
null
}
val request = Request.Builder()
.url("https://placeholder.com")
.build()
return Response.Builder()
.protocol(HTTP_2)
.request(request)
.message(message)
.code(code)
.body(responseBody)
.build()
}
private fun fakeModel() = ServerModel(
id = 1,
name = "Wakanda Forever",
url = "https://www.wakanda.gov",
validationMode = STATUS_CODE
)
private fun fakeJob(id: Int): JobInfo {
return mock {
on { this.id } doReturn id
}
}
}

View file

@ -0,0 +1,58 @@
/*
* Licensed under Apache-2.0
*
* Designed and developed by Aidan Follestad (@afollestad)
*/
package com.afollestad.nocknock.engine
import android.app.job.JobInfo
import android.content.ComponentName
import android.os.PersistableBundle
import com.afollestad.nocknock.utilities.providers.BundleProvider
import com.afollestad.nocknock.utilities.providers.IBundle
import com.afollestad.nocknock.utilities.providers.IBundler
import com.afollestad.nocknock.utilities.providers.JobInfoProvider
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.doAnswer
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.whenever
fun testBundleProvider(): BundleProvider {
val provider = mock<BundleProvider>()
whenever(provider.createPersistable(any())).doAnswer {
val realBundle = mock<PersistableBundle>()
val creator = it.getArgument<IBundler>(0)
creator(object : IBundle {
override fun putInt(
key: String,
value: Int
) {
whenever(realBundle.getInt(key)).doReturn(value)
}
})
return@doAnswer realBundle
}
return provider
}
fun testJobInfoProvider(): JobInfoProvider {
val provider = mock<JobInfoProvider>()
whenever(provider.createCheckJob(any(), any(), any(), any(), any())).doAnswer { inv ->
val jobInfo = mock<JobInfo>()
val id = inv.getArgument<Int>(0)
val delay = inv.getArgument<Long>(2)
val extras = inv.getArgument<PersistableBundle>(3)
val target = inv.getArgument<Class<*>>(4)
val component = mock<ComponentName>()
whenever(component.className).doReturn(target.name)
whenever(jobInfo.id).doReturn(id)
whenever(jobInfo.minLatencyMillis).doReturn(delay)
whenever(jobInfo.extras).doReturn(extras)
whenever(jobInfo.service).doReturn(component)
return@doAnswer jobInfo
}
return provider
}

View file

@ -6,11 +6,15 @@
package com.afollestad.nocknock.utilities
import com.afollestad.nocknock.utilities.providers.BitmapProvider
import com.afollestad.nocknock.utilities.providers.BundleProvider
import com.afollestad.nocknock.utilities.providers.IntentProvider
import com.afollestad.nocknock.utilities.providers.JobInfoProvider
import com.afollestad.nocknock.utilities.providers.NotificationChannelProvider
import com.afollestad.nocknock.utilities.providers.NotificationProvider
import com.afollestad.nocknock.utilities.providers.RealBitmapProvider
import com.afollestad.nocknock.utilities.providers.RealBundleProvider
import com.afollestad.nocknock.utilities.providers.RealIntentProvider
import com.afollestad.nocknock.utilities.providers.RealJobInfoProvider
import com.afollestad.nocknock.utilities.providers.RealNotificationChannelProvider
import com.afollestad.nocknock.utilities.providers.RealNotificationProvider
import com.afollestad.nocknock.utilities.providers.RealSdkProvider
@ -60,4 +64,16 @@ abstract class UtilitiesModule {
abstract fun provideNotificationProvider(
notificationProvider: RealNotificationProvider
): NotificationProvider
@Binds
@Singleton
abstract fun provideBundleProvider(
bundleProvider: RealBundleProvider
): BundleProvider
@Binds
@Singleton
abstract fun provideJobInfoProvider(
jobInfoProvider: RealJobInfoProvider
): JobInfoProvider
}

View file

@ -0,0 +1,39 @@
/*
* Licensed under Apache-2.0
*
* Designed and developed by Aidan Follestad (@afollestad)
*/
package com.afollestad.nocknock.utilities.providers
import android.os.PersistableBundle
import javax.inject.Inject
interface IBundle {
fun putInt(
key: String,
value: Int
)
}
typealias IBundler = IBundle.() -> Unit
/** @author Aidan Follestad (@afollestad) */
interface BundleProvider {
fun createPersistable(builder: IBundle.() -> Unit): PersistableBundle
}
/** @author Aidan Follestad (@afollestad) */
class RealBundleProvider @Inject constructor() : BundleProvider {
override fun createPersistable(bundler: IBundler): PersistableBundle {
val realBundle = PersistableBundle()
bundler(object : IBundle {
override fun putInt(
key: String,
value: Int
) = realBundle.putInt(key, value)
})
return realBundle
}
}

View file

@ -0,0 +1,53 @@
/*
* Licensed under Apache-2.0
*
* Designed and developed by Aidan Follestad (@afollestad)
*/
package com.afollestad.nocknock.utilities.providers
import android.app.Application
import android.app.job.JobInfo
import android.app.job.JobInfo.NETWORK_TYPE_ANY
import android.app.job.JobInfo.NETWORK_TYPE_UNMETERED
import android.content.ComponentName
import android.os.PersistableBundle
import javax.inject.Inject
interface JobInfoProvider {
fun createCheckJob(
id: Int,
onlyUnmeteredNetwork: Boolean,
delayMs: Long,
extras: PersistableBundle,
target: Class<*>
): JobInfo
}
class RealJobInfoProvider @Inject constructor(
private val app: Application
) : JobInfoProvider {
// Note: we don't use the periodic feature of JobScheduler because it requires a
// minimum of 15 minutes between each execution which may not be what's requested by the
// user of the app.
override fun createCheckJob(
id: Int,
onlyUnmeteredNetwork: Boolean,
delayMs: Long,
extras: PersistableBundle,
target: Class<*>
): JobInfo {
val component = ComponentName(app, target)
val networkType = if (onlyUnmeteredNetwork) {
NETWORK_TYPE_UNMETERED
} else {
NETWORK_TYPE_ANY
}
return JobInfo.Builder(id, component)
.setRequiredNetworkType(networkType)
.setMinimumLatency(delayMs)
.setExtras(extras)
.build()
}
}