Skip to content

Commit

Permalink
feat: SMS API
Browse files Browse the repository at this point in the history
  • Loading branch information
SMadani committed Jul 4, 2024
1 parent 9644637 commit fb4e1ca
Show file tree
Hide file tree
Showing 7 changed files with 342 additions and 53 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ 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.3.0] - 2024-07-??

### Added
- SMS API

### Removed
- `parseInboundMessage`

## [0.2.0] - 2024-07-02

### Added
Expand Down
45 changes: 45 additions & 0 deletions src/main/kotlin/com/vonage/client/kt/SMS.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.vonage.client.kt

import com.vonage.client.messages.InboundMessage
import com.vonage.client.sms.*
import com.vonage.client.sms.messages.*

class SMS(private val smsClient: SmsClient) {

private fun send(msgObj: Message, statusReport: Boolean?, ttl: Int?,
messageClass: Message.MessageClass?, clientRef: String?,
contentId: String?, entityId: String?,
callbackUrl: String?): List<SmsSubmissionResponseMessage> {
if (statusReport != null) msgObj.statusReportRequired = statusReport
msgObj.timeToLive = ttl?.toLong()
msgObj.messageClass = messageClass
if (clientRef != null) msgObj.clientReference = clientRef
msgObj.contentId = contentId
msgObj.entityId = entityId
msgObj.callbackUrl = callbackUrl
return smsClient.submitMessage(msgObj).messages
}

fun sendText(from: String, to: String, message: String, unicode: Boolean = false,
statusReport: Boolean? = null, ttl: Int? = null,
messageClass: Message.MessageClass? = null, clientRef: String? = null,
contentId: String? = null, entityId: String? = null,
callbackUrl: String? = null): List<SmsSubmissionResponseMessage> =
send(
TextMessage(from, to, message, unicode),
statusReport, ttl, messageClass, clientRef, contentId, entityId, callbackUrl
)

fun sendBinary(from: String, to: String, body: ByteArray, udh: ByteArray,
protocolId: Int? = null, statusReport: Boolean? = null, ttl: Int? = null,
messageClass: Message.MessageClass? = null, clientRef: String? = null,
contentId: String? = null, entityId: String? = null, callbackUrl: String? = null
): List<SmsSubmissionResponseMessage> {
val msgObj = BinaryMessage(from, to, body, udh)
if (protocolId != null) msgObj.protocolId = protocolId
return send(msgObj, statusReport, ttl, messageClass, clientRef, contentId, entityId, callbackUrl)
}

fun wasSuccessfullySent(response: List<SmsSubmissionResponseMessage>): Boolean =
response.all { ssrm -> ssrm.status == MessageStatus.OK }
}
3 changes: 2 additions & 1 deletion src/main/kotlin/com/vonage/client/kt/Vonage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ package com.vonage.client.kt
import com.vonage.client.HttpConfig
import com.vonage.client.VonageClient

