Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MAIN-T-99 Send registration email async #98

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ dependencies {
testImplementation 'org.apache.httpcomponents:httpclient:4.5.13'
testImplementation 'org.junit.jupiter:junit-jupiter:5.8.2'
testImplementation 'org.mockito:mockito-junit-jupiter:4.0.0'
testImplementation "org.mockito.kotlin:mockito-kotlin:5.0.0"
testImplementation 'org.mockito.kotlin:mockito-kotlin:5.0.0'
testImplementation 'com.icegreen:greenmail:2.0.1'
testImplementation 'org.awaitility:awaitility:4.2.1'


testImplementation('io.rest-assured:rest-assured:5.3.0') {
Expand Down Expand Up @@ -111,6 +112,7 @@ testing {

implementation 'com.icegreen:greenmail:2.0.1'
implementation 'org.apache.solr:solr-solrj:9.5.0'
implementation 'org.awaitility:awaitility:4.2.1'
}
}
apiTest(JvmTestSuite) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.hkurh.doky.users

import com.icegreen.greenmail.util.GreenMail
import org.awaitility.Awaitility.await
import org.hkurh.doky.DokyIntegrationTest
import org.hkurh.doky.SmtpServerExtension
import org.junit.jupiter.api.Assertions.assertEquals
Expand All @@ -10,6 +11,7 @@ import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.test.context.jdbc.Sql
import java.util.concurrent.TimeUnit

@ExtendWith(SmtpServerExtension::class)
@DisplayName("DefaultUserService integration test")
Expand All @@ -34,6 +36,7 @@ class DefaultUserServiceIntegrationTest : DokyIntegrationTest() {
userService.create(userEmail, "password")

// then
await().atMost(10, TimeUnit.SECONDS).until { smtpServer!!.receivedMessages.isNotEmpty() }
smtpServer!!.apply {
assertNotNull(receivedMessages, "No emails sent")
assertEquals(1, receivedMessages.size, "Incorrect amount of messages")
Expand Down
2 changes: 2 additions & 0 deletions server/src/main/kotlin/org/hkurh/doky/DokyApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.context.annotation.Bean
import org.springframework.scheduling.annotation.EnableAsync
import org.springframework.scheduling.annotation.EnableScheduling
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
Expand All @@ -18,6 +19,7 @@ import javax.crypto.spec.SecretKeySpec


@EnableScheduling
@EnableAsync
@SpringBootApplication
class DokyApplication {
@Bean
Expand Down
27 changes: 27 additions & 0 deletions server/src/main/kotlin/org/hkurh/doky/events/DokyEventListener.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.hkurh.doky.events

import org.hkurh.doky.email.EmailService
import org.slf4j.LoggerFactory
import org.springframework.context.event.EventListener
import org.springframework.mail.MailException
import org.springframework.stereotype.Component

@Component
class DokyEventListener(
private val emailService: EmailService
) {

@EventListener
fun sendRegistrationEmail(event: UserRegistrationEvent) {
try {
LOG.debug("Process user registration event for user [${event.user.id}]")
emailService.sendRegistrationConfirmationEmail(event.user)
} catch (e: MailException) {
LOG.error("Error during sending registration email for user [${event.user.id}]", e)
}
}

companion object {
private val LOG = LoggerFactory.getLogger(DokyEventListener::class.java)
}
}
21 changes: 21 additions & 0 deletions server/src/main/kotlin/org/hkurh/doky/events/DokyEventPublisher.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.hkurh.doky.events

import org.hkurh.doky.users.db.UserEntity
import org.slf4j.LoggerFactory
import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Component

@Component
class DokyEventPublisher(private val applicationEventPublisher: ApplicationEventPublisher) {

fun publishUserRegistrationEvent(user: UserEntity) {
LOG.debug("Publishing user registration event for user [${user.id}]")
val event = UserRegistrationEvent(this, user)
applicationEventPublisher.publishEvent(event)
}


companion object {
private val LOG = LoggerFactory.getLogger(DokyEventPublisher::class.java)
}
}
29 changes: 29 additions & 0 deletions server/src/main/kotlin/org/hkurh/doky/events/DokyEventsConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.hkurh.doky.events

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.event.ApplicationEventMulticaster
import org.springframework.context.event.SimpleApplicationEventMulticaster
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor

@Configuration
class DokyEventsConfig {

@Bean
fun applicationEventMulticaster(): ApplicationEventMulticaster {
return SimpleApplicationEventMulticaster().apply {
setTaskExecutor(threadPoolTaskExecutor())
}
}

@Bean
fun threadPoolTaskExecutor(): ThreadPoolTaskExecutor {
return ThreadPoolTaskExecutor().apply {
corePoolSize = 5
maxPoolSize = 10
queueCapacity = 25
setThreadNamePrefix("doky-event-pool-")
initialize()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.hkurh.doky.events

import org.hkurh.doky.users.db.UserEntity
import org.springframework.context.ApplicationEvent

class UserRegistrationEvent(
source: Any,
var user: UserEntity
) : ApplicationEvent(source)
3 changes: 0 additions & 3 deletions server/src/main/kotlin/org/hkurh/doky/security/JwtProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@ import io.jsonwebtoken.SignatureAlgorithm
import org.hkurh.doky.DokyApplication
import org.joda.time.DateTime
import org.joda.time.DateTimeZone
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component

/**
* [JwtProvider] is a utility class responsible for generating and parsing JSON Web Tokens (JWTs).
*/
@Component
object JwtProvider {
private val LOG = LoggerFactory.getLogger(JwtProvider::class.java)
private val jwtParser = Jwts.parserBuilder().setSigningKey(DokyApplication.SECRET_KEY_SPEC).build()

/**
Expand All @@ -23,7 +21,6 @@ object JwtProvider {
* @return The generated token.
*/
fun generateToken(username: String): String {
LOG.debug("Generate token for user $username")
val currentTime = DateTime(DateTimeZone.getDefault())
val expireTokenTime = currentTime.plusDays(1)
return Jwts.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class DefaultUserFacade(private val userService: UserService, private val passwo
if (userService.exists(userUid)) throw DokyRegistrationException("User already exists")
val encodedPassword = passwordEncoder.encode(password)
val userEntity = userService.create(userUid, encodedPassword)
LOG.info("Register new user $userEntity")
LOG.info("Register new user [${userEntity.id}]")
return userEntity.toDto()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
package org.hkurh.doky.users.impl

import org.hkurh.doky.email.EmailService
import org.hkurh.doky.errorhandling.DokyNotFoundException
import org.hkurh.doky.events.DokyEventPublisher
import org.hkurh.doky.security.DokyUserDetails
import org.hkurh.doky.users.UserService
import org.hkurh.doky.users.db.UserEntity
import org.hkurh.doky.users.db.UserEntityRepository
import org.slf4j.LoggerFactory
import org.springframework.mail.MailException
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Service

@Service
class DefaultUserService(
private val userEntityRepository: UserEntityRepository,
private val emailService: EmailService
private val eventPublisher: DokyEventPublisher,
) : UserService {
override fun findUserByUid(userUid: String): UserEntity? {
return userEntityRepository.findByUid(userUid) ?: throw DokyNotFoundException("User doesn't exist")
Expand All @@ -26,12 +25,8 @@ class DefaultUserService(
userEntity.password = encodedPassword
userEntity.name = extractNameFromUid(userUid)
val createdUser = userEntityRepository.save(userEntity)
LOG.debug("Created new user ${createdUser.id}")
try {
emailService.sendRegistrationConfirmationEmail(createdUser)
} catch (e: MailException) {
LOG.error("Error during sending registration email for user [${createdUser.id}]", e)
}
LOG.debug("Created new user [${createdUser.id}]")
eventPublisher.publishUserRegistrationEvent(createdUser)
return createdUser
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.hkurh.doky.events

import org.hkurh.doky.DokyUnitTest
import org.hkurh.doky.email.EmailService
import org.hkurh.doky.users.db.UserEntity
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertDoesNotThrow
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import org.springframework.mail.MailSendException

@DisplayName("DokyEventListenerTest unit test")
class DokyEventListenerTest : DokyUnitTest {

private val emailService: EmailService = mock()
private val eventListener = DokyEventListener(emailService)

@Test
@DisplayName("Should send an email")
fun shouldSendAnEmail() {
// given
val user = UserEntity()
val event = UserRegistrationEvent(this, user)

// when
eventListener.sendRegistrationEmail(event)

// then
verify(emailService).sendRegistrationConfirmationEmail(user)
}

@Test
@DisplayName("Should not fail if sending email fails")
fun shouldNotFail_whenSendingEmailFails() {
// given
val user = UserEntity()
val event = UserRegistrationEvent(this, user)
whenever(emailService.sendRegistrationConfirmationEmail(user)).thenThrow(MailSendException("Test exception"))

// when - then
assertDoesNotThrow { eventListener.sendRegistrationEmail(event) }
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
package org.hkurh.doky.users

import org.hkurh.doky.DokyUnitTest
import org.hkurh.doky.email.EmailService
import org.hkurh.doky.events.DokyEventPublisher
import org.hkurh.doky.users.db.UserEntity
import org.hkurh.doky.users.db.UserEntityRepository
import org.hkurh.doky.users.impl.DefaultUserService
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
import org.mockito.Mockito
import org.mockito.kotlin.any
import org.mockito.kotlin.argThat
import org.mockito.kotlin.mock
Expand All @@ -21,7 +20,6 @@ import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Sort
import org.springframework.data.jpa.domain.Specification
import org.springframework.data.repository.query.FluentQuery
import org.springframework.mail.MailSendException
import java.util.*
import java.util.function.Function

Expand All @@ -32,55 +30,43 @@ class DefaultUserServiceTest : DokyUnitTest {
private val userPassword = "password"

private var userEntityRepository: MockUserEntityRepository = mock()
private val emailService: EmailService = mock()
private var userService = DefaultUserService(userEntityRepository, emailService)
private val eventPublisher: DokyEventPublisher = mock()
private var userService = DefaultUserService(userEntityRepository, eventPublisher)

@Test
@DisplayName("Should send registration email when user is successfully registered")
fun shouldSendEmail_whenUserSuccessfullyRegistered() {
@DisplayName("Should publish user registration event when user is successfully registered")
fun shouldPublishEvent_whenUserSuccessfullyRegistered() {
// given
val userEntity = createUserEntity()
Mockito.`when`(userEntityRepository.save(any())).thenReturn(userEntity)
whenever(userEntityRepository.save(any())).thenReturn(userEntity)

// when
assertDoesNotThrow { userService.create(userUid, userPassword) }

// then
verify(emailService).sendRegistrationConfirmationEmail(userEntity)
verify(eventPublisher).publishUserRegistrationEvent(userEntity)
}

@Test
@DisplayName("Should not send registration email when user is not successfully registered")
fun shouldNotSendEmail_whenUserNotSuccessfullyRegistered() {
@DisplayName("Should not publish user registration event when user is not successfully registered")
fun shouldNotPublishEvent_whenUserNotSuccessfullyRegistered() {
// given
val userEntity = createUserEntity()
Mockito.`when`(userEntityRepository.save(any())).thenThrow(RuntimeException())
whenever(userEntityRepository.save(any())).thenThrow(RuntimeException())

// when
assertThrows<RuntimeException> { userService.create(userUid, userPassword) }

// then
verify(emailService, never()).sendRegistrationConfirmationEmail(userEntity)
}

@Test
@DisplayName("Should not throw exception when sending email is not successfully")
fun shouldNotThrowException_whenEmailSendNotSuccessfully() {
// given
val userEntity = createUserEntity()
Mockito.`when`(userEntityRepository.save(any())).thenReturn(userEntity)
whenever(emailService.sendRegistrationConfirmationEmail(userEntity)).thenThrow(MailSendException(""))

// when - then
assertDoesNotThrow { userService.create(userUid, userPassword) }
verify(eventPublisher, never()).publishUserRegistrationEvent(userEntity)
}

@Test
@DisplayName("Should set name for user from UID when register")
fun shouldSetUserNameFromUid_whenRegister() {
// given
val userEntity = createUserEntity()
Mockito.`when`(userEntityRepository.save(any())).thenReturn(userEntity)
whenever(userEntityRepository.save(any())).thenReturn(userEntity)

// when
userService.create(userUid, userPassword)
Expand Down
Loading