From 4b63783457840f1bb0a772ba81976e33c1a0085d Mon Sep 17 00:00:00 2001 From: fatemeh imanipour Date: Mon, 6 Jul 2026 18:49:35 +0330 Subject: [PATCH] Implement new sms providers to support international phone numbers --- .../opex/otp/app/data/SMSProviderType.kt | 7 ++ .../nilin/opex/otp/app/model/SMSProvider.kt | 19 +++++ .../opex/otp/app/model/SMSProviderRoute.kt | 13 +++ .../opex/otp/app/proxy/KaveNegarProxy.kt | 25 +++--- .../nilin/opex/otp/app/proxy/SMSProvider.kt | 13 +++ .../co/nilin/opex/otp/app/proxy/SMSTOProxy.kt | 62 ++++++++++++++ .../nilin/opex/otp/app/proxy/TWILIOProxy.kt | 73 +++++++++++++++++ .../app/repository/SMSProviderRepository.kt | 14 ++++ .../repository/SMSProviderRouteRepository.kt | 11 +++ .../opex/otp/app/service/SMSProviderRouter.kt | 34 ++++++++ .../opex/otp/app/service/message/SMSSender.kt | 9 ++- .../src/main/resources/application.yml | 4 +- otp/otp-app/src/main/resources/schema.sql | 21 ++++- .../app/conttroller/SMSProviderRouterTest.kt | 80 +++++++++++++++++++ .../opex/otp/app/conttroller/SMSTOProxyIT.kt | 51 ++++++++++++ .../opex/otp/app/conttroller/TWILIOProxyIT.kt | 60 ++++++++++++++ .../src/test/resources/application.yml | 4 +- otp/pom.xml | 4 - 18 files changed, 479 insertions(+), 25 deletions(-) create mode 100644 otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/SMSProviderType.kt create mode 100644 otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/model/SMSProvider.kt create mode 100644 otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/model/SMSProviderRoute.kt create mode 100644 otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/proxy/SMSProvider.kt create mode 100644 otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/proxy/SMSTOProxy.kt create mode 100644 otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/proxy/TWILIOProxy.kt create mode 100644 otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/repository/SMSProviderRepository.kt create mode 100644 otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/repository/SMSProviderRouteRepository.kt create mode 100644 otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/service/SMSProviderRouter.kt create mode 100644 otp/otp-app/src/test/kotlin/co/nilin/opex/otp/app/conttroller/SMSProviderRouterTest.kt create mode 100644 otp/otp-app/src/test/kotlin/co/nilin/opex/otp/app/conttroller/SMSTOProxyIT.kt create mode 100644 otp/otp-app/src/test/kotlin/co/nilin/opex/otp/app/conttroller/TWILIOProxyIT.kt diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/SMSProviderType.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/SMSProviderType.kt new file mode 100644 index 000000000..62c092aa1 --- /dev/null +++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/data/SMSProviderType.kt @@ -0,0 +1,7 @@ +package co.nilin.opex.otp.app.data + +enum class SMSProviderType { + KAVENEGAR, + TWILIO, + SMSTO +} \ No newline at end of file diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/model/SMSProvider.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/model/SMSProvider.kt new file mode 100644 index 000000000..e4e2bc815 --- /dev/null +++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/model/SMSProvider.kt @@ -0,0 +1,19 @@ +package co.nilin.opex.otp.app.model + +import org.springframework.data.annotation.Id +import org.springframework.data.relational.core.mapping.Table + + +@Table(name = "sms_provider") +class SMSProvider( + @Id + val id: String, + val enabled: Boolean, + val baseUrl: String, + val apiKey: String?, + val template: String?, + val username: String?, + val password: String?, + val sender: String?, + val extraConfig: String? +) \ No newline at end of file diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/model/SMSProviderRoute.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/model/SMSProviderRoute.kt new file mode 100644 index 000000000..e9a46f8e8 --- /dev/null +++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/model/SMSProviderRoute.kt @@ -0,0 +1,13 @@ +package co.nilin.opex.otp.app.model + +import org.springframework.data.annotation.Id +import org.springframework.data.relational.core.mapping.Table + +@Table(name = "sms_provider_route") +class SMSProviderRoute( + @Id + val id: Long? = null, + val prefix: String, + val provider: String, + val enabled: Boolean = true +) \ No newline at end of file diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/proxy/KaveNegarProxy.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/proxy/KaveNegarProxy.kt index fcb27584c..8f1d56b24 100644 --- a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/proxy/KaveNegarProxy.kt +++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/proxy/KaveNegarProxy.kt @@ -1,6 +1,9 @@ package co.nilin.opex.otp.app.proxy +import co.nilin.opex.common.OpexError import co.nilin.opex.common.utils.LoggerDelegate +import co.nilin.opex.otp.app.data.SMSProviderType +import co.nilin.opex.otp.app.repository.SMSProviderRepository import kotlinx.coroutines.reactor.awaitSingleOrNull import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component @@ -10,22 +13,22 @@ import org.springframework.web.util.UriComponentsBuilder @Component class KaveNegarProxy( - @Value("\${otp.sms.provider.url}") - private val url: String, - @Value("\${otp.sms.provider.api-key}") - private val apiKey: String, - @Value("\${otp.sms.provider.template}") - private val template: String, - private val webClient: WebClient -) { + private val webClient: WebClient, + private val smsProviderRepository: SMSProviderRepository, +) : SMSProvider { + + override val type = SMSProviderType.KAVENEGAR + private val logger by LoggerDelegate() - private val baseUrl = "${url}/$apiKey/" - suspend fun send(receiver: String, message: String, sender: String? = null, type: String? = null): Boolean { + override suspend fun send(receiver: String, message: String): Boolean { + val config = smsProviderRepository.findById(type.name) ?: throw OpexError.UnableToSendOTP.exception() + val baseUrl = "${config.baseUrl}/${config.apiKey}/" + val uri = UriComponentsBuilder.fromUriString("$baseUrl/verify/lookup.json") .queryParam("receptor", receiver) - .queryParam("template", template) + .queryParam("template", config.template) .queryParam("token", message) .build().toUri() diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/proxy/SMSProvider.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/proxy/SMSProvider.kt new file mode 100644 index 000000000..69afef42e --- /dev/null +++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/proxy/SMSProvider.kt @@ -0,0 +1,13 @@ +package co.nilin.opex.otp.app.proxy + +import co.nilin.opex.otp.app.data.SMSProviderType + +interface SMSProvider { + + val type: SMSProviderType + + suspend fun send( + receiver: String, + message: String, + ): Boolean +} \ No newline at end of file diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/proxy/SMSTOProxy.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/proxy/SMSTOProxy.kt new file mode 100644 index 000000000..e9d928df5 --- /dev/null +++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/proxy/SMSTOProxy.kt @@ -0,0 +1,62 @@ +package co.nilin.opex.otp.app.proxy + +import co.nilin.opex.common.OpexError +import co.nilin.opex.common.utils.LoggerDelegate +import co.nilin.opex.otp.app.data.SMSProviderType +import co.nilin.opex.otp.app.repository.SMSProviderRepository +import kotlinx.coroutines.reactor.awaitSingleOrNull +import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.bodyToMono + +@Component +class SMSTOProxy( + private val webClient: WebClient, + private val smsProviderRepository: SMSProviderRepository, +) : SMSProvider { + + override val type = SMSProviderType.SMSTO + + private val logger by LoggerDelegate() + + override suspend fun send( + receiver: String, + message: String, + ): Boolean { + + val config = smsProviderRepository.findById(type.name) + ?: throw OpexError.UnableToSendOTP.exception() + + val request = SMSRequest( + to = receiver, + message = message, + sender_id = config.sender + ) + + return try { + val response = webClient.post() + .uri("${config.baseUrl}/sms/send") + .header(HttpHeaders.AUTHORIZATION, "Bearer ${config.apiKey}") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(request) + .retrieve() + .onStatus({ it.isError }) { it.createException() } + .bodyToMono() + .awaitSingleOrNull() + + logger.debug("Message sent to receiver $receiver.\n$response") + true + } catch (e: Exception) { + logger.error("Failed to send SMS", e) + false + } + } + + data class SMSRequest( + val to: String, + val message: String, + val sender_id: String? = null, + ) +} \ No newline at end of file diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/proxy/TWILIOProxy.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/proxy/TWILIOProxy.kt new file mode 100644 index 000000000..ed9c00fb7 --- /dev/null +++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/proxy/TWILIOProxy.kt @@ -0,0 +1,73 @@ +package co.nilin.opex.otp.app.proxy + +import co.nilin.opex.common.OpexError +import co.nilin.opex.common.utils.LoggerDelegate +import co.nilin.opex.otp.app.data.SMSProviderType +import co.nilin.opex.otp.app.repository.SMSProviderRepository +import kotlinx.coroutines.reactor.awaitSingleOrNull +import org.springframework.http.MediaType +import org.springframework.stereotype.Component +import org.springframework.util.LinkedMultiValueMap +import org.springframework.web.reactive.function.BodyInserters +import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.WebClientResponseException +import org.springframework.web.reactive.function.client.bodyToMono + +@Component +class TWILIOProxy( + private val webClient: WebClient, + private val smsProviderRepository: SMSProviderRepository, +) : SMSProvider { + + override val type = SMSProviderType.TWILIO + + private val logger by LoggerDelegate() + + override suspend fun send( + receiver: String, + message: String, + ): Boolean { + + val config = smsProviderRepository.findById(type.name) + ?: throw OpexError.UnableToSendOTP.exception() + + val accountSid = config.username + ?: throw IllegalStateException("Twilio Account SID is not configured") + + val authToken = config.apiKey + ?: throw IllegalStateException("Twilio Auth Token is not configured") + + val sender = config.sender + ?: throw IllegalStateException("Twilio sender number is not configured") + + val formData = LinkedMultiValueMap().apply { + add("To", receiver) + add("From", sender) + add("Body", message) + } + + return try { + val response = webClient.post() + .uri("${config.baseUrl}/2010-04-01/Accounts/$accountSid/Messages.json") + .headers { + it.setBasicAuth(accountSid, authToken) + } + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(BodyInserters.fromFormData(formData)) + .retrieve() + .onStatus({ it.isError }) { it.createException() } + .bodyToMono() + .awaitSingleOrNull() + + logger.debug("Message sent to receiver $receiver.\n$response") + true + } catch (e: WebClientResponseException) { + logger.error( + "Twilio error: ${e.statusCode}\n${e.responseBodyAsString}", + e + ) + logger.error("Failed to send SMS", e) + false + } + } +} \ No newline at end of file diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/repository/SMSProviderRepository.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/repository/SMSProviderRepository.kt new file mode 100644 index 000000000..74dd1e5b5 --- /dev/null +++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/repository/SMSProviderRepository.kt @@ -0,0 +1,14 @@ +package co.nilin.opex.otp.app.repository + +import co.nilin.opex.otp.app.model.SMSProvider +import co.nilin.opex.otp.app.model.SMSProviderRoute +import co.nilin.opex.otp.app.model.TOTPConfig +import org.springframework.data.r2dbc.repository.Query +import org.springframework.data.repository.kotlin.CoroutineCrudRepository +import org.springframework.stereotype.Repository + +@Repository +interface SMSProviderRepository : CoroutineCrudRepository { + @Query("select * from sms_provider where id=:type") + suspend fun findConfig(type: String): SMSProvider +} \ No newline at end of file diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/repository/SMSProviderRouteRepository.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/repository/SMSProviderRouteRepository.kt new file mode 100644 index 000000000..2a51d6e6d --- /dev/null +++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/repository/SMSProviderRouteRepository.kt @@ -0,0 +1,11 @@ +package co.nilin.opex.otp.app.repository + +import co.nilin.opex.otp.app.model.SMSProviderRoute +import org.springframework.data.repository.kotlin.CoroutineCrudRepository +import org.springframework.stereotype.Repository + +@Repository +interface SMSProviderRouteRepository : CoroutineCrudRepository { + + fun findAllByEnabledTrue(): List +} \ No newline at end of file diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/service/SMSProviderRouter.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/service/SMSProviderRouter.kt new file mode 100644 index 000000000..143c41e74 --- /dev/null +++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/service/SMSProviderRouter.kt @@ -0,0 +1,34 @@ +package co.nilin.opex.otp.app.service; + +import co.nilin.opex.otp.app.data.SMSProviderType; +import co.nilin.opex.otp.app.proxy.SMSProvider; +import co.nilin.opex.otp.app.repository.SMSProviderRouteRepository +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component; + +@Component +class SMSProviderRouter( + providers: List, + private val routeRepository: SMSProviderRouteRepository, + @Value("\${otp.sms.provider.default}") + private val defaultSmsProvider: SMSProviderType?, +) { + + private val providerMap: Map = providers.associateBy { it.type } + + suspend fun getProvider(receiver: String): SMSProvider { + val routes = routeRepository.findAllByEnabledTrue() + + val providerType = routes + .sortedByDescending { it.prefix.length } + .firstOrNull { receiver.startsWith(it.prefix) } + ?.provider + ?.let(SMSProviderType::valueOf) + ?: defaultSmsProvider + + return providerMap[providerType] + ?: throw IllegalStateException( + "SMS provider $providerType is configured but no implementation was found." + ) + } +} \ No newline at end of file diff --git a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/service/message/SMSSender.kt b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/service/message/SMSSender.kt index 9132f1527..c1365d8f4 100644 --- a/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/service/message/SMSSender.kt +++ b/otp/otp-app/src/main/kotlin/co/nilin/opex/otp/app/service/message/SMSSender.kt @@ -1,12 +1,15 @@ package co.nilin.opex.otp.app.service.message import co.nilin.opex.otp.app.proxy.KaveNegarProxy +import co.nilin.opex.otp.app.service.SMSProviderRouter import org.springframework.stereotype.Component @Component -class SMSSender(private val smsProxy: KaveNegarProxy) : MessageSender { - +class SMSSender( + private val router: SMSProviderRouter +) : MessageSender { override suspend fun send(receiver: String, message: String, metadata: Map): Boolean { - return smsProxy.send(receiver, message) + val provider = router.getProvider(receiver) + return provider.send(receiver, message) } } \ No newline at end of file diff --git a/otp/otp-app/src/main/resources/application.yml b/otp/otp-app/src/main/resources/application.yml index 6dd681baa..10199ab14 100644 --- a/otp/otp-app/src/main/resources/application.yml +++ b/otp/otp-app/src/main/resources/application.yml @@ -51,9 +51,7 @@ otp: response-enabled: ${OTP_CODE_RESPONSE_ENABLED:false} sms: provider: - url: ${SMS_PROVIDER_URL} - api-key: ${SMS_PROVIDER_API_KEY} - template: ${SMS_PROVIDER_TEMPLATE} + default: ${DEFAULT_SMS_PROVIDER} email: host: ${SMTP_HOST} port: ${SMTP_PORT} diff --git a/otp/otp-app/src/main/resources/schema.sql b/otp/otp-app/src/main/resources/schema.sql index 30589c13c..51e84040b 100644 --- a/otp/otp-app/src/main/resources/schema.sql +++ b/otp/otp-app/src/main/resources/schema.sql @@ -59,4 +59,23 @@ on conflict do nothing; insert into totp_config values (true, 128, 'Opex') -on conflict do nothing; \ No newline at end of file +on conflict do nothing; + +CREATE TABLE sms_provider_route ( + id BIGSERIAL PRIMARY KEY, + prefix VARCHAR(32) NOT NULL, + provider VARCHAR(64) NOT NULL, + enabled BOOLEAN NOT NULL DEFAULT TRUE +); + +CREATE TABLE sms_provider ( + id VARCHAR(64) PRIMARY KEY, + enabled BOOLEAN NOT NULL DEFAULT TRUE, + base_url TEXT NOT NULL, + api_key TEXT, + template VARCHAR(128), + username VARCHAR(128), + password VARCHAR(128), + sender VARCHAR(64), + extra_config TEXT +); \ No newline at end of file diff --git a/otp/otp-app/src/test/kotlin/co/nilin/opex/otp/app/conttroller/SMSProviderRouterTest.kt b/otp/otp-app/src/test/kotlin/co/nilin/opex/otp/app/conttroller/SMSProviderRouterTest.kt new file mode 100644 index 000000000..b26b2a21c --- /dev/null +++ b/otp/otp-app/src/test/kotlin/co/nilin/opex/otp/app/conttroller/SMSProviderRouterTest.kt @@ -0,0 +1,80 @@ +import co.nilin.opex.otp.app.data.SMSProviderType +import co.nilin.opex.otp.app.model.SMSProviderRoute +import co.nilin.opex.otp.app.proxy.SMSProvider +import co.nilin.opex.otp.app.repository.SMSProviderRouteRepository +import co.nilin.opex.otp.app.service.SMSProviderRouter +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.Mockito +import org.mockito.Mockito.mock + +class SMSProviderRouterTest { + + private val repository: SMSProviderRouteRepository = mock() + + private val twilioProvider: SMSProvider = mock() + private val kavenegarProvider: SMSProvider = mock() + private val smsToProvider: SMSProvider = mock() + + private lateinit var router: SMSProviderRouter + + @BeforeEach + fun setUp() { + Mockito.`when`(twilioProvider.type).thenReturn(SMSProviderType.TWILIO) + Mockito.`when`(kavenegarProvider.type).thenReturn(SMSProviderType.KAVENEGAR) + Mockito.`when`(smsToProvider.type).thenReturn(SMSProviderType.SMSTO) + + router = SMSProviderRouter( + listOf( + twilioProvider, + kavenegarProvider, + smsToProvider + ), + repository, + SMSProviderType.KAVENEGAR + ) + } + + @Test + fun givenNoMatchingRoute_whenGetProvider_thenDefaultProviderReturned(): Unit = runBlocking { + Mockito.`when`(repository.findAllByEnabledTrue()) + .thenReturn( + listOf( + SMSProviderRoute(1,"+98", SMSProviderType.SMSTO.name), + SMSProviderRoute(2,"+989", SMSProviderType.KAVENEGAR.name), + ) + ) + val provider = router.getProvider("+447700123456") + assertEquals(kavenegarProvider, provider) + } + + @Test + fun givenNoMatchingRoute_whenGetProvider_thenLongestMatchedProviderReturned(): Unit = runBlocking { + Mockito.`when`(repository.findAllByEnabledTrue()) + .thenReturn( + listOf( + SMSProviderRoute(1,"+98", SMSProviderType.SMSTO.name), + SMSProviderRoute(2,"+989", SMSProviderType.KAVENEGAR.name), + ) + ) + val provider = router.getProvider("+989556677788") + assertEquals(kavenegarProvider, provider) + } + + @Test + fun givenNoMatchingRoute_whenGetProvider_thenMatchedProviderReturned(): Unit = runBlocking { + Mockito.`when`(repository.findAllByEnabledTrue()) + .thenReturn( + listOf( + SMSProviderRoute(1,"+98", SMSProviderType.SMSTO.name), + SMSProviderRoute(2,"+989", SMSProviderType.KAVENEGAR.name), + SMSProviderRoute(3,"+44", SMSProviderType.TWILIO.name), + + ) + ) + val provider = router.getProvider("+44555777999") + assertEquals(twilioProvider, provider) + } +} \ No newline at end of file diff --git a/otp/otp-app/src/test/kotlin/co/nilin/opex/otp/app/conttroller/SMSTOProxyIT.kt b/otp/otp-app/src/test/kotlin/co/nilin/opex/otp/app/conttroller/SMSTOProxyIT.kt new file mode 100644 index 000000000..22640f4c8 --- /dev/null +++ b/otp/otp-app/src/test/kotlin/co/nilin/opex/otp/app/conttroller/SMSTOProxyIT.kt @@ -0,0 +1,51 @@ +package co.nilin.opex.otp.app.conttroller + +import co.nilin.opex.otp.app.data.SMSProviderType +import co.nilin.opex.otp.app.proxy.SMSTOProxy +import co.nilin.opex.otp.app.repository.SMSProviderRepository +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Test +import org.mockito.Mockito +import org.springframework.web.reactive.function.client.WebClient +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Disabled + +class SMSTOProxyIT { + + private val repository = Mockito.mock(SMSProviderRepository::class.java) + + private val webClient = WebClient.builder().build() + + private val proxy = SMSTOProxy( + webClient = webClient, + smsProviderRepository = repository + ) + + @Test + @Disabled("Manual test: requires real apiKey") + fun sendRealSms(): Unit = runBlocking { + + Mockito.`when`( + repository.findById(SMSProviderType.SMSTO.name) + ).thenReturn( + co.nilin.opex.otp.app.model.SMSProvider( + id = SMSProviderType.SMSTO.name, + enabled = true, + baseUrl = "https://api.sms.to", + apiKey = "", + template = null, + username = null, + password = null, + sender = "", + extraConfig = null + ) + ) + + val success = proxy.send( + receiver = "", + message = "Manual SMS Test ${System.currentTimeMillis()}" + ) + + assertTrue(success) + } +} \ No newline at end of file diff --git a/otp/otp-app/src/test/kotlin/co/nilin/opex/otp/app/conttroller/TWILIOProxyIT.kt b/otp/otp-app/src/test/kotlin/co/nilin/opex/otp/app/conttroller/TWILIOProxyIT.kt new file mode 100644 index 000000000..b403d386c --- /dev/null +++ b/otp/otp-app/src/test/kotlin/co/nilin/opex/otp/app/conttroller/TWILIOProxyIT.kt @@ -0,0 +1,60 @@ +package co.nilin.opex.otp.app.conttroller + +import co.nilin.opex.common.OpexError +import co.nilin.opex.common.utils.LoggerDelegate +import co.nilin.opex.otp.app.data.SMSProviderType +import co.nilin.opex.otp.app.proxy.SMSProvider +import co.nilin.opex.otp.app.proxy.SMSTOProxy +import co.nilin.opex.otp.app.proxy.TWILIOProxy +import co.nilin.opex.otp.app.repository.SMSProviderRepository +import kotlinx.coroutines.reactor.awaitSingleOrNull +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.mockito.Mockito +import org.springframework.http.MediaType +import org.springframework.stereotype.Component +import org.springframework.util.LinkedMultiValueMap +import org.springframework.web.reactive.function.BodyInserters +import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.bodyToMono + + +class TWILIOProxyIT{ + private val repository = Mockito.mock(SMSProviderRepository::class.java) + + private val webClient = WebClient.builder().build() + + private val proxy = TWILIOProxy( + webClient = webClient, + smsProviderRepository = repository + ) + @Test + @Disabled("Manual test: requires real apiKey") + fun sendRealSms(): Unit = runBlocking { + + Mockito.`when`( + repository.findById(SMSProviderType.TWILIO.name) + ).thenReturn( + co.nilin.opex.otp.app.model.SMSProvider( + id = SMSProviderType.TWILIO.name, + enabled = true, + baseUrl = "https://api.twilio.com", + apiKey = "", + template = null, + username = "", + password = null, + sender = "", + extraConfig = null + ) + ) + val success = proxy.send( + receiver = "", + message = "Manual SMS Test ${System.currentTimeMillis()}" + ) + + assertTrue(success) + } + +} \ No newline at end of file diff --git a/otp/otp-app/src/test/resources/application.yml b/otp/otp-app/src/test/resources/application.yml index 909266384..dbeebe5c7 100644 --- a/otp/otp-app/src/test/resources/application.yml +++ b/otp/otp-app/src/test/resources/application.yml @@ -24,9 +24,7 @@ otp: response-enabled: ${OTP_CODE_RESPONSE_ENABLED:false} sms: provider: - url: ${SMS_PROVIDER_URL} - api-key: ${SMS_PROVIDER_API_KEY} - template: ${SMS_PROVIDER_TEMPLATE} + default: ${DEFAULT_SMS_PROVIDER} email: host: ${SMTP_HOST} port: ${SMTP_PORT} diff --git a/otp/pom.xml b/otp/pom.xml index 712576203..b63670ab8 100644 --- a/otp/pom.xml +++ b/otp/pom.xml @@ -20,10 +20,6 @@ - - org.springframework.boot - spring-boot-starter-test - co.nilin.opex common