From f7ade8ad31d06abf0ddf22c6ea6966c66c89a5b1 Mon Sep 17 00:00:00 2001 From: Sina Madani Date: Tue, 23 Jul 2024 17:52:23 +0200 Subject: [PATCH] feat: Add Verify v1 (#1) * wip: Verify v1 * Test check code * Bump versions * All success tests * Test errors * Test check failure * Fix failing build on older JDK --- CHANGELOG.md | 5 + pom.xml | 24 +- .../com/vonage/client/kt/VerifyLegacy.kt | 44 +++ .../kotlin/com/vonage/client/kt/Vonage.kt | 1 + .../com/vonage/client/kt/AbstractTest.kt | 11 +- .../com/vonage/client/kt/ConversionTest.kt | 2 +- .../com/vonage/client/kt/VerifyLegacyTest.kt | 276 ++++++++++++++++++ .../kotlin/com/vonage/client/kt/VoiceTest.kt | 1 - 8 files changed, 346 insertions(+), 18 deletions(-) create mode 100644 src/main/kotlin/com/vonage/client/kt/VerifyLegacy.kt create mode 100644 src/test/kotlin/com/vonage/client/kt/VerifyLegacyTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index a6e8aef..44b5d87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [0.4.0] - 2024-07-?? + +### Added +- Verify v1 API + ## [0.3.1] - 2024-07-12 ### Changed diff --git a/pom.xml b/pom.xml index e191b27..d149214 100644 --- a/pom.xml +++ b/pom.xml @@ -5,9 +5,8 @@ com.vonage server-sdk-kotlin - 0.3.1 + 0.4.0 - jar Vonage Kotlin Server SDK Kotlin client for Vonage APIs https://github.com/Vonage/vonage-kotlin-sdk @@ -48,6 +47,7 @@ 2.0 2.0 1.8 + 8 @@ -59,7 +59,7 @@ com.vonage server-sdk - 8.9.2 + 8.9.3 org.jetbrains.kotlin @@ -76,7 +76,7 @@ org.wiremock wiremock-standalone - 3.8.0 + 3.9.0 test @@ -93,7 +93,6 @@ ${project.basedir}/src/test/kotlin - org.apache.maven.plugins maven-enforcer-plugin 3.5.0 @@ -108,7 +107,7 @@ 3.6.3 - 1.8 + ${java.version} @@ -116,9 +115,8 @@ - org.apache.maven.plugins maven-surefire-plugin - 3.3.0 + 3.3.1 org.jacoco @@ -154,7 +152,7 @@ maven-javadoc-plugin - 3.7.0 + 3.8.0 dokka-jar @@ -170,7 +168,6 @@ - org.apache.maven.plugins maven-jar-plugin 3.4.2 @@ -185,13 +182,12 @@ - org.apache.maven.plugins maven-compiler-plugin 3.13.0 - 8 - 8 - 8 + ${java.version} + ${java.version} + ${java.version} diff --git a/src/main/kotlin/com/vonage/client/kt/VerifyLegacy.kt b/src/main/kotlin/com/vonage/client/kt/VerifyLegacy.kt new file mode 100644 index 0000000..186fd00 --- /dev/null +++ b/src/main/kotlin/com/vonage/client/kt/VerifyLegacy.kt @@ -0,0 +1,44 @@ +package com.vonage.client.kt + +import com.vonage.client.verify.* + +class VerifyLegacy(private val verifyClient: VerifyClient) { + + fun verify(number: String, brand: String, properties: (VerifyRequest.Builder.() -> Unit) = {}): VerifyResponse = + verifyClient.verify(VerifyRequest.builder(number, brand).apply(properties).build()) + + fun psd2Verify(number: String, amount: Double, payee: String, + properties: (Psd2Request.Builder.() -> Unit) = {}): VerifyResponse = + verifyClient.psd2Verify(Psd2Request.builder(number, amount, payee).apply(properties).build()) + + fun search(vararg requestIds: String): SearchVerifyResponse = verifyClient.search(*requestIds) + + fun request(requestId: String): ExistingRequest = ExistingRequest(requestId) + + fun request(response: VerifyResponse): ExistingRequest = request(response.requestId) + + inner class ExistingRequest internal constructor(private val requestId: String) { + + fun cancel(): ControlResponse = verifyClient.cancelVerification(requestId) + + fun advance(): ControlResponse = verifyClient.advanceVerification(requestId) + + fun check(code: String): CheckResponse = verifyClient.check(requestId, code) + + fun search(): SearchVerifyResponse = verifyClient.search(requestId) + + @Override + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as ExistingRequest + return requestId == other.requestId + } + + @Override + override fun hashCode(): Int { + return requestId.hashCode() + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/vonage/client/kt/Vonage.kt b/src/main/kotlin/com/vonage/client/kt/Vonage.kt index 24b6224..1187f63 100644 --- a/src/main/kotlin/com/vonage/client/kt/Vonage.kt +++ b/src/main/kotlin/com/vonage/client/kt/Vonage.kt @@ -11,6 +11,7 @@ class Vonage(init: VonageClient.Builder.() -> Unit) { val sms = Sms(vonageClient.smsClient) val conversion = Conversion(vonageClient.conversionClient) val redact = Redact(vonageClient.redactClient) + val verifyLegacy = VerifyLegacy(vonageClient.verifyClient) } fun VonageClient.Builder.authFromEnv(): VonageClient.Builder { diff --git a/src/test/kotlin/com/vonage/client/kt/AbstractTest.kt b/src/test/kotlin/com/vonage/client/kt/AbstractTest.kt index dcfb9a4..92b49ce 100644 --- a/src/test/kotlin/com/vonage/client/kt/AbstractTest.kt +++ b/src/test/kotlin/com/vonage/client/kt/AbstractTest.kt @@ -1,6 +1,5 @@ package com.vonage.client.kt -import com.fasterxml.jackson.databind.ObjectMapper import com.github.tomakehurst.wiremock.WireMockServer import com.github.tomakehurst.wiremock.client.WireMock import com.github.tomakehurst.wiremock.client.WireMock.* @@ -12,6 +11,7 @@ import com.vonage.client.common.HttpMethod import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.assertThrows +import com.fasterxml.jackson.databind.ObjectMapper import java.net.URI import java.net.URLEncoder import java.nio.charset.StandardCharsets @@ -25,9 +25,9 @@ abstract class AbstractTest { private val apiSecret = "1234567890abcdef" private val apiKeySecretEncoded = "YTFiMmMzZDQ6MTIzNDU2Nzg5MGFiY2RlZg==" private val privateKeyPath = "src/test/resources/com/vonage/client/kt/application_key" + private val signatureSecretName = "sig" private val apiSecretName = "api_secret" private val apiKeyName = "api_key" - private val signatureSecretName = "sig" protected val testUuidStr = "aaaaaaaa-bbbb-4ccc-8ddd-0123456789ab" protected val testUuid: UUID = UUID.fromString(testUuidStr) protected val toNumber = "447712345689" @@ -43,9 +43,13 @@ abstract class AbstractTest { protected val endTime: Instant = Instant.parse(endTimeStr) protected val timestampStr = "2016-11-14T07:45:14Z" protected val timestampDateStr = "2016-11-14 07:45:14" + protected val timestampDate = strToDate(timestampDateStr) + protected val timestampDate2Str = "2019-03-02 18:46:57" + protected val timestampDate2 = strToDate(timestampDate2Str) protected val timestamp: Instant = Instant.parse(timestampStr) protected val timestamp2Str = "2020-01-29T14:08:30.201Z" protected val timestamp2: Instant = Instant.parse(timestamp2Str) + protected val currency = "EUR" private val port = 8081 private val wiremock: WireMockServer = WireMockServer( @@ -71,6 +75,9 @@ abstract class AbstractTest { wiremock.stop() } + protected fun strToDate(dateStr: String): Date = + Date(Instant.parse(dateStr.replace(' ', 'T') + 'Z').toEpochMilli()) + protected enum class ContentType(val mime: String) { APPLICATION_JSON("application/json"), FORM_URLENCODED("application/x-www-form-urlencoded"); diff --git a/src/test/kotlin/com/vonage/client/kt/ConversionTest.kt b/src/test/kotlin/com/vonage/client/kt/ConversionTest.kt index 006bf43..dae717c 100644 --- a/src/test/kotlin/com/vonage/client/kt/ConversionTest.kt +++ b/src/test/kotlin/com/vonage/client/kt/ConversionTest.kt @@ -18,7 +18,7 @@ class ConversionTest : AbstractTest() { fun `submit sms conversion with timestamp`() { val delivered = true mockSuccess(smsMessageId, smsEndpoint, delivered, true) - conversionClient.convertSms(smsMessageId, delivered, timestamp) + conversionClient.convertSms(smsMessageId, delivered, timestampDate.toInstant()) } @Test diff --git a/src/test/kotlin/com/vonage/client/kt/VerifyLegacyTest.kt b/src/test/kotlin/com/vonage/client/kt/VerifyLegacyTest.kt new file mode 100644 index 0000000..fbca0ce --- /dev/null +++ b/src/test/kotlin/com/vonage/client/kt/VerifyLegacyTest.kt @@ -0,0 +1,276 @@ +package com.vonage.client.kt + +import com.vonage.client.verify.* +import java.math.BigDecimal +import java.util.* +import kotlin.test.* + +class VerifyLegacyTest : AbstractTest() { + private val verifyClient = vonage.verifyLegacy + private val requestId = "abcdef0123456789abcdef0123456789" + private val existingRequest = verifyClient.request(requestId) + private val eventId = "0A00000012345678" + private val accountId = "abcdef01" + private val payee = "Acme Inc" + private val amount = 48.31 + private val pinExpiry = 60 + private val nextEventWait = 750 + private val pinCode = "987123" + private val codeLength = 6 + private val price = "0.10000000" + private val estimatedPriceMessagesSent = "0.03330000" + + private fun getBaseUri(endpoint: String): String = + "/verify/$endpoint/json".replace("//", "/") + + private fun assertVerify(params: Map, invocation: VerifyLegacy.() -> VerifyResponse) { + val expectedUrl = getBaseUri(if (params.containsKey("payee")) "psd2" else "") + val successResponse = + mockPostQueryParams(expectedUrl, params, expectedResponseParams = mapOf( + "request_id" to requestId, + "status" to "0" + )) + val successParsed = invocation.invoke(verifyClient) + assertNotNull(successParsed) + assertEquals(requestId, successParsed.requestId) + assertEquals(VerifyStatus.OK, successParsed.status) + assertEquals(existingRequest, verifyClient.request(successParsed)) + + val errorText = "Your request is incomplete and missing the mandatory parameter `number`" + mockPostQueryParams(expectedUrl, params, expectedResponseParams = mapOf( + "request_id" to requestId, + "status" to "2", + "error_text" to errorText, + "network" to networkCode + )) + val failureParsed = invocation.invoke(verifyClient) + assertNotNull(failureParsed) + assertEquals(requestId, failureParsed.requestId) + assertEquals(VerifyStatus.MISSING_PARAMS, failureParsed.status) + assertEquals(errorText, failureParsed.errorText) + assertEquals(networkCode, failureParsed.network) + } + + private fun invokeControl(command: VerifyControlCommand) = when(command) { + VerifyControlCommand.CANCEL -> existingRequest.cancel() + VerifyControlCommand.TRIGGER_NEXT_EVENT -> existingRequest.advance() + } + + private fun assertControl(command: VerifyControlCommand) { + val cmdStr = command.name.lowercase() + val expectedUrl = getBaseUri("control") + val expectedRequestParams = mapOf( + "request_id" to requestId, + "cmd" to cmdStr + ) + mockPostQueryParams(expectedUrl, expectedRequestParams, + expectedResponseParams = mapOf( + "status" to "0", + "command" to cmdStr + ) + ) + val parsedSuccess = invokeControl(command) + assertNotNull(parsedSuccess) + assertEquals(command, parsedSuccess.command) + assertEquals(VerifyStatus.OK, VerifyStatus.fromInt(parsedSuccess.status.toInt())) + assertNull(parsedSuccess.errorText) + + val errorText = "Your account does not have sufficient credit to process this request." + mockPostQueryParams(expectedUrl, expectedRequestParams, + expectedResponseParams = mapOf( + "status" to "9", + "error_text" to errorText + ) + ) + + try { + val parsedFailure = invokeControl(command) + fail("Expected VerifyException but got $parsedFailure") + } + catch (ex: VerifyException) { + assertEquals(VerifyStatus.PARTNER_QUOTA_EXCEEDED, VerifyStatus.fromInt(ex.status.toInt())) + assertEquals(errorText, ex.errorText) + } + } + + private fun assertSearch(vararg requestIds: String) { + val single = requestIds.size == 1 + mockPostQueryParams( + expectedUrl = getBaseUri("search"), + expectedRequestParams = if (single) + mapOf("request_id" to requestIds.first()) else + mapOf("request_ids" to requestIds.last()), // Will contain duplicates otherwise + expectedResponseParams = mapOf( + "status" to "0", + "verification_requests" to listOf( + mapOf("status" to "EXPIRED"), + mapOf( + "request_id" to requestId, + "account_id" to accountId, + "number" to toNumber, + "currency" to currency, + "sender_id" to payee, + "date_submitted" to timestampDateStr, + "date_finalized" to timestampDate2Str, + "first_event_date" to timestampDateStr, + "last_event_date" to timestampDate2Str, + "status" to "IN PROGRESS", + "price" to price, + "estimated_price_messages_sent" to estimatedPriceMessagesSent + ), + mapOf() + ) + ) + ) + val response = if (single) existingRequest.search() else verifyClient.search(*requestIds) + assertNotNull(response) + assertNull(response.errorText) + assertEquals(3, response.verificationRequests.size) + assertNotNull(response.verificationRequests[2]) + assertEquals(VerifyDetails.Status.EXPIRED, response.verificationRequests[0].status) + val detail = response.verificationRequests[1] + assertNotNull(detail) + assertEquals(requestId, detail.requestId) + assertEquals(accountId, detail.accountId) + assertEquals(toNumber, detail.number) + assertEquals(currency, detail.currency) + assertEquals(payee, detail.senderId) + assertEquals(timestampDate, detail.dateSubmitted) + assertEquals(timestampDate2, detail.dateFinalized) + assertEquals(timestampDate, detail.firstEventDate) + assertEquals(timestampDate2, detail.lastEventDate) + assertEquals(VerifyDetails.Status.IN_PROGRESS, detail.status) + assertEquals(BigDecimal(price), detail.price) + assertEquals(BigDecimal(estimatedPriceMessagesSent), detail.estimatedPriceMessagesSent) + } + + @Test + fun `existing request hashCode is based on the requestId`() { + assertEquals(requestId.hashCode(), existingRequest.hashCode()) + assertEquals(existingRequest, verifyClient.request(requestId)) + } + + @Test + fun `verify request success required parameters`() { + assertVerify(mapOf("brand" to payee, "number" to toNumber)) { + verify(toNumber, payee) + } + } + + @Test + fun `verify request all parameters`() { + assertVerify(mapOf( + "brand" to payee, "number" to toNumber, + "sender_id" to altNumber, + "pin_expiry" to pinExpiry, + "pin_code" to pinCode, + "next_event_wait" to nextEventWait, + "country" to "GB", + "lg" to "en-gb", + "workflow_id" to 2 + )) { + verify(toNumber, payee) { + senderId(altNumber); pinCode(pinCode) + pinExpiry(pinExpiry); nextEventWait(nextEventWait) + locale(Locale.UK); country("GB") + workflow(VerifyRequest.Workflow.SMS_SMS_TTS) + } + } + } + + @Test + fun `psd2 request required parameters`() { + assertVerify(mapOf( + "number" to toNumber, "amount" to amount, "payee" to payee + )) { + psd2Verify(toNumber, amount, payee) + } + } + + @Test + fun `psd2 request all parameters`() { + val country = "DE" + assertVerify(mapOf( + "number" to toNumber, + "amount" to amount, + "payee" to payee, + "code_length" to codeLength, + "pin_expiry" to pinExpiry, + "next_event_wait" to nextEventWait, + "country" to country, + "lg" to "de-${country.lowercase()}", + "workflow_id" to 5 + + )) { + psd2Verify(toNumber, amount, payee) { + length(codeLength); pinExpiry(pinExpiry) + nextEventWait(nextEventWait); country(country) + locale(Locale.GERMANY) + workflow(Psd2Request.Workflow.SMS_TTS) + } + } + } + + @Test + fun `check verification code`() { + val expectedUrl = getBaseUri("check") + val expectedRequestParams = mapOf( + "request_id" to requestId, + "code" to pinCode + ) + mockPostQueryParams(expectedUrl, expectedRequestParams, + expectedResponseParams = mapOf( + "request_id" to requestId, + "event_id" to eventId, + "status" to 0, + "price" to price, + "currency" to currency, + "estimated_price_messages_sent" to estimatedPriceMessagesSent + ) + ) + + val parsedSuccess = existingRequest.check(pinCode) + assertNotNull(parsedSuccess) + assertNull(parsedSuccess.errorText) + assertEquals(requestId, parsedSuccess.requestId) + assertEquals(eventId, parsedSuccess.eventId) + assertEquals(VerifyStatus.OK, parsedSuccess.status) + assertEquals(currency, parsedSuccess.currency) + assertEquals(BigDecimal(estimatedPriceMessagesSent), parsedSuccess.estimatedPriceMessagesSent) + + val errorText = "The code inserted does not match the expected value" + mockPostQueryParams(expectedUrl, expectedRequestParams, + expectedResponseParams = mapOf( + "request_id" to requestId, + "status" to 16, + "error_text" to errorText + ) + ) + + val parsedFailure = existingRequest.check(pinCode) + assertNotNull(parsedFailure) + assertEquals(requestId, parsedFailure.requestId) + assertEquals(VerifyStatus.INVALID_CODE, parsedFailure.status) + assertEquals(errorText, parsedFailure.errorText) + } + + @Test + fun `cancel verification`() { + assertControl(VerifyControlCommand.CANCEL) + } + + @Test + fun `advance verification`() { + assertControl(VerifyControlCommand.TRIGGER_NEXT_EVENT) + } + + @Test + fun `search single request`() { + assertSearch(requestId) + } + + @Test + fun `search multiple requests`() { + assertSearch(requestId, textHexEncoded, testUuidStr) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/vonage/client/kt/VoiceTest.kt b/src/test/kotlin/com/vonage/client/kt/VoiceTest.kt index 81228bb..2d016b4 100644 --- a/src/test/kotlin/com/vonage/client/kt/VoiceTest.kt +++ b/src/test/kotlin/com/vonage/client/kt/VoiceTest.kt @@ -4,7 +4,6 @@ import com.vonage.client.common.HttpMethod import com.vonage.client.voice.* import com.vonage.client.voice.ncco.* import java.net.URI -import java.time.Instant import java.util.* import kotlin.test.Test import kotlin.test.*