class Vonage constructor(init: VonageClient.Builder.() -> Unit) {
class Vonage(init: VonageClient.Builder.() -> Unit) {
private val vonageClient : VonageClient = VonageClient.builder().apply(init).build();
val messages = Messages(vonageClient.messagesClient)
val verify = Verify(vonageClient.verify2Client)
val voice = Voice(vonageClient.voiceClient)
val sms = SMS(vonageClient.smsClient)
}

fun VonageClient.Builder.authFromEnv(): VonageClient.Builder {
Expand Down
125 changes: 75 additions & 50 deletions src/test/kotlin/com/vonage/client/kt/AbstractTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.assertThrows
import java.net.URI
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.util.*
import kotlin.test.assertEquals

Expand All @@ -27,6 +29,7 @@ abstract class AbstractTest {
protected val toNumber = "447712345689"
protected val altNumber = "447700900001"
protected val text = "Hello, World!"
protected val textHexEncoded = "48656c6c6f2c20576f726c6421"
protected val networkCode = "65512"
protected val startTime = "2020-09-17T12:34:56Z"
protected val endTime = "2021-09-17T12:35:28Z"
Expand All @@ -38,9 +41,8 @@ abstract class AbstractTest {
)

val vonage = Vonage {
apiKey(apiKey); apiSecret(apiSecret);
signatureSecret(signatureSecret); applicationId(applicationId)
privateKeyPath(privateKeyPath)
apiKey(apiKey); apiSecret(apiSecret); signatureSecret(signatureSecret);
applicationId(applicationId); privateKeyPath(privateKeyPath)
httpConfig {
baseUri("http://localhost:$port")
}
Expand Down Expand Up @@ -68,7 +70,14 @@ abstract class AbstractTest {
}

protected enum class AuthType {
JWT, API_KEY_SECRET_HEADER, API_KEY_SECRET_QUERY_PARAMS
JWT, API_KEY_SECRET_HEADER, API_KEY_SECRET_QUERY_PARAMS, API_KEY_SIGNATURE_SECRET
}

protected fun Map<String, Any>.toFormEncodedString(): String {
val utf8 = StandardCharsets.UTF_8.toString()
return entries.joinToString("&") { (key, value) ->
"${URLEncoder.encode(key, utf8)}=${URLEncoder.encode(value.toString(), utf8)}"
}
}

protected fun mockRequest(
Expand All @@ -79,38 +88,65 @@ abstract class AbstractTest {
authType: AuthType? = null,
expectedParams: Map<String, Any>? = null): BuildingStep =
wiremock.requestServerBuilderStep({
url equalTo expectedUrl
urlPath equalTo expectedUrl
headers contains "User-Agent" like "vonage-java-sdk\\/.+ java\\/.+"
if (contentType != null) {
headers contains "Content-Type" equalTo contentType.mime
}
if (accept != null) {
headers contains "Accept" equalTo accept.mime
}
val formEncodedParams = if (
contentType == ContentType.FORM_URLENCODED && httpMethod != HttpMethod.GET
) mutableMapOf<String, Any>() else null

if (authType != null) {
val authHeaderName = "Authorization"
val apiKeyName = "api_key"
when (authType) {
AuthType.JWT -> headers contains authHeaderName like
"Bearer eyJ0eXBlIjoiSldUIiwiYWxnIjoiUlMyNTYifQ(\\..+){2}"
"Bearer eyJ0eXBlIjoiSldUIiwiYWxnIjoiUlMyNTYifQ(\\..+){2}"

AuthType.API_KEY_SECRET_HEADER ->
headers contains authHeaderName equalTo "Basic $apiKeySecretEncoded"

AuthType.API_KEY_SECRET_QUERY_PARAMS -> {
headers contains "api_key" equalTo apiKey
headers contains "api_secret" equalTo apiSecret
val apiSecretName = "api_secret"
if (formEncodedParams != null) {
formEncodedParams[apiKeyName] = apiKey
formEncodedParams[apiSecretName] = apiSecret
}
else {
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
}
}
}
}
if (contentType != null) {
headers contains "Content-Type" equalTo contentType.mime
}
if (accept != null) {
headers contains "Accept" equalTo accept.mime
}
if (expectedParams != null) {
if (contentType == ContentType.APPLICATION_JSON) {
if (expectedParams != null) when (contentType) {
ContentType.APPLICATION_JSON -> {
body equalTo ObjectMapper().writeValueAsString(expectedParams)
}
else {
url like "$expectedUrl\\?.+"
expectedParams.forEach { (k, v) ->
queryParams contains k equalTo v.toString()
}
ContentType.FORM_URLENCODED -> {
formEncodedParams?.putAll(expectedParams)
}
else -> {
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
Expand All @@ -123,46 +159,35 @@ abstract class AbstractTest {
private fun mockP(requestMethod: HttpMethod, expectedUrl: String,
expectedRequestParams: Map<String, Any>? = null,
status: Int = 200, authType: AuthType? = AuthType.JWT,
expectedResponseParams: Map<String, Any>? = null) =
contentType: ContentType?, expectedResponseParams: Map<String, Any>? = null) =

mockRequest(requestMethod, expectedUrl,
contentType = if (expectedRequestParams != null) ContentType.APPLICATION_JSON else null,
mockRequest(requestMethod, expectedUrl, if (expectedRequestParams != null) contentType else null,
accept = if (expectedResponseParams != null && status < 400) ContentType.APPLICATION_JSON else null,
authType = authType, expectedRequestParams
).mockReturn(status, expectedResponseParams)

protected fun mockPost(expectedUrl: String,
expectedRequestParams: Map<String, Any>? = null,
status: Int = 200,
authType: AuthType? = AuthType.JWT,
expectedResponseParams: Map<String, Any>? = null) =
mockP(HttpMethod.POST, expectedUrl, expectedRequestParams, status, authType, expectedResponseParams)

protected fun mockPut(expectedUrl: String,
expectedRequestParams: Map<String, Any>? = null,
status: Int = 200,
authType: AuthType? = AuthType.JWT,
expectedResponseParams: Map<String, Any>? = null) =
mockP(HttpMethod.PUT, expectedUrl, expectedRequestParams, status, authType, expectedResponseParams)

protected fun mockPatch(expectedUrl: String,
expectedRequestParams: Map<String, Any>? = null,
status: Int = 200,
authType: AuthType? = AuthType.JWT,
expectedResponseParams: Map<String, Any>? = null) =
mockP(HttpMethod.PUT, expectedUrl, expectedRequestParams, status, authType, expectedResponseParams)
protected fun mockPost(expectedUrl: String, expectedRequestParams: Map<String, Any>? = null,
status: Int = 200, contentType: ContentType? = ContentType.APPLICATION_JSON,
authType: AuthType? = AuthType.JWT, expectedResponseParams: Map<String, Any>? = null) =
mockP(HttpMethod.POST, expectedUrl, expectedRequestParams, status, authType, contentType, expectedResponseParams)

protected fun mockPut(expectedUrl: String, expectedRequestParams: Map<String, Any>? = null,
status: Int = 200, contentType: ContentType? = ContentType.APPLICATION_JSON,
authType: AuthType? = AuthType.JWT, expectedResponseParams: Map<String, Any>? = null) =
mockP(HttpMethod.PUT, expectedUrl, expectedRequestParams, status, authType, contentType, expectedResponseParams)

protected fun mockPatch(expectedUrl: String, expectedRequestParams: Map<String, Any>? = null,
status: Int = 200, contentType: ContentType? = ContentType.APPLICATION_JSON,
authType: AuthType? = AuthType.JWT, expectedResponseParams: Map<String, Any>? = null) =
mockP(HttpMethod.PUT, expectedUrl, expectedRequestParams, status, authType, contentType, expectedResponseParams)

protected fun mockDelete(expectedUrl: String, authType: AuthType? = null,
expectedResponseParams: Map<String, Any>? = null) =
mockRequest(HttpMethod.DELETE, expectedUrl, authType = authType)
.mockReturn(if (expectedResponseParams == null) 204 else 200, expectedResponseParams)

protected fun mockGet(expectedUrl: String,
expectedQueryParams: Map<String, Any>? = null,
status: Int = 200,
authType: AuthType? = AuthType.JWT,
expectedResponseParams: Map<String, Any>) =

protected fun mockGet(expectedUrl: String, expectedQueryParams: Map<String, Any>? = null, status: Int = 200,
authType: AuthType? = AuthType.JWT, expectedResponseParams: Map<String, Any>) =
mockRequest(HttpMethod.GET, expectedUrl, accept = ContentType.APPLICATION_JSON, authType = authType,
expectedParams = expectedQueryParams).mockReturn(status, expectedResponseParams)

Expand Down
2 changes: 1 addition & 1 deletion src/test/kotlin/com/vonage/client/kt/MessagesTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ class MessagesTest : AbstractTest() {
fun `parse inbound MMS image`() {
val timestampStr = "2020-01-29T14:08:30.201Z"
val networkCode = "54123"
val parsed = parseInboundMessage(
val parsed = InboundMessage.fromJson(
"""
{
"channel": "$mmsChannel",
Expand Down
Loading

0 comments on commit fb4e1ca

Please sign in to comment.