Skip to content

Commit

Permalink
feat: support create token endpoint (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
jleon15 authored Dec 21, 2022
1 parent c3c93ad commit f441a5c
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 15 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Simply include one or more elements within your application's views:
```

Then tokenize the user input by referencing these elements. This can be wired up in response to a
button click, or any other user action.
button click, or any other user action.

```kotlin
val cardNumberElement = findViewById(R.id.card_number)
Expand Down
28 changes: 25 additions & 3 deletions docs/BasisTheoryElements.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,28 @@ val tokenizeResponse = bt.tokenize(object {
})
```

As you can see from this example, the `tokenize` function is capable of resolving the raw data
from references to element inputs. This enables your application to tokenize sensitive data values
without needing to touch the raw data directly.
As you can see from this example, the `tokenize` function is capable of resolving the raw data
from references to Element inputs. This enables your application to tokenize sensitive data values without
needing to touch the raw data directly.

### createToken

This function wraps our **`[create token API](https://docs.basistheory.com/#tokens-create-token)`** to
be able to create a single strongly typed token. It also provides added support for referencing
instances of Elements within your request payload.

```kotlin
val createTokenResponse = bt.createToken(CreateTokenRequest().apply {
this.type = "token"
this.data = object {
val name = nameElement // an instance of TextElement
val phoneNumber = phoneNumberElement // an instance of TextElement
val note = "Non sensitive value" // plaintext strings can also be included in the token body
}
this.expires_at = "2022-11-03T19:14:21.4333333Z" // all standard token attributes are supported
})
```

As you can see from this example, the `createToken` function is capable of resolving the raw data
from references to Element inputs. This enables your application to tokenize sensitive data values without
needing to touch the raw data directly.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.basistheory.android.service
import com.basistheory.ApiClient
import com.basistheory.Configuration
import com.basistheory.TokenizeApi
import com.basistheory.TokensApi
import com.basistheory.android.BuildConfig
import com.basistheory.auth.ApiKeyAuth

Expand All @@ -14,6 +15,9 @@ internal class ApiClientProvider(
fun getTokenizeApi(apiKeyOverride: String? = null): TokenizeApi =
TokenizeApi(getApiClient(apiKeyOverride))

fun getTokensApi(apiKeyOverride: String? = null): TokensApi =
TokensApi(getApiClient(apiKeyOverride))

private fun getApiClient(apiKeyOverride: String? = null): ApiClient {
val apiKey = apiKeyOverride ?: defaultApiKey
requireNotNull(apiKey)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.basistheory.android.service

import com.basistheory.CreateTokenRequest
import com.basistheory.CreateTokenResponse
import com.basistheory.android.model.ElementValueReference
import com.basistheory.android.util.isPrimitiveType
import com.basistheory.android.util.toMap
Expand All @@ -26,20 +28,42 @@ class BasisTheoryElements internal constructor(
tokenizeApiClient.tokenize(request)
}

@JvmOverloads
suspend fun createToken(
createTokenRequest: CreateTokenRequest,
apiKeyOverride: String? = null
): CreateTokenResponse =
withContext(ioDispatcher) {
val tokensApi = apiClientProvider.getTokensApi(apiKeyOverride)
val data =
if (createTokenRequest.data == null) null
else if (createTokenRequest.data!!::class.java.isPrimitiveType()) createTokenRequest.data
else if (createTokenRequest.data is TextElement) (createTokenRequest.data as TextElement).getText()
else if (createTokenRequest.data is ElementValueReference) (createTokenRequest.data as ElementValueReference).getValue()
else replaceElementRefs(createTokenRequest.data!!.toMap())

createTokenRequest.data = data

tokensApi.create(createTokenRequest)
}

private fun replaceElementRefs(map: MutableMap<String, Any?>): MutableMap<String, Any?> {
for ((key, value) in map) {
if (value == null) continue
val fieldType = value::class.java
if (!fieldType.isPrimitiveType()) {
if (value is TextElement) {
map[key] = value.getText()
}
else if (value is ElementValueReference) {
map[key] = value.getValue()
} else {
val children = value.toMap()
map[key] = children
replaceElementRefs(children)
when (value) {
is TextElement -> {
map[key] = value.getText()
}
is ElementValueReference -> {
map[key] = value.getValue()
}
else -> {
val children = value.toMap()
map[key] = children
replaceElementRefs(children)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.basistheory.android.service

import android.app.Activity
import com.basistheory.CreateTokenRequest
import com.basistheory.TokenizeApi
import com.basistheory.TokensApi
import com.basistheory.android.view.CardExpirationDateElement
import com.basistheory.android.view.CardNumberElement
import com.basistheory.android.view.CardVerificationCodeElement
Expand All @@ -23,9 +25,7 @@ import org.robolectric.RobolectricTestRunner
import java.time.Instant
import java.time.LocalDate
import java.time.temporal.ChronoUnit
import java.time.temporal.TemporalUnit
import java.util.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject

@RunWith(RobolectricTestRunner::class)
Expand All @@ -43,6 +43,9 @@ class BasisTheoryElementsTests {
@RelaxedMockK
private lateinit var tokenizeApi: TokenizeApi

@RelaxedMockK
private lateinit var tokensApi: TokensApi

@RelaxedMockK
private lateinit var provider: ApiClientProvider

Expand Down Expand Up @@ -251,4 +254,193 @@ class BasisTheoryElementsTests {

verify { tokenizeApi.tokenize(expectedRequest) }
}

@Test
fun `createToken should pass api key override to ApiClientProvider`() = runBlocking {
val apiKeyOverride = UUID.randomUUID().toString()

every { provider.getTokensApi(any()) } returns tokensApi

bt.createToken(CreateTokenRequest(), apiKeyOverride)

verify { provider.getTokensApi(apiKeyOverride) }
}

@Test
fun `createToken should forward top level primitive value without modification`() =
runBlocking {
every { provider.getTokensApi(any()) } returns tokensApi

val name = faker.name().fullName()
val createTokenRequest = createTokenRequest(name)
bt.createToken(createTokenRequest)

verify { tokensApi.create(createTokenRequest) }
}

@Test
fun `createToken should forward complex data values within request without modification`() =
runBlocking {
every { provider.getTokensApi(any()) } returns tokensApi

val data = object {
val string = faker.lorem().word()
val int = faker.random().nextInt(10, 100)
val nullValue = null
val nested = object {
val double = faker.random().nextDouble()
val bool = faker.random().nextBoolean()
val timestamp = Instant.now().toString()
val nullValue = null
}
}
val request = createTokenRequest(data)

bt.createToken(request)

val expectedData = mapOf(
"string" to data.string,
"int" to data.int,
"nullValue" to null,
"nested" to mapOf(
"double" to data.nested.double,
"bool" to data.nested.bool,
"timestamp" to data.nested.timestamp,
"nullValue" to null
)
)
val expectedRequest = createTokenRequest(expectedData)

verify { tokensApi.create(expectedRequest) }
}

@Test
fun `createToken should replace top level TextElement ref with underlying data value`() =
runBlocking {
every { provider.getTokensApi(any()) } returns tokensApi

val name = faker.name().fullName()
nameElement.setText(name)

val createTokenRequest = createTokenRequest(nameElement)

bt.createToken(createTokenRequest)

val expectedRequest = createTokenRequest(name)

verify { tokensApi.create(expectedRequest) }
}

@Test
fun `createToken should replace top level CardElement ref with underlying data value`() =
runBlocking {
every { provider.getTokensApi(any()) } returns tokensApi

val cardNumber = faker.business().creditCardNumber()
cardNumberElement.setText(cardNumber)

val createTokenRequest = createTokenRequest(cardNumberElement)

bt.createToken(createTokenRequest)

val expectedRequest = createTokenRequest(cardNumber.replace(Regex("""[^\d]"""), ""))

verify { tokensApi.create(expectedRequest) }
}

@Test
fun `createToken should replace top level CardExpirationDateElement refs with underlying data value`() =
runBlocking {
every { provider.getTokensApi(any()) } returns tokensApi

val expDate = LocalDate.now().plus(2, ChronoUnit.YEARS)
val month = expDate.monthValue.toString().padStart(2, '0')
val year = expDate.year.toString()
cardExpElement.setText("$month/${year.takeLast(2)}")

val createTokenRequestMonth = createTokenRequest(cardExpElement.month())
val createTokenRequestYear = createTokenRequest(cardExpElement.year())

bt.createToken(createTokenRequestMonth)

val expectedMonthRequest = createTokenRequest(month)
verify { tokensApi.create(expectedMonthRequest) }

bt.createToken(createTokenRequestYear)

val expectedYearRequest = createTokenRequest(year)
verify { tokensApi.create(expectedYearRequest) }
}

@Test
fun `createToken should replace Element refs within request object with underlying data values`() =
runBlocking {
every { provider.getTokensApi(any()) } returns tokensApi

val name = faker.name().fullName()
nameElement.setText(name)

val phoneNumber = faker.phoneNumber().phoneNumber()
phoneNumberElement.setText(phoneNumber)

val cardNumber = faker.business().creditCardNumber()
cardNumberElement.setText(cardNumber)

val expDate = LocalDate.now().plus(2, ChronoUnit.YEARS)
val expMonth = expDate.monthValue.toString().padStart(2, '0')
val expYear = expDate.year.toString()
cardExpElement.setText("$expMonth/${expYear.takeLast(2)}")

val cvc = faker.random().nextInt(100, 999).toString()
cvcElement.setText(cvc)

val data = object {
val type = "token"
val data = object {
val raw = faker.lorem().word()
val name = nameElement
val card = object {
val number = cardNumberElement
val expMonth = cardExpElement.month()
val expYear = cardExpElement.year()
val cvc = cvcElement
}
val nested = object {
val raw = faker.lorem().word()
val phoneNumber = phoneNumberElement
}
}
}
val createTokenRequest = createTokenRequest(data)

bt.createToken(createTokenRequest)

val expectedData = mapOf<String, Any?>(
"type" to data.type,
"data" to mapOf(
"raw" to data.data.raw,
"name" to name,
"card" to mapOf(
"number" to cardNumber.replace(Regex("""[^\d]"""), ""),
"expMonth" to expMonth,
"expYear" to expYear,
"cvc" to cvc
),
"nested" to mapOf(
"raw" to data.data.nested.raw,
"phoneNumber" to phoneNumber
)
)
)

val expectedCreateTokenRequest = createTokenRequest(expectedData)

verify { tokensApi.create(expectedCreateTokenRequest) }
}

private fun createTokenRequest(data: Any): CreateTokenRequest =
CreateTokenRequest().apply {
this.type = "token"
this.data = data
}
}

0 comments on commit f441a5c

Please sign in to comment.