Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add integration test checking functionality of Push #300

Merged
merged 9 commits into from
Sep 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()

signingConfigs {
debug {
storeFile file("debug.keystore")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.jraska.github.client

import com.jraska.github.client.core.android.ServiceModel
import com.jraska.github.client.push.PushHandleModel
import com.jraska.github.client.xpush.PushAwaitRule

class DecoratedServiceModelFactory(
private val productionFactory: ServiceModel.Factory
) : ServiceModel.Factory {

@Suppress("UNCHECKED_CAST")
override fun <T : ServiceModel> create(modelClass: Class<T>): T {
if (modelClass == PushHandleModel::class.java) {
return PushAwaitRule.TestPushHandleModel(productionFactory.create(modelClass)) as T
}
return productionFactory.create(modelClass)
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
package com.jraska.github.client

import androidx.test.platform.app.InstrumentationRegistry
import com.jraska.github.client.core.android.ServiceModel
import com.jraska.github.client.http.ReplayHttpComponent

class TestUITestApp : GitHubClientApp() {
val coreComponent = FakeCoreComponent()
val decoratedServiceFactory by lazy {
DecoratedServiceModelFactory(super.serviceModelFactory())
}

override fun serviceModelFactory(): ServiceModel.Factory {
return decoratedServiceFactory
}

override fun retrofit(): HasRetrofit {
return ReplayHttpComponent.create()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.jraska.github.client.xpush

import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource
import androidx.test.espresso.idling.CountingIdlingResource
import androidx.test.internal.runner.junit4.statement.UiThreadStatement
import com.google.firebase.messaging.RemoteMessage
import com.jraska.github.client.push.PushHandleModel
import org.junit.rules.ExternalResource

class PushAwaitRule : ExternalResource() {
override fun before() {
IdlingRegistry.getInstance().register(PushAwaitIdlingResource.idlingResource())
}

override fun after() {
IdlingRegistry.getInstance().unregister(PushAwaitIdlingResource.idlingResource())
}

fun waitForPush() {
PushAwaitIdlingResource.waitForPush()
}

private object PushAwaitIdlingResource {
private val countingIdlingResource = CountingIdlingResource("Push Await")

fun idlingResource(): IdlingResource = countingIdlingResource

fun waitForPush() = countingIdlingResource.increment()

fun onPush() {
// Doing this to make sure anything scheduled on UI thread will run before this
UiThreadStatement.runOnUiThread {
countingIdlingResource.decrement()
}
}
}

class TestPushHandleModel(
val productionModel: PushHandleModel,
) : PushHandleModel by productionModel {
override fun onMessageReceived(message: RemoteMessage) {
productionModel.onMessageReceived(message)
PushAwaitIdlingResource.onPush()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.jraska.github.client.xpush

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import com.google.android.gms.tasks.Tasks
import com.google.firebase.iid.FirebaseInstanceId
import com.jraska.github.client.DeepLinkLaunchTest
import org.junit.Assume
import org.junit.Before
import org.junit.Rule
import org.junit.Test

class PushIntegrationTest {

lateinit var pushClient: PushServerClient
lateinit var thisDeviceToken: String

@get:Rule
val pushRule = PushAwaitRule()

@Before
fun setUp() {
pushClient = PushServerClient.create(apiKey())

val instanceIdTask = FirebaseInstanceId.getInstance().instanceId
thisDeviceToken = Tasks.await(instanceIdTask).token
}

@Test
fun testPushIntegration_fromSettingsToAbout() {
DeepLinkLaunchTest.launchDeepLink("https://github.com/settings")

sendDeepLinkPush("https://github.com/about")

awaitPush()
onView(withText("by Josef Raska")).check(matches(isDisplayed()))
}

@Test
fun testPushIntegration_fromAboutToSettings() {
DeepLinkLaunchTest.launchDeepLink("https://github.com/about")

sendDeepLinkPush("https://github.com/settings")

awaitPush()
onView(withText("Purchase")).check(matches(isDisplayed()))
}

// See LaunchDeepLinkCommand to see how this is handled.
private fun sendDeepLinkPush(deepLink: String) {
val messageToThisDevice = PushServerDto().apply {
ids.add(thisDeviceToken)
data["action"] = "launch_deep_link"
data["deepLink"] = deepLink
}

pushClient.sendPush(messageToThisDevice).blockingAwait()
}

private fun apiKey(): String {
val apiKey = InstrumentationRegistry.getArguments()["FCM_API_KEY"]
Assume.assumeTrue("FCM key not found in argument 'FCM_API_KEY', ignoring the test.", apiKey is String)

return apiKey as String
}

private fun awaitPush() {
pushRule.waitForPush()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.jraska.github.client.xpush

import io.reactivex.Completable
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.Body
import retrofit2.http.POST
import timber.log.Timber

interface PushServerClient {
@POST("/fcm/send")
fun sendPush(@Body message: PushServerDto): Completable

companion object {
fun create(authorizationToken: String): PushServerClient {
return Retrofit.Builder().validateEagerly(true)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("https://fcm.googleapis.com")
.client(
OkHttpClient.Builder()
.addInterceptor { chain ->
chain.proceed(
chain.request().newBuilder()
.addHeader("Authorization", "key=$authorizationToken")
.build()
)
}
.addInterceptor(HttpLoggingInterceptor { Timber.d(it) }.setLevel(HttpLoggingInterceptor.Level.BASIC))
.build()
)
.build()
.create(PushServerClient::class.java)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.jraska.github.client.xpush

import com.google.gson.annotations.SerializedName

class PushServerDto {
@SerializedName("registration_ids")
val ids = mutableListOf<String>()

@SerializedName("data")
val data = mutableMapOf<String, String>()
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,9 @@ package com.jraska.github.client.push

import com.google.firebase.messaging.RemoteMessage
import com.jraska.github.client.core.android.ServiceModel
import javax.inject.Inject

internal class PushHandleModel @Inject constructor(
private val pushHandler: PushHandler,
private val analytics: PushAnalytics,
private val tokenSynchronizer: PushTokenSynchronizer
) : ServiceModel {
fun onMessageReceived(remoteMessage: RemoteMessage) {
val action = RemoteMessageToActionConverter.convert(remoteMessage)
interface PushHandleModel : ServiceModel {
fun onMessageReceived(message: RemoteMessage)

val pushResult = pushHandler.handlePush(action)

analytics.onPushHandled(action, pushResult)
}

fun onNewToken(token: String) {
analytics.onTokenRefresh()
tokenSynchronizer.onTokenRefresh(token)
}
fun onNewToken(token: String)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.jraska.github.client.push

import com.google.firebase.messaging.RemoteMessage
import com.jraska.github.client.core.android.ServiceModel
import javax.inject.Inject

internal class PushHandleModelImpl @Inject constructor(
private val pushHandler: PushHandler,
private val analytics: PushAnalytics,
private val tokenSynchronizer: PushTokenSynchronizer
) : PushHandleModel, ServiceModel {
override fun onMessageReceived(message: RemoteMessage) {
val action = RemoteMessageToActionConverter.convert(message)

val pushResult = pushHandler.handlePush(action)

analytics.onPushHandled(action, pushResult)
}

override fun onNewToken(token: String) {
analytics.onTokenRefresh()
tokenSynchronizer.onTokenRefresh(token)
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
package com.jraska.github.client.push

import com.jraska.github.client.analytics.EventAnalytics
import com.jraska.github.client.common.BooleanResult
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Provider

class PushHandler @Inject internal constructor(
private val eventAnalytics: EventAnalytics,
private val pushCommands: Map<String, @JvmSuppressWildcards Provider<PushActionCommand>>
) {

internal fun handlePush(action: PushAction): BooleanResult {
Timber.v("Push received action: %s", action.name)
Timber.d("Push received action: %s", action.name)

return handleInternal(action)
val result = handleInternal(action)

Timber.d("Push result: %s, Action: %s", result, action)

return result
}

private fun handleInternal(action: PushAction): BooleanResult {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ object PushModule {
@ClassKey(PushHandleModel::class)
internal abstract fun bindServiceModel(pushHandleModel: PushHandleModel): ServiceModel

@Binds
internal abstract fun bindPushModel(pushHandleModel: PushHandleModelImpl): PushHandleModel

@Binds
@IntoMap
@StringKey("refresh_config")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ class FirebaseTestLabPlugin : Plugin<Project> {
val device = "model=$deviceName,version=$androidVersion,locale=en,orientation=portrait"
val resultDir = DateTimeFormatter.ISO_DATE_TIME.format(LocalDateTime.now())

val fcmKey = System.getenv("FCM_API_KEY")

resultsFileToPull = "gs://test-lab-twsawhz0hy5am-h35y3vymzadax/$resultDir/$deviceName-$androidVersion-en-portrait/test_result_1.xml"

it.commandLine =
Expand All @@ -41,7 +43,8 @@ class FirebaseTestLabPlugin : Plugin<Project> {
"--test $testApk " +
"--device $device " +
"--results-dir $resultDir " +
"--no-performance-metrics")
"--no-performance-metrics " +
"--environment-variables FCM_API_KEY=$fcmKey")
.split(' ')

it.dependsOn(project.tasks.named("assembleDebugAndroidTest"))
Expand Down