Compare commits
5 Commits
master
...
feature/mo
| Author | SHA1 | Date |
|---|---|---|
|
|
ad61be705b | |
|
|
37fb9443ea | |
|
|
5f81afcb08 | |
|
|
1fffef06d9 | |
|
|
47759afac8 |
26
README.md
26
README.md
|
|
@ -213,3 +213,29 @@ server.info:
|
||||||
```
|
```
|
||||||
3) Implement ServerInfoService (optional. If you want to add other headers)
|
3) Implement ServerInfoService (optional. If you want to add other headers)
|
||||||
4) Add dir with impl ServerInfoService in ComponentScan annotation
|
4) Add dir with impl ServerInfoService in ComponentScan annotation
|
||||||
|
|
||||||
|
## push-message-provider
|
||||||
|
|
||||||
|
Интерфейсы и компоненты для модулей по обеспечению интеграции с сервисами отправки пуш-уведомлений.
|
||||||
|
|
||||||
|
## push-message-provider-fcm
|
||||||
|
|
||||||
|
Модуль по обеспечению интеграции с Firebase Cloud Messaging.
|
||||||
|
1) Подключение компонентов Spring осуществляется при помощи аннотации `@EnablePushMessageProviderFcm`.
|
||||||
|
2) Необходимо добавление конфигурации для модуля. Пример файла конфигурации в формате yaml:
|
||||||
|
``` yaml
|
||||||
|
push-message-provider:
|
||||||
|
platformProviders:
|
||||||
|
ANDROID_GOOGLE:
|
||||||
|
- FCM
|
||||||
|
IOS:
|
||||||
|
- FCM
|
||||||
|
fcm:
|
||||||
|
appName: ${appName}
|
||||||
|
auth:
|
||||||
|
resourcePath: credentials/firebase-admin.json
|
||||||
|
client:
|
||||||
|
readTimeout: 10s
|
||||||
|
connectionTimeout: 1s
|
||||||
|
```
|
||||||
|
3) По обозначенному пути `push-message-provider-fcm.auth.resourcePath` добавляется json файл с настройками и доступами из консоли Firebase.
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,8 @@ subprojects {
|
||||||
dependency("com.auth0:java-jwt:3.10.3")
|
dependency("com.auth0:java-jwt:3.10.3")
|
||||||
|
|
||||||
dependency("software.amazon.awssdk:s3:2.10.11")
|
dependency("software.amazon.awssdk:s3:2.10.11")
|
||||||
|
|
||||||
|
dependency("com.google.firebase:firebase-admin:9.0.0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
plugins {
|
||||||
|
id("kotlin")
|
||||||
|
id("kotlin-spring")
|
||||||
|
id("maven-publish")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||||
|
|
||||||
|
implementation("org.springframework.boot:spring-boot")
|
||||||
|
|
||||||
|
implementation(project(":logger-spring"))
|
||||||
|
|
||||||
|
implementation(project(":push-message-provider"))
|
||||||
|
implementation("com.google.firebase:firebase-admin")
|
||||||
|
|
||||||
|
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
|
testImplementation("org.testcontainers:junit-jupiter")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
@file:Suppress("unused")
|
||||||
|
package ru.touchin.push.message.provider.fcm
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Import
|
||||||
|
import ru.touchin.push.message.provider.fcm.configurations.PushMessageProviderFcmConfiguration
|
||||||
|
|
||||||
|
@Import(value = [PushMessageProviderFcmConfiguration::class])
|
||||||
|
annotation class EnablePushMessageProviderFcm
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package ru.touchin.push.message.provider.fcm.clients
|
||||||
|
|
||||||
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
|
import com.google.firebase.messaging.FirebaseMessagingException
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import ru.touchin.push.message.provider.dto.request.PushTokenMessage
|
||||||
|
import ru.touchin.push.message.provider.dto.result.SendPushResult
|
||||||
|
import ru.touchin.push.message.provider.dto.result.SendPushTokenMessageResult
|
||||||
|
import ru.touchin.push.message.provider.exceptions.InvalidPushTokenException
|
||||||
|
import ru.touchin.push.message.provider.exceptions.PushMessageProviderException
|
||||||
|
import ru.touchin.push.message.provider.fcm.converters.FirebaseMessagingExceptionConverter
|
||||||
|
import ru.touchin.push.message.provider.fcm.converters.PushTokenMessageConverter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service that provides integration with FCM.
|
||||||
|
* @see <a href="https://firebase.google.com/docs/cloud-messaging">FCM documentation</a>
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
class FcmClient(
|
||||||
|
private val firebaseMessaging: FirebaseMessaging,
|
||||||
|
private val pushTokenMessageConverter: PushTokenMessageConverter,
|
||||||
|
private val firebaseMessagingExceptionConverter: FirebaseMessagingExceptionConverter
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Throws(PushMessageProviderException::class, InvalidPushTokenException::class)
|
||||||
|
fun sendPushTokenMessage(request: PushTokenMessage): SendPushResult {
|
||||||
|
val message = pushTokenMessageConverter(request)
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val messageId = firebaseMessaging.send(message)
|
||||||
|
|
||||||
|
SendPushTokenMessageResult(messageId)
|
||||||
|
} catch (e: FirebaseMessagingException) {
|
||||||
|
throw firebaseMessagingExceptionConverter(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package ru.touchin.push.message.provider.fcm.configurations
|
||||||
|
|
||||||
|
import com.google.auth.oauth2.GoogleCredentials
|
||||||
|
import com.google.firebase.FirebaseApp
|
||||||
|
import com.google.firebase.FirebaseOptions
|
||||||
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.ComponentScan
|
||||||
|
import org.springframework.context.annotation.Import
|
||||||
|
import org.springframework.core.io.ClassPathResource
|
||||||
|
import ru.touchin.push.message.provider.configurations.PushMessageProviderConfiguration
|
||||||
|
import ru.touchin.push.message.provider.fcm.properties.PushMessageProviderFcmProperties
|
||||||
|
|
||||||
|
@ComponentScan("ru.touchin.push.message.provider.fcm")
|
||||||
|
@ConfigurationPropertiesScan(basePackages = ["ru.touchin.push.message.provider.fcm"])
|
||||||
|
@Import(value = [PushMessageProviderConfiguration::class])
|
||||||
|
class PushMessageProviderFcmConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun firebaseMessaging(
|
||||||
|
properties: PushMessageProviderFcmProperties
|
||||||
|
): FirebaseMessaging {
|
||||||
|
val credentials = GoogleCredentials.fromStream(ClassPathResource(properties.auth.resourcePath).inputStream)
|
||||||
|
|
||||||
|
val options: FirebaseOptions = FirebaseOptions.builder()
|
||||||
|
.setCredentials(credentials)
|
||||||
|
.setConnectTimeout(properties.client.connectionTimeout.toMillis().toInt())
|
||||||
|
.setReadTimeout(properties.client.readTimeout.toMillis().toInt())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val firebaseApp: FirebaseApp = FirebaseApp.initializeApp(options, properties.appName)
|
||||||
|
|
||||||
|
return FirebaseMessaging.getInstance(firebaseApp)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package ru.touchin.push.message.provider.fcm.converters
|
||||||
|
|
||||||
|
import com.google.firebase.messaging.FirebaseMessagingException
|
||||||
|
import com.google.firebase.messaging.MessagingErrorCode
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import ru.touchin.common.exceptions.CommonException
|
||||||
|
import ru.touchin.push.message.provider.exceptions.InvalidPushTokenException
|
||||||
|
import ru.touchin.push.message.provider.exceptions.PushMessageProviderException
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class FirebaseMessagingExceptionConverter {
|
||||||
|
|
||||||
|
operator fun invoke(exception: FirebaseMessagingException): CommonException {
|
||||||
|
return when (exception.messagingErrorCode) {
|
||||||
|
MessagingErrorCode.INVALID_ARGUMENT,
|
||||||
|
MessagingErrorCode.UNREGISTERED -> InvalidPushTokenException()
|
||||||
|
else -> PushMessageProviderException(
|
||||||
|
description = exception.message.orEmpty(),
|
||||||
|
cause = exception
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package ru.touchin.push.message.provider.fcm.converters
|
||||||
|
|
||||||
|
import com.google.firebase.messaging.Notification as FcmNotification
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import ru.touchin.push.message.provider.dto.Notification
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class NotificationConverter {
|
||||||
|
|
||||||
|
operator fun invoke(notification: Notification): FcmNotification {
|
||||||
|
return FcmNotification.builder()
|
||||||
|
.setTitle(notification.title)
|
||||||
|
.setBody(notification.description)
|
||||||
|
.setImage(notification.imageUrl)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package ru.touchin.push.message.provider.fcm.converters
|
||||||
|
|
||||||
|
import com.google.firebase.messaging.Message
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import ru.touchin.push.message.provider.dto.Notification
|
||||||
|
import ru.touchin.push.message.provider.dto.request.PushTokenMessage
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class PushTokenMessageConverter(
|
||||||
|
private val notificationConverter: NotificationConverter
|
||||||
|
) {
|
||||||
|
|
||||||
|
operator fun invoke(request: PushTokenMessage): Message {
|
||||||
|
return Message.builder()
|
||||||
|
.setToken(request.token)
|
||||||
|
.setIfExists(request.notification)
|
||||||
|
.putAllData(request.data)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Message.Builder.setIfExists(notification: Notification?): Message.Builder {
|
||||||
|
return if (notification != null) {
|
||||||
|
setNotification(notificationConverter(notification))
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package ru.touchin.push.message.provider.fcm.properties
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||||
|
import org.springframework.boot.context.properties.ConstructorBinding
|
||||||
|
import ru.touchin.push.message.provider.enums.PlatformType
|
||||||
|
import ru.touchin.push.message.provider.enums.PushMessageProviderType
|
||||||
|
import ru.touchin.push.message.provider.properties.PushMessageProviderProperties
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
@ConstructorBinding
|
||||||
|
@ConfigurationProperties(prefix = "push-message-provider.fcm")
|
||||||
|
class PushMessageProviderFcmProperties(
|
||||||
|
val appName: String,
|
||||||
|
val auth: Auth.Credentials,
|
||||||
|
val client: Client
|
||||||
|
) {
|
||||||
|
|
||||||
|
sealed interface Auth {
|
||||||
|
|
||||||
|
data class Credentials(
|
||||||
|
val resourcePath: String
|
||||||
|
) : Auth
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Client(
|
||||||
|
val readTimeout: Duration,
|
||||||
|
val connectionTimeout: Duration
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package ru.touchin.push.message.provider.fcm.services
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import ru.touchin.push.message.provider.dto.request.PushTokenMessage
|
||||||
|
import ru.touchin.push.message.provider.dto.request.SendPushRequest
|
||||||
|
import ru.touchin.push.message.provider.dto.result.SendPushResult
|
||||||
|
import ru.touchin.push.message.provider.enums.PushMessageProviderType
|
||||||
|
import ru.touchin.push.message.provider.exceptions.InvalidPushTokenException
|
||||||
|
import ru.touchin.push.message.provider.exceptions.PushMessageProviderException
|
||||||
|
import ru.touchin.push.message.provider.fcm.clients.FcmClient
|
||||||
|
import ru.touchin.push.message.provider.services.PushMessageProviderService
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class PushMessageProviderFcmService(
|
||||||
|
private val fcmClient: FcmClient
|
||||||
|
) : PushMessageProviderService {
|
||||||
|
|
||||||
|
override val type: PushMessageProviderType = PushMessageProviderType.FCM
|
||||||
|
|
||||||
|
@Throws(PushMessageProviderException::class, InvalidPushTokenException::class)
|
||||||
|
override fun send(request: SendPushRequest): SendPushResult {
|
||||||
|
return when (request) {
|
||||||
|
is PushTokenMessage -> fcmClient.sendPushTokenMessage(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package ru.touchin.push.message.provider.fcm
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonAutoDetect
|
||||||
|
import com.fasterxml.jackson.annotation.PropertyAccessor
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.fasterxml.jackson.databind.SerializationFeature
|
||||||
|
import org.springframework.boot.SpringBootConfiguration
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
|
||||||
|
import org.springframework.boot.test.context.TestConfiguration
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Import
|
||||||
|
import org.springframework.test.context.ContextConfiguration
|
||||||
|
import ru.touchin.push.message.provider.fcm.configurations.PushMessageProviderFcmConfiguration
|
||||||
|
|
||||||
|
@TestConfiguration
|
||||||
|
@SpringBootConfiguration
|
||||||
|
@EnablePushMessageProviderFcm
|
||||||
|
class PushMessageProviderFcmTestApplication {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun objectMapper(): ObjectMapper {
|
||||||
|
return ObjectMapper().apply {
|
||||||
|
configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
|
||||||
|
setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE)
|
||||||
|
setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package ru.touchin.push.message.provider.fcm.converters
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
|
import ru.touchin.push.message.provider.dto.Notification
|
||||||
|
import com.google.firebase.messaging.Notification as FcmNotification
|
||||||
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
class NotificationConverterTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
lateinit var notificationConverter: NotificationConverter
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
lateinit var objectMapper: ObjectMapper
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Конвертация уведомления происходит корректно")
|
||||||
|
fun invoke_basic() {
|
||||||
|
val notification = Notification(
|
||||||
|
title = "title",
|
||||||
|
description = "description",
|
||||||
|
imageUrl = "imageUrl"
|
||||||
|
)
|
||||||
|
|
||||||
|
val realResult = notificationConverter(notification)
|
||||||
|
val realResultJson = objectMapper.writeValueAsString(realResult)
|
||||||
|
|
||||||
|
val expectedResult = FcmNotification.builder()
|
||||||
|
.setTitle(notification.title)
|
||||||
|
.setBody(notification.description)
|
||||||
|
.setImage(notification.imageUrl)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val expectedResultJson = objectMapper.writeValueAsString(expectedResult)
|
||||||
|
|
||||||
|
Assert.assertEquals(
|
||||||
|
"Конвертация некорректна",
|
||||||
|
realResultJson,
|
||||||
|
expectedResultJson
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
package ru.touchin.push.message.provider.fcm.converters
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.google.firebase.messaging.Message
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
|
import ru.touchin.push.message.provider.dto.Notification
|
||||||
|
import ru.touchin.push.message.provider.dto.request.PushTokenMessage
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
class PushTokenMessageConverterTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
lateinit var pushTokenMessageConverter: PushTokenMessageConverter
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
lateinit var notificationConverter: NotificationConverter
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
lateinit var objectMapper: ObjectMapper
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Конвертация сообщения с уведомлением происходит корректно")
|
||||||
|
fun invoke_withNotification() {
|
||||||
|
val notification = Notification(
|
||||||
|
title = "title",
|
||||||
|
description = "description",
|
||||||
|
imageUrl = "imageUrl"
|
||||||
|
)
|
||||||
|
val pushTokenMessage = PushTokenMessage(
|
||||||
|
token = "token",
|
||||||
|
notification = notification,
|
||||||
|
data = mapOf("testKey" to "testvalue")
|
||||||
|
)
|
||||||
|
|
||||||
|
val realResult = pushTokenMessageConverter(pushTokenMessage)
|
||||||
|
val realResultJson = objectMapper.writeValueAsString(realResult)
|
||||||
|
|
||||||
|
val expectedResult = Message.builder()
|
||||||
|
.setToken(pushTokenMessage.token)
|
||||||
|
.setNotification(notificationConverter(notification))
|
||||||
|
.putAllData(pushTokenMessage.data)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val expectedResultJson = objectMapper.writeValueAsString(expectedResult)
|
||||||
|
|
||||||
|
Assert.assertEquals(
|
||||||
|
"Конвертация некорректна",
|
||||||
|
realResultJson,
|
||||||
|
expectedResultJson
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Конвертация сообщения без уведомления происходит корректно")
|
||||||
|
fun invoke_withoutNotification() {
|
||||||
|
val pushTokenMessage = PushTokenMessage(
|
||||||
|
token = "token",
|
||||||
|
notification = null,
|
||||||
|
data = mapOf("testKey" to "testvalue")
|
||||||
|
)
|
||||||
|
|
||||||
|
val realResult = pushTokenMessageConverter(pushTokenMessage)
|
||||||
|
val realResultJson = objectMapper.writeValueAsString(realResult)
|
||||||
|
|
||||||
|
val expectedResult = Message.builder()
|
||||||
|
.setToken(pushTokenMessage.token)
|
||||||
|
.putAllData(pushTokenMessage.data)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val expectedResultJson = objectMapper.writeValueAsString(expectedResult)
|
||||||
|
|
||||||
|
Assert.assertEquals(
|
||||||
|
"Конвертация некорректна",
|
||||||
|
realResultJson,
|
||||||
|
expectedResultJson
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
package ru.touchin.push.message.provider.fcm.services
|
||||||
|
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.mockito.Mockito
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean
|
||||||
|
import ru.touchin.push.message.provider.dto.request.PushTokenMessage
|
||||||
|
import ru.touchin.push.message.provider.dto.result.SendPushTokenMessageResult
|
||||||
|
import ru.touchin.push.message.provider.fcm.clients.FcmClient
|
||||||
|
import ru.touchin.push.message.provider.services.PushMessageProviderService
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
class PushMessageProviderFcmServiceTest {
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
lateinit var fcmClient: FcmClient
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
lateinit var pushMessageProviderService: PushMessageProviderService
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Обработка запроса на отправку единичного сообщения происходит корректно")
|
||||||
|
fun send_basic() {
|
||||||
|
val request = PushTokenMessage(
|
||||||
|
token = "testToken",
|
||||||
|
notification = null,
|
||||||
|
data = emptyMap()
|
||||||
|
)
|
||||||
|
|
||||||
|
val expectedResult = SendPushTokenMessageResult("testMessageId")
|
||||||
|
|
||||||
|
Mockito.`when`(
|
||||||
|
fcmClient.sendPushTokenMessage(request)
|
||||||
|
).thenReturn(
|
||||||
|
expectedResult
|
||||||
|
)
|
||||||
|
|
||||||
|
val realResult = pushMessageProviderService.send(request)
|
||||||
|
|
||||||
|
Assert.assertEquals(
|
||||||
|
"Обработка запроса на отправку единичного сообщения происходит некорректно",
|
||||||
|
expectedResult,
|
||||||
|
realResult
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
push-message-provider:
|
||||||
|
platformProviders:
|
||||||
|
ANDROID_GOOGLE:
|
||||||
|
- FCM
|
||||||
|
IOS:
|
||||||
|
- FCM
|
||||||
|
fcm:
|
||||||
|
appName: testAppName
|
||||||
|
auth:
|
||||||
|
resourcePath: credentials/firebase-admin.json
|
||||||
|
client:
|
||||||
|
readTimeout: 10s
|
||||||
|
connectionTimeout: 1s
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"type": "service_account",
|
||||||
|
"project_id": "testProjectId",
|
||||||
|
"private_key_id": "privateKeyId",
|
||||||
|
"private_key": "-----BEGIN PRIVATE KEY-----\nMIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALfBshaLMW2yddmAZJRNXTZzcSbwvY93Dnjj6naWgoBJoB3mOM5bcoyWwBw12A4rwecorz74OUOc6zdqX3j8hwsSyzgAUStKM5PkOvPNRKsI4eXAWU0fmb8h1jyXwftl7EzeBjEMBTpyXkgDk3wLfHN6ciCZrnQndOvS+mMl3b0hAgMBAAECgYEAmIQZByMSrITR0ewCDyFDO52HjhWEkF310hsBkNoNiOMTFZ3vCj/WjJ/W5dM+90wUTYN0KOSnytmkVUNh6K5Yekn+yRg/mBRTwwn88hU6umB8tUqoNz7AyUltAOGyQMWqAAcVgxV+mAp/Y018j69poEHgrW4qKol65/NRZyV7/J0CQQD4rCDjmxGEuA1yMzL2i8NyNl/5vvLVfLcEnVqpHbc1+KfUHZuY7iv38xpzfmErqhCxAXfQ52edq5rXmMIVSbFrAkEAvSvfSSK9XQDJl3NEyfR3BGbsoqKIYOuJAnv4OQPSODZfTNWhc11S8y914qaSWB+Iid9HoLvAIgPH5mrzPzjSowJBAJcw4FZCI+aTmOlEI8ous8gvMy8/X5lZWFUf7s0/2fKgmjmnPsE+ndEFJ6HsxturbLaR8+05pJAClARdRjN3OL0CQGoF+8gmw1ErztCmVyiFbms2MGxagesoN4r/5jg2Tw0YVENg/HMHHCWWNREJ4L2pNsJnNOL+N4oY6mHXEWwesdcCQCUYTfLYxi+Wg/5BSC7fgl/gu0mlx07AzMoMQLDOXdisV5rpxrOoT3BOLBqyccv37AZ3e2gqb8JYyNzO6C0zswQ=\n-----END PRIVATE KEY-----\n",
|
||||||
|
"client_email": "clientEmail",
|
||||||
|
"client_id": "clientId",
|
||||||
|
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||||
|
"token_uri": "https://accounts.google.com/o/oauth2/token",
|
||||||
|
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||||
|
"client_x509_cert_url": "clientX509CertUrl"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
plugins {
|
||||||
|
id("kotlin")
|
||||||
|
id("kotlin-spring")
|
||||||
|
id("maven-publish")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||||
|
|
||||||
|
implementation("org.springframework.boot:spring-boot")
|
||||||
|
|
||||||
|
implementation(project(":common"))
|
||||||
|
|
||||||
|
testImplementation("org.testcontainers:junit-jupiter")
|
||||||
|
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package ru.touchin.push.message.provider.configurations
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
|
||||||
|
import org.springframework.context.annotation.ComponentScan
|
||||||
|
|
||||||
|
@ComponentScan("ru.touchin.push.message.provider")
|
||||||
|
@ConfigurationPropertiesScan(basePackages = ["ru.touchin.push.message.provider"])
|
||||||
|
class PushMessageProviderConfiguration
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package ru.touchin.push.message.provider.dto
|
||||||
|
|
||||||
|
class Notification(
|
||||||
|
val title: String,
|
||||||
|
val description: String,
|
||||||
|
val imageUrl: String
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package ru.touchin.push.message.provider.dto.request
|
||||||
|
|
||||||
|
import ru.touchin.push.message.provider.dto.Notification
|
||||||
|
|
||||||
|
class PushTokenMessage(
|
||||||
|
val token: String,
|
||||||
|
override val notification: Notification?,
|
||||||
|
override val data: Map<String, String>
|
||||||
|
) : SendPushRequest
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package ru.touchin.push.message.provider.dto.request
|
||||||
|
|
||||||
|
import ru.touchin.push.message.provider.dto.Notification
|
||||||
|
|
||||||
|
sealed interface SendPushRequest {
|
||||||
|
|
||||||
|
val notification: Notification?
|
||||||
|
val data: Map<String, String>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
package ru.touchin.push.message.provider.dto.result
|
||||||
|
|
||||||
|
sealed interface SendPushResult
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package ru.touchin.push.message.provider.dto.result
|
||||||
|
|
||||||
|
class SendPushTokenMessageResult(
|
||||||
|
val messageId: String
|
||||||
|
) : SendPushResult
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package ru.touchin.push.message.provider.enums
|
||||||
|
|
||||||
|
enum class PlatformType {
|
||||||
|
|
||||||
|
ANDROID_GOOGLE,
|
||||||
|
IOS
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package ru.touchin.push.message.provider.enums
|
||||||
|
|
||||||
|
enum class PushMessageProviderType {
|
||||||
|
|
||||||
|
FCM
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package ru.touchin.push.message.provider.exceptions
|
||||||
|
|
||||||
|
import ru.touchin.common.exceptions.CommonException
|
||||||
|
|
||||||
|
class InvalidPushTokenException : CommonException("Invalid push token")
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package ru.touchin.push.message.provider.exceptions
|
||||||
|
|
||||||
|
import ru.touchin.common.exceptions.CommonException
|
||||||
|
|
||||||
|
open class PushMessageProviderException(
|
||||||
|
description: String,
|
||||||
|
cause: Throwable?
|
||||||
|
) : CommonException(description, cause)
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package ru.touchin.push.message.provider.factories
|
||||||
|
|
||||||
|
import ru.touchin.push.message.provider.enums.PlatformType
|
||||||
|
import ru.touchin.push.message.provider.services.PushMessageProviderService
|
||||||
|
|
||||||
|
interface PushMessageProviderServiceFactory {
|
||||||
|
|
||||||
|
fun get(platformType: PlatformType): PushMessageProviderService
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package ru.touchin.push.message.provider.factories
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import ru.touchin.common.exceptions.CommonException
|
||||||
|
import ru.touchin.push.message.provider.enums.PlatformType
|
||||||
|
import ru.touchin.push.message.provider.properties.PushMessageProviderProperties
|
||||||
|
import ru.touchin.push.message.provider.services.PushMessageProviderService
|
||||||
|
import kotlin.jvm.Throws
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class PushMessageProviderServiceFactoryImpl(
|
||||||
|
private val pushMessageProviderProperties: PushMessageProviderProperties,
|
||||||
|
private val pushMessageProviderServices: List<PushMessageProviderService>
|
||||||
|
) : PushMessageProviderServiceFactory {
|
||||||
|
|
||||||
|
@Throws(CommonException::class)
|
||||||
|
override fun get(platformType: PlatformType): PushMessageProviderService {
|
||||||
|
val supportedProviderTypes = pushMessageProviderProperties.platformProviders[platformType]?.firstOrNull()
|
||||||
|
?: throw CommonException("Configuration has no setup for platform '$platformType'")
|
||||||
|
|
||||||
|
return pushMessageProviderServices.find { it.type == supportedProviderTypes }
|
||||||
|
?: throw CommonException("Configuration has no push message provider support for platform '$platformType'")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package ru.touchin.push.message.provider.properties
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||||
|
import org.springframework.boot.context.properties.ConstructorBinding
|
||||||
|
import ru.touchin.push.message.provider.enums.PlatformType
|
||||||
|
import ru.touchin.push.message.provider.enums.PushMessageProviderType
|
||||||
|
|
||||||
|
@ConstructorBinding
|
||||||
|
@ConfigurationProperties(prefix = "push-message-provider")
|
||||||
|
class PushMessageProviderProperties(
|
||||||
|
val platformProviders: Map<PlatformType, List<PushMessageProviderType>>
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package ru.touchin.push.message.provider.services
|
||||||
|
|
||||||
|
import ru.touchin.push.message.provider.dto.request.SendPushRequest
|
||||||
|
import ru.touchin.push.message.provider.dto.result.SendPushResult
|
||||||
|
import ru.touchin.push.message.provider.enums.PushMessageProviderType
|
||||||
|
|
||||||
|
interface PushMessageProviderService {
|
||||||
|
|
||||||
|
val type: PushMessageProviderType
|
||||||
|
|
||||||
|
fun send(request: SendPushRequest): SendPushResult
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package ru.touchin.push.message.provider
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringBootConfiguration
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
|
||||||
|
import org.springframework.boot.test.context.TestConfiguration
|
||||||
|
import org.springframework.context.annotation.Import
|
||||||
|
import org.springframework.test.context.ContextConfiguration
|
||||||
|
import ru.touchin.push.message.provider.configurations.PushMessageProviderConfiguration
|
||||||
|
|
||||||
|
@TestConfiguration
|
||||||
|
@SpringBootConfiguration
|
||||||
|
@ContextConfiguration(classes = [PushMessageProviderConfiguration::class])
|
||||||
|
@Import(PushMessageProviderConfiguration::class)
|
||||||
|
class PushMessageProviderTestApplication
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
package ru.touchin.push.message.provider.factories
|
||||||
|
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import ru.touchin.common.exceptions.CommonException
|
||||||
|
|
||||||
|
import ru.touchin.push.message.provider.dto.request.SendPushRequest
|
||||||
|
import ru.touchin.push.message.provider.dto.result.SendPushResult
|
||||||
|
import ru.touchin.push.message.provider.enums.PlatformType
|
||||||
|
import ru.touchin.push.message.provider.enums.PushMessageProviderType
|
||||||
|
import ru.touchin.push.message.provider.properties.PushMessageProviderProperties
|
||||||
|
import ru.touchin.push.message.provider.services.PushMessageProviderService
|
||||||
|
|
||||||
|
class PushMessageProviderServiceFactoryImplTest {
|
||||||
|
|
||||||
|
private val pushMessageProviderServiceFcm = object : PushMessageProviderService {
|
||||||
|
|
||||||
|
override val type: PushMessageProviderType = PushMessageProviderType.FCM
|
||||||
|
|
||||||
|
override fun send(request: SendPushRequest): SendPushResult = throw NotImplementedError()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("При отсутствии поддерживаемых платформ выбрасывается исключение")
|
||||||
|
fun get_platformNotFound() {
|
||||||
|
val pushMessageProviderServiceFactory = PushMessageProviderServiceFactoryImpl(
|
||||||
|
pushMessageProviderProperties = PushMessageProviderProperties(
|
||||||
|
platformProviders = emptyMap()
|
||||||
|
),
|
||||||
|
pushMessageProviderServices = listOf(pushMessageProviderServiceFcm)
|
||||||
|
)
|
||||||
|
|
||||||
|
Assert.assertThrows(
|
||||||
|
"Исключение не выбрасывается или принадлежит иному типу",
|
||||||
|
CommonException::class.java
|
||||||
|
) {
|
||||||
|
pushMessageProviderServiceFactory.get(PlatformType.IOS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("При отсутствии назначенного провайдера у поддерживаемой платформы выбрасывается исключение")
|
||||||
|
fun get_providerServiceForPlatformNotFound() {
|
||||||
|
val pushMessageProviderServiceFactory = PushMessageProviderServiceFactoryImpl(
|
||||||
|
pushMessageProviderProperties = PushMessageProviderProperties(
|
||||||
|
platformProviders = mapOf(
|
||||||
|
PlatformType.IOS to emptyList()
|
||||||
|
)
|
||||||
|
),
|
||||||
|
pushMessageProviderServices = listOf(pushMessageProviderServiceFcm)
|
||||||
|
)
|
||||||
|
|
||||||
|
Assert.assertThrows(
|
||||||
|
"Исключение не выбрасывается или принадлежит иному типу",
|
||||||
|
CommonException::class.java
|
||||||
|
) {
|
||||||
|
pushMessageProviderServiceFactory.get(PlatformType.IOS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Настроенной платформе назначается первый сервис из доступных")
|
||||||
|
fun get_firstSupportedProviderService() {
|
||||||
|
val pushMessageProviderServiceFactory = PushMessageProviderServiceFactoryImpl(
|
||||||
|
pushMessageProviderProperties = PushMessageProviderProperties(
|
||||||
|
platformProviders = mapOf(PlatformType.IOS to listOf(PushMessageProviderType.FCM))
|
||||||
|
),
|
||||||
|
pushMessageProviderServices = listOf(
|
||||||
|
pushMessageProviderServiceFcm,
|
||||||
|
object : PushMessageProviderService {
|
||||||
|
|
||||||
|
override val type: PushMessageProviderType = PushMessageProviderType.FCM
|
||||||
|
|
||||||
|
override fun send(request: SendPushRequest): SendPushResult = throw NotImplementedError()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Assert.assertEquals(
|
||||||
|
"Платформе назначен не первый сервис из доступных",
|
||||||
|
pushMessageProviderServiceFcm,
|
||||||
|
pushMessageProviderServiceFactory.get(PlatformType.IOS)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
push-message-provider:
|
||||||
|
platformProviders:
|
||||||
|
ANDROID_GOOGLE:
|
||||||
|
- FCM
|
||||||
|
IOS:
|
||||||
|
- FCM
|
||||||
|
|
@ -42,6 +42,8 @@ include("exception-handler-spring-security-web")
|
||||||
include("exception-handler-logger-spring-web")
|
include("exception-handler-logger-spring-web")
|
||||||
include("validation-spring")
|
include("validation-spring")
|
||||||
include("version-spring-web")
|
include("version-spring-web")
|
||||||
|
include("push-message-provider")
|
||||||
|
include("push-message-provider-fcm")
|
||||||
include("response-wrapper-spring-web")
|
include("response-wrapper-spring-web")
|
||||||
include("settings-spring-jpa")
|
include("settings-spring-jpa")
|
||||||
include("security-authorization-server-core")
|
include("security-authorization-server-core")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue