From 816fd118de030defcf568452c026d65ed7439b63 Mon Sep 17 00:00:00 2001 From: Sina Madani Date: Fri, 5 Jul 2024 17:45:49 +0100 Subject: [PATCH] feat: Conversion API --- CHANGELOG.md | 2 + .../kotlin/com/vonage/client/kt/Conversion.kt | 20 +++++ .../kotlin/com/vonage/client/kt/Vonage.kt | 2 + .../com/vonage/client/kt/AbstractTest.kt | 81 ++++++++++--------- .../com/vonage/client/kt/ConversionTest.kt | 29 +++++++ .../com/vonage/client/kt/MessagesTest.kt | 5 +- .../kotlin/com/vonage/client/kt/SmsTest.kt | 5 +- .../kotlin/com/vonage/client/kt/VoiceTest.kt | 11 ++- 8 files changed, 105 insertions(+), 50 deletions(-) create mode 100644 src/main/kotlin/com/vonage/client/kt/Conversion.kt create mode 100644 src/test/kotlin/com/vonage/client/kt/ConversionTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index f671cbd..5ad7597 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added - SMS API +- Conversion API +- Redact API ### Removed - `parseInboundMessage` diff --git a/src/main/kotlin/com/vonage/client/kt/Conversion.kt b/src/main/kotlin/com/vonage/client/kt/Conversion.kt new file mode 100644 index 0000000..3659fd4 --- /dev/null +++ b/src/main/kotlin/com/vonage/client/kt/Conversion.kt @@ -0,0 +1,20 @@ +package com.vonage.client.kt + +import com.vonage.client.conversion.* +import java.time.Instant +import java.util.* + +class Conversion(private val conversionClient: ConversionClient) { + + private fun convert(type: ConversionRequest.Type, messageId: String, delivered: Boolean, + timestamp: Instant? = null) = + conversionClient.submitConversion(type, messageId, delivered, + if (timestamp == null) null else Date.from(timestamp) + ) + + fun convertSms(messageId: String, delivered: Boolean, timestamp: Instant = Instant.now()) = + convert(ConversionRequest.Type.SMS, messageId, delivered, timestamp) + + fun convertVoice(callId: String, delivered: Boolean, timestamp: Instant = Instant.now()) = + convert(ConversionRequest.Type.VOICE, callId, delivered, timestamp) +} diff --git a/src/main/kotlin/com/vonage/client/kt/Vonage.kt b/src/main/kotlin/com/vonage/client/kt/Vonage.kt index 5802158..24b6224 100644 --- a/src/main/kotlin/com/vonage/client/kt/Vonage.kt +++ b/src/main/kotlin/com/vonage/client/kt/Vonage.kt @@ -9,6 +9,8 @@ class Vonage(init: VonageClient.Builder.() -> Unit) { val verify = Verify(vonageClient.verify2Client) val voice = Voice(vonageClient.voiceClient) val sms = Sms(vonageClient.smsClient) + val conversion = Conversion(vonageClient.conversionClient) + val redact = Redact(vonageClient.redactClient) } 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 b5f234b..0ba3744 100644 --- a/src/test/kotlin/com/vonage/client/kt/AbstractTest.kt +++ b/src/test/kotlin/com/vonage/client/kt/AbstractTest.kt @@ -3,6 +3,7 @@ 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.* import com.github.tomakehurst.wiremock.common.ConsoleNotifier import com.github.tomakehurst.wiremock.core.WireMockConfiguration.options import com.marcinziolo.kotlin.wiremock.* @@ -14,6 +15,7 @@ import org.junit.jupiter.api.assertThrows import java.net.URI import java.net.URLEncoder import java.nio.charset.StandardCharsets +import java.time.Instant import java.util.* import kotlin.test.assertEquals @@ -24,16 +26,27 @@ abstract class AbstractTest { private val signatureSecret = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQR" private val apiKeySecretEncoded = "YTFiMmMzZDQ6MTIzNDU2Nzg5MGFiY2RlZg==" private val privateKeyPath = "src/test/resources/com/vonage/client/kt/application_key" + protected val apiSecretName = "api_secret" + protected val apiKeyName = "api_key" + protected val signatureSecretName = "sig" protected val testUuidStr = "aaaaaaaa-bbbb-4ccc-8ddd-0123456789ab" protected val testUuid = UUID.fromString(testUuidStr) protected val toNumber = "447712345689" protected val altNumber = "447700900001" protected val text = "Hello, World!" protected val textHexEncoded = "48656c6c6f2c20576f726c6421" + protected val smsMessageId = "0C000000217B7F02" + protected val callIdStr = "63f61863-4a51-4f6b-86e1-46edebcf9356" protected val networkCode = "65512" - protected val startTime = "2020-09-17T12:34:56Z" - protected val endTime = "2021-09-17T12:35:28Z" - protected val timestamp = "2016-11-14T07:45:14Z" + protected val startTimeStr = "2020-09-17T12:34:56Z" + protected val startTime = Instant.parse(startTimeStr) + protected val endTimeStr = "2021-09-17T12:35:28Z" + protected val endTime = Instant.parse(endTimeStr) + protected val timestampStr = "2016-11-14T07:45:14Z" + protected val timestampDateStr = "2016-11-14 07:45:14" + protected val timestamp = Instant.parse(timestampStr) + protected val timestamp2Str = "2020-01-29T14:08:30.201Z" + protected val timestamp2 = Instant.parse(timestamp2Str) private val port = 8081 val wiremock: WireMockServer = WireMockServer( @@ -73,6 +86,15 @@ abstract class AbstractTest { JWT, API_KEY_SECRET_HEADER, API_KEY_SECRET_QUERY_PARAMS, API_KEY_SIGNATURE_SECRET } + private fun HttpMethod.toWireMockMethod(): Method = when (this) { + HttpMethod.GET -> WireMock::get + HttpMethod.POST -> WireMock::post + HttpMethod.PUT -> WireMock::put + HttpMethod.PATCH -> WireMock::patch + HttpMethod.DELETE -> WireMock::delete + else -> throw IllegalArgumentException("Unhandled HTTP method: $this") + } + protected fun Map.toFormEncodedString(): String { val utf8 = StandardCharsets.UTF_8.toString() return entries.joinToString("&") { (key, value) -> @@ -80,6 +102,18 @@ abstract class AbstractTest { } } + protected fun mockPostQueryParams(expectedUrl: String, expectedRequestParams: Map, + status: Int = 200, expectedResponseParams: Map? = null) { + val stub = post(urlPathEqualTo(expectedUrl)) + expectedRequestParams.forEach {(k, v) -> stub.withFormParam(k, equalTo(v.toString()))} + val response = aResponse().withStatus(status) + if (expectedResponseParams != null) { + response.withBody(expectedResponseParams.toFormEncodedString()) + } + stub.willReturn(response) + wiremock.stubFor(stub) + } + protected fun mockRequest( httpMethod: HttpMethod, expectedUrl: String, @@ -96,13 +130,9 @@ abstract class AbstractTest { if (accept != null) { headers contains "Accept" equalTo accept.mime } - val formEncodedParams = if ( - contentType == ContentType.FORM_URLENCODED && httpMethod != HttpMethod.GET - ) mutableMapOf() else null if (authType != null) { val authHeaderName = "Authorization" - val apiKeyName = "api_key" when (authType) { AuthType.JWT -> headers contains authHeaderName like "Bearer eyJ0eXBlIjoiSldUIiwiYWxnIjoiUlMyNTYifQ(\\..+){2}" @@ -111,27 +141,13 @@ abstract class AbstractTest { headers contains authHeaderName equalTo "Basic $apiKeySecretEncoded" AuthType.API_KEY_SECRET_QUERY_PARAMS -> { - val apiSecretName = "api_secret" - if (formEncodedParams != null) { - formEncodedParams[apiKeyName] = apiKey - formEncodedParams[apiSecretName] = apiSecret - } - else { - queryParams contains apiKeyName equalTo apiKey - queryParams contains apiSecretName equalTo apiSecret - } + queryParams contains apiKeyName equalTo apiKey + queryParams contains apiSecretName equalTo apiSecret } AuthType.API_KEY_SIGNATURE_SECRET -> { - val signatureName = "sig" - if (formEncodedParams != null) { - formEncodedParams[apiKeyName] = apiKey - formEncodedParams[signatureName] = signatureSecret - } - else { - queryParams contains apiKeyName equalTo apiKey - queryParams contains signatureName equalTo signatureSecret - } + queryParams contains apiKeyName equalTo apiKey + queryParams contains signatureSecretName equalTo signatureSecret } } } @@ -139,22 +155,11 @@ abstract class AbstractTest { ContentType.APPLICATION_JSON -> { body equalTo ObjectMapper().writeValueAsString(expectedParams) } - ContentType.FORM_URLENCODED -> { - formEncodedParams?.putAll(expectedParams) - } else -> { - expectedParams.forEach { (k, v) -> queryParams contains k equalTo v.toString() } + expectedParams.forEach {(k, v) -> queryParams contains k equalTo v.toString()} } } - // TODO: assertion on the body once it's supported by the WireMock DSL - }, when (httpMethod) { - HttpMethod.GET -> WireMock::get - HttpMethod.POST -> WireMock::post - HttpMethod.PUT -> WireMock::put - HttpMethod.PATCH -> WireMock::patch - HttpMethod.DELETE -> WireMock::delete - else -> throw IllegalArgumentException("Unhandled HTTP method: $httpMethod") - }) + }, httpMethod.toWireMockMethod()) private fun mockP(requestMethod: HttpMethod, expectedUrl: String, expectedRequestParams: Map? = null, diff --git a/src/test/kotlin/com/vonage/client/kt/ConversionTest.kt b/src/test/kotlin/com/vonage/client/kt/ConversionTest.kt new file mode 100644 index 0000000..1fa7df0 --- /dev/null +++ b/src/test/kotlin/com/vonage/client/kt/ConversionTest.kt @@ -0,0 +1,29 @@ +package com.vonage.client.kt + +import kotlin.test.* + +class ConversionTest : AbstractTest() { + private val conversionClient = vonage.conversion + + private fun mockSuccess(id: String, endpoint: String, delivered: Boolean) { + mockPostQueryParams("/conversions/$endpoint", mapOf( + "message-id" to id, + "delivered" to delivered, + "timestamp" to timestampDateStr + )) + } + + @Test + fun `submit sms conversion`() { + val delivered = true + mockSuccess(smsMessageId, "sms", delivered) + conversionClient.convertSms(smsMessageId, delivered, timestamp) + } + + @Test + fun `submit voice conversion`() { + val delivered = false + mockSuccess(callIdStr, "voice", delivered) + conversionClient.convertVoice(callIdStr, delivered, timestamp) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/vonage/client/kt/MessagesTest.kt b/src/test/kotlin/com/vonage/client/kt/MessagesTest.kt index c9aea46..baa2a37 100644 --- a/src/test/kotlin/com/vonage/client/kt/MessagesTest.kt +++ b/src/test/kotlin/com/vonage/client/kt/MessagesTest.kt @@ -417,7 +417,6 @@ class MessagesTest : AbstractTest() { @Test fun `parse inbound MMS image`() { - val timestampStr = "2020-01-29T14:08:30.201Z" val networkCode = "54123" val parsed = InboundMessage.fromJson( """ @@ -426,7 +425,7 @@ class MessagesTest : AbstractTest() { "message_uuid": "$messageUuid", "to": "$toNumber", "from": "$altNumber", - "timestamp": "$timestampStr", + "timestamp": "$timestamp2Str", "origin": { "network_code": "$networkCode" }, @@ -444,7 +443,7 @@ class MessagesTest : AbstractTest() { assertEquals(messageUuid, parsed.messageUuid) assertEquals(toNumber, parsed.to) assertEquals(altNumber, parsed.from) - assertEquals(Instant.parse(timestampStr), parsed.timestamp) + assertEquals(timestamp2, parsed.timestamp) assertEquals(networkCode, parsed.networkCode) assertEquals(MessageType.IMAGE, parsed.messageType) assertEquals(URI.create(imageUrl), parsed.imageUrl) diff --git a/src/test/kotlin/com/vonage/client/kt/SmsTest.kt b/src/test/kotlin/com/vonage/client/kt/SmsTest.kt index 143c7b7..89101af 100644 --- a/src/test/kotlin/com/vonage/client/kt/SmsTest.kt +++ b/src/test/kotlin/com/vonage/client/kt/SmsTest.kt @@ -25,7 +25,6 @@ class SmsTest : AbstractTest() { private fun testSuccessSingleMessage(requestParams: Map, invocation: () -> List) { - val messageId = "0C000000217B7F02" val remainingBalance = "15.53590000" val messagePrice = "0.03330000" val network = "23410" @@ -36,7 +35,7 @@ class SmsTest : AbstractTest() { "messages" to listOf( mapOf( "to" to toNumber, - "message-id" to messageId, + "message-id" to smsMessageId, "status" to "0", "remaining-balance" to remainingBalance, "message-price" to messagePrice, @@ -54,7 +53,7 @@ class SmsTest : AbstractTest() { val first = response.first() assertNotNull(first) assertEquals(toNumber, first.to) - assertEquals(messageId, first.id) + assertEquals(smsMessageId, first.id) assertEquals(OK, first.status) assertEquals(BigDecimal(remainingBalance), first.remainingBalance) assertEquals(BigDecimal(messagePrice), first.messagePrice) diff --git a/src/test/kotlin/com/vonage/client/kt/VoiceTest.kt b/src/test/kotlin/com/vonage/client/kt/VoiceTest.kt index fad84a6..81228bb 100644 --- a/src/test/kotlin/com/vonage/client/kt/VoiceTest.kt +++ b/src/test/kotlin/com/vonage/client/kt/VoiceTest.kt @@ -12,7 +12,6 @@ import kotlin.test.* class VoiceTest : AbstractTest() { private val voiceClient = vonage.voice private val callsBaseUrl = "/v1/calls" - private val callIdStr = "63f61863-4a51-4f6b-86e1-46edebcf9356" private val callUrl = "$callsBaseUrl/$callIdStr" private val callObj = voiceClient.call(UUID.fromString(callIdStr)) private val conversationId = "CON-f972836a-550f-45fa-956c-12a2ab5b7d22" @@ -64,8 +63,8 @@ class VoiceTest : AbstractTest() { "rate" to rate, "price" to price, "duration" to duration, - "start_time" to startTime, - "end_time" to endTime, + "start_time" to startTimeStr, + "end_time" to endTimeStr, "network" to networkCode ) private val listCallsResponse = mapOf( @@ -98,8 +97,8 @@ class VoiceTest : AbstractTest() { assertEquals(rate, callInfo.rate) assertEquals(price, callInfo.price) assertEquals(duration, callInfo.duration) - assertEquals(Instant.parse(startTime), callInfo.startTime.toInstant()) - assertEquals(Instant.parse(endTime), callInfo.endTime.toInstant()) + assertEquals(startTime, callInfo.startTime.toInstant()) + assertEquals(endTime, callInfo.endTime.toInstant()) assertEquals(networkCode, callInfo.network) } @@ -371,7 +370,7 @@ class VoiceTest : AbstractTest() { val callsPage = voiceClient.listCalls { status(CallStatus.UNANSWERED) - dateStart(startTime); dateEnd(endTime) + dateStart(startTimeStr); dateEnd(endTimeStr) pageSize(pageSize); recordIndex(recordIndex) order(CallOrder.DESCENDING); conversationUuid(conversationId) }