diff --git a/server/build.gradle b/server/build.gradle index cec14d02..86600920 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -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') { @@ -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) { diff --git a/server/src/integrationTest/kotlin/org/hkurh/doky/users/DefaultUserServiceIntegrationTest.kt b/server/src/integrationTest/kotlin/org/hkurh/doky/users/DefaultUserServiceIntegrationTest.kt index c8bd1c13..c957683b 100644 --- a/server/src/integrationTest/kotlin/org/hkurh/doky/users/DefaultUserServiceIntegrationTest.kt +++ b/server/src/integrationTest/kotlin/org/hkurh/doky/users/DefaultUserServiceIntegrationTest.kt @@ -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 @@ -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") @@ -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") diff --git a/server/src/main/kotlin/org/hkurh/doky/DokyApplication.kt b/server/src/main/kotlin/org/hkurh/doky/DokyApplication.kt index e1581c78..82aea7ce 100644 --- a/server/src/main/kotlin/org/hkurh/doky/DokyApplication.kt +++ b/server/src/main/kotlin/org/hkurh/doky/DokyApplication.kt @@ -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 @@ -18,6 +19,7 @@ import javax.crypto.spec.SecretKeySpec @EnableScheduling +@EnableAsync @SpringBootApplication class DokyApplication { @Bean diff --git a/server/src/main/kotlin/org/hkurh/doky/events/DokyEventListener.kt b/server/src/main/kotlin/org/hkurh/doky/events/DokyEventListener.kt new file mode 100644 index 00000000..28eaafa2 --- /dev/null +++ b/server/src/main/kotlin/org/hkurh/doky/events/DokyEventListener.kt @@ -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) + } +} diff --git a/server/src/main/kotlin/org/hkurh/doky/events/DokyEventPublisher.kt b/server/src/main/kotlin/org/hkurh/doky/events/DokyEventPublisher.kt new file mode 100644 index 00000000..f584a9de --- /dev/null +++ b/server/src/main/kotlin/org/hkurh/doky/events/DokyEventPublisher.kt @@ -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) + } +} diff --git a/server/src/main/kotlin/org/hkurh/doky/events/DokyEventsConfig.kt b/server/src/main/kotlin/org/hkurh/doky/events/DokyEventsConfig.kt new file mode 100644 index 00000000..81c0eed0 --- /dev/null +++ b/server/src/main/kotlin/org/hkurh/doky/events/DokyEventsConfig.kt @@ -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() + } + } +} diff --git a/server/src/main/kotlin/org/hkurh/doky/events/UserRegistrationEvent.kt b/server/src/main/kotlin/org/hkurh/doky/events/UserRegistrationEvent.kt new file mode 100644 index 00000000..62b7ad3e --- /dev/null +++ b/server/src/main/kotlin/org/hkurh/doky/events/UserRegistrationEvent.kt @@ -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) diff --git a/server/src/main/kotlin/org/hkurh/doky/security/JwtProvider.kt b/server/src/main/kotlin/org/hkurh/doky/security/JwtProvider.kt index 32420bcb..e034aa09 100644 --- a/server/src/main/kotlin/org/hkurh/doky/security/JwtProvider.kt +++ b/server/src/main/kotlin/org/hkurh/doky/security/JwtProvider.kt @@ -5,7 +5,6 @@ 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 /** @@ -13,7 +12,6 @@ import org.springframework.stereotype.Component */ @Component object JwtProvider { - private val LOG = LoggerFactory.getLogger(JwtProvider::class.java) private val jwtParser = Jwts.parserBuilder().setSigningKey(DokyApplication.SECRET_KEY_SPEC).build() /** @@ -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() diff --git a/server/src/main/kotlin/org/hkurh/doky/users/impl/DefaultUserFacade.kt b/server/src/main/kotlin/org/hkurh/doky/users/impl/DefaultUserFacade.kt index 51092e73..f7f57353 100644 --- a/server/src/main/kotlin/org/hkurh/doky/users/impl/DefaultUserFacade.kt +++ b/server/src/main/kotlin/org/hkurh/doky/users/impl/DefaultUserFacade.kt @@ -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() } diff --git a/server/src/main/kotlin/org/hkurh/doky/users/impl/DefaultUserService.kt b/server/src/main/kotlin/org/hkurh/doky/users/impl/DefaultUserService.kt index f1abe942..0477271a 100644 --- a/server/src/main/kotlin/org/hkurh/doky/users/impl/DefaultUserService.kt +++ b/server/src/main/kotlin/org/hkurh/doky/users/impl/DefaultUserService.kt @@ -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") @@ -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 } diff --git a/server/src/test/kotlin/org/hkurh/doky/events/DokyEventListenerTest.kt b/server/src/test/kotlin/org/hkurh/doky/events/DokyEventListenerTest.kt new file mode 100644 index 00000000..779e35f5 --- /dev/null +++ b/server/src/test/kotlin/org/hkurh/doky/events/DokyEventListenerTest.kt @@ -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) } + } +} diff --git a/server/src/test/kotlin/org/hkurh/doky/users/DefaultUserServiceTest.kt b/server/src/test/kotlin/org/hkurh/doky/users/DefaultUserServiceTest.kt index 44a42386..fff72888 100644 --- a/server/src/test/kotlin/org/hkurh/doky/users/DefaultUserServiceTest.kt +++ b/server/src/test/kotlin/org/hkurh/doky/users/DefaultUserServiceTest.kt @@ -1,7 +1,7 @@ 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 @@ -9,7 +9,6 @@ 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 @@ -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 @@ -32,47 +30,35 @@ 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 { 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 @@ -80,7 +66,7 @@ class DefaultUserServiceTest : DokyUnitTest { 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)