8000 DFP: Unit tests by jhaven-stytch · Pull Request #91 · stytchauth/stytch-android · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

DFP: Unit tests #91

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

Merged
merged 4 commits into from
Sep 11, 2023
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
1 change: 1 addition & 0 deletions sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,5 @@ dependencies {
testImplementation "io.mockk:mockk:1.12.5"
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
testImplementation("com.squareup.okhttp3:mockwebserver:4.7.2")
testImplementation("org.json:json:20230227")
}
4 changes: 4 additions & 0 deletions sdk/src/main/java/com/stytch/sdk/common/dfp/DFPProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.view.ViewGroup
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import kotlin.coroutines.resume
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.suspendCancellableCoroutine
Expand Down Expand Up @@ -60,6 +61,9 @@ internal class DFPProviderImpl(
webview = createWebView(it)
it.addContentView(webview, ViewGroup.LayoutParams(0, 0))
}
} ?: run {
// Couldn't inject webview, return empty string
continuation.resume("")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ internal fun Context.getDeviceInfo(): DeviceInfo {
deviceInfo.applicationPackageName = applicationContext.packageName
deviceInfo.osVersion = Build.VERSION.SDK_INT.toString()
deviceInfo.deviceName = Build.MODEL
deviceInfo.osName = Build.VERSION.CODENAME
deviceInfo.osName = "Android"

try {
// throw exceptions if packageName not found
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.stytch.sdk.common.extensions

import okhttp3.RequestBody
import okio.Buffer

internal fun RequestBody?.asJsonString(): String {
if (this == null) return "{}"
val buffer = Buffer()
writeTo(buffer)
var bodyAsString: String
buffer.use {
bodyAsString = it.readUtf8()
}
if (bodyAsString.isBlank()) {
bodyAsString = "{}"
}
return bodyAsString
}
16 changes: 16 additions & 0 deletions sdk/src/main/java/com/stytch/sdk/common/extensions/RequestExt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.stytch.sdk.common.extensions

import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject

internal fun Request.toNewRequest(params: Map<String, String>): Request {
val bodyAsString = body.asJsonString()
val updatedBody = JSONObject(bodyAsString).apply {
params.forEach {
put(it.key, it.value)
}
}.toString()
val newBody = updatedBody.toRequestBody(body?.contentType())
return newBuilder().method(method, newBody).build()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.stytch.sdk.common.extensions

import okhttp3.Response

private const val HTTP_UNAUTHORIZED = 403

internal fun Response.requiresCaptcha(): Boolean {
return code == HTTP_UNAUTHORIZED
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,77 +2,40 @@ package com.stytch.sdk.common.network

import com.stytch.sdk.common.dfp.CaptchaProvider
import com.stytch.sdk.common.dfp.DFPProvider
import com.stytch.sdk.common.extensions.requiresCaptcha
import com.stytch.sdk.common.extensions.toNewRequest
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okio.Buffer
import org.json.JSONObject

private const val DFP_TELEMETRY_ID_KEY = "dfp_telemetry_id"
private const val CAPTCHA_TOKEN_KEY = "captcha_token"
private const val HTTP_UNAUTHORIZED = 403

internal const val DFP_TELEMETRY_ID_KEY = "dfp_telemetry_id"
internal const val CAPTCHA_TOKEN_KEY = "captcha_token"
internal class StytchDFPInterceptor(
private val dfpProvider: DFPProvider,
private val captchaProvider: CaptchaProvider
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
if (request.method == "GET" || request.method == "DELETE") return chain.proceed(request)
val response = chain.proceed(request.addDfpTelemetryIdToRequest())
val response = chain.proceed(
request.toNewRequest(
mapOf(
DFP_TELEMETRY_ID_KEY to runBlocking { dfpProvider.getTelemetryId() }
)
)
)
return if (response.requiresCaptcha()) {
response.close()
chain.proceed(request.addDfpTelemetryIdAndCaptchaTokenToRequest())
chain.proceed(
request.toNewRequest(
mapOf(
DFP_TELEMETRY_ID_KEY to runBlocking { dfpProvider.getTelemetryId() },
CAPTCHA_TOKEN_KEY to runBlocking { captchaProvider.executeRecaptcha() }
)
)
)
} else {
response
}
}

private fun Response.requiresCaptcha(): Boolean {
return code == HTTP_UNAUTHORIZED && message.contains("Captcha required")
}

private fun Request.addDfpTelemetryIdToRequest(): Request {
val dfpTelemetryId = runBlocking { dfpProvider.getTelemetryId() }
F438 return toNewRequest(mapOf(DFP_TELEMETRY_ID_KEY to dfpTelemetryId))
}

private fun Request.addDfpTelemetryIdAndCaptchaTokenToRequest(): Request {
val dfpTelemetryId = runBlocking { dfpProvider.getTelemetryId() }
val dfpCaptchaToken = runBlocking { captchaProvider.executeRecaptcha() }
return toNewRequest(
mapOf(
DFP_TELEMETRY_ID_KEY to dfpTelemetryId,
CAPTCHA_TOKEN_KEY to dfpCaptchaToken
)
)
}

private fun Request.toNewRequest(params: Map<String, String>): Request {
val bodyAsString = body.asJsonString()
val updatedBody = JSONObject(bodyAsString).apply {
params.forEach {
put(it.key, it.value)
}
}.toString()
val newBody = updatedBody.toRequestBody(body?.contentType())
return newBuilder().method(method, newBody).build()
}

private fun RequestBody?.asJsonString(): String {
if (this == null) return "{}"
val buffer = Buffer()
writeTo(buffer)
var bodyAsString: String
buffer.use {
bodyAsString = it.readUtf8()
}
if (bodyAsString.isBlank()) {
bodyAsString = "{}"
}
return bodyAsString
}
}
9 changes: 8 additions & 1 deletion sdk/src/test/java/com/stytch/sdk/b2b/StytchB2BClientTest.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.stytch.sdk.b2b

import android.app.Application
import android.content.Context
import android.net.Uri
import com.stytch.sdk.b2b.magicLinks.B2BMagicLinks
Expand Down Expand Up @@ -59,7 +60,13 @@ internal class StytchB2BClientTest {
mockkStatic("com.stytch.sdk.common.extensions.ContextExtKt")
mockkObject(EncryptionManager)
every { EncryptionManager.createNewKeys(any(), any()) } returns Unit
mContextMock = mockk(relaxed = true)
val mockApplication: Application = mockk {
every { registerActivityLifecycleCallbacks(any()) } just runs
every { packageName } returns "Stytch"
}
mContextMock = mockk(relaxed = true) {
every { applicationContext } returns mockApplication
}
every { KeyStore.getInstance(any()) } returns mockk(relaxed = true)
mockkObject(StorageHelper)
mockkObject(StytchB2BApi)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ internal class StytchB2BApiServiceTest {
apiService = ApiService.createApiService(
mockWebServer.url("/").toString(),
null,
null,
{},
StytchB2BApiService::class.java
)
Expand Down
10 changes: 10 additions & 0 deletions sdk/src/test/java/com/stytch/sdk/b2b/network/StytchB2BApiTest.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.stytch.sdk.b2b.network

import android.app.Application
import android.content.Context
import com.stytch.sdk.b2b.StytchB2BClient
import com.stytch.sdk.b2b.network.models.AllowedAuthMethods
Expand All @@ -18,9 +19,11 @@ import io.mockk.clearAllMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.unmockkAll
import java.security.KeyStore
import kotlinx.coroutines.ExperimentalCoroutinesApi
Expand All @@ -36,6 +39,13 @@ internal class StytchB2BApiTest {

@Before
fun before() {
val mockApplication: Application = mockk {
every { registerActivityLifecycleCallbacks(any()) } just runs
every { packageName } returns "Stytch"
}
mContextMock = mockk(relaxed = true) {
every { applicationContext } returns mockApplication
}
mockkStatic(KeyStore::class)
mockkObject(EncryptionManager)
mockkObject(StytchB2BApi)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.stytch.sdk.common.extensions

import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import org.junit.Test

internal class RequestBodyExtTest {
@Test
fun `null requestBody returns empty JSON string`() {
val rb: RequestBody? = null
assert(rb.asJsonString() == "{}")
}

@Test
fun `blank requestBody returns empty JSON string`() {
val rb = "".toRequestBody("application/json".toMediaTypeOrNull())
assert(rb.asJsonString() == "{}")
}

@Test
fun `a body is read into a string`() {
val rb = """{"a": true}""".toRequestBody("application/json".toMediaTypeOrNull())
assert(rb.asJsonString() == """{"a": true}""")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.stytch.sdk.common.extensions

import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import org.junit.Test

internal class RequestExtTest {
@Test
fun `toNewRequest adds appropriate items to body`() {
val originalBody = """{"a": true}""".toRequestBody("application/json".toMediaTypeOrNull())
val originalRequest = Request.Builder().url("http://stytch.com/").post(originalBody).build()
val newParams = mapOf(
"telemetry_id" to "telemetry-id",
"captcha_token" to "captcha-token"
)
val newRequest = originalRequest.toNewRequest(newParams)
val newRequestBodyAsJson = JSONObject(newRequest.body.asJsonString())
assert(newRequestBodyAsJson.getBoolean("a"))
assert(newRequestBodyAsJson.getString("telemetry_id") == "telemetry-id")
assert(newRequestBodyAsJson.getString("captcha_token") == "captcha-token")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.stytch.sdk.common.extensions

import io.mockk.every
import io.mockk.mockk
import okhttp3.Response
import org.junit.Test

internal class ResponseExtTest {
@Test
fun `non-403 codes return false`() {
val response: Response = mockk {
every { code } returns 200
}
assert(!response.requiresCaptcha())
}

@Test
fun `403 code returns true`() {
val response: Response = mockk {
every { code } returns 403
}
assert(response.requiresCaptcha())
}
}
Loading
0