diff --git a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/HankeControllerITests.kt b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/HankeControllerITests.kt index 37e5b6828..6b6c5e5cb 100644 --- a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/HankeControllerITests.kt +++ b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/HankeControllerITests.kt @@ -7,6 +7,7 @@ import fi.hel.haitaton.hanke.factory.AlluDataFactory import fi.hel.haitaton.hanke.factory.DateFactory import fi.hel.haitaton.hanke.factory.HankeFactory import fi.hel.haitaton.hanke.factory.HankeFactory.Companion.withGeneratedOmistaja +import fi.hel.haitaton.hanke.factory.HankeFactory.Companion.withYhteystiedot import fi.hel.haitaton.hanke.geometria.Geometriat import fi.hel.haitaton.hanke.logging.DisclosureLogService import fi.hel.haitaton.hanke.permissions.PermissionCode @@ -329,14 +330,19 @@ class HankeControllerITests(@Autowired override val mockMvc: MockMvc) : Controll @Test fun `Sanitize hanke input and return 200`() { - val hanke = HankeFactory.create().apply { generated = true } - every { hankeService.createHanke(hanke.copy(id = null, generated = false)) } returns - hanke.copy(generated = false) + val hanke = HankeFactory.create().withYhteystiedot().apply { generated = true } + val expectedServiceArgument = + hanke.apply { + generated = false + id = null + } + every { hankeService.createHanke(expectedServiceArgument) } returns + expectedServiceArgument post(url, hanke).andExpect(status().isOk) verifySequence { - hankeService.createHanke(any()) + hankeService.createHanke(expectedServiceArgument) disclosureLogService.saveDisclosureLogsForHanke(any(), any()) } } diff --git a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/email/EmailSenderServiceITest.kt b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/email/EmailSenderServiceITest.kt index 5e1961bb3..007180518 100644 --- a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/email/EmailSenderServiceITest.kt +++ b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/email/EmailSenderServiceITest.kt @@ -163,18 +163,6 @@ class EmailSenderServiceITest : DatabaseTest() { contains("""""") } } - - @Test - fun `sendHankeInvitationEmail handles input without inviter name`() { - val data = hankeInvitationData(inviterName = null) - - emailSenderService.sendHankeInvitationEmail(data) - - val email = greenMail.firstReceivedMessage() - val (textBody, htmlBody) = getBodiesFromHybridEmail(email) - assertThat(textBody).contains("Asioija ${data.inviterEmail}") - assertThat(htmlBody).contains("Asioija ${data.inviterEmail}") - } } @Nested @@ -229,18 +217,6 @@ class EmailSenderServiceITest : DatabaseTest() { contains("""""") } } - - @Test - fun `sendApplicationInvitationEmail handles input without inviter name`() { - val data = applicationInvitationData(inviterName = null) - - emailSenderService.sendApplicationInvitationEmail(data) - - val email = greenMail.firstReceivedMessage() - val (textBody, htmlBody) = getBodiesFromHybridEmail(email) - assertThat(textBody).contains("Asioija ${data.inviterEmail} on tehnyt") - assertThat(htmlBody).contains("Asioija ${data.inviterEmail} on tehnyt") - } } /** Returns a (text body, HTML body) pair. */ @@ -270,7 +246,7 @@ class EmailSenderServiceITest : DatabaseTest() { return Pair(bodies[0], bodies[1]) } - private fun hankeInvitationData(inviterName: String? = DEFAULT_INVITER_NAME) = + private fun hankeInvitationData(inviterName: String = DEFAULT_INVITER_NAME) = HankeInvitationData( inviterName = inviterName, inviterEmail = "kalle.kutsuja@test.fi", @@ -280,7 +256,7 @@ class EmailSenderServiceITest : DatabaseTest() { invitationToken = "MgtzRbcPsvoKQamnaSxCnmW7", ) - private fun applicationInvitationData(inviterName: String? = DEFAULT_INVITER_NAME) = + private fun applicationInvitationData(inviterName: String = DEFAULT_INVITER_NAME) = ApplicationInvitationData( inviterName = inviterName, inviterEmail = "kalle.kutsuja@test.fi", diff --git a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/permissions/HankeKayttajaServiceITest.kt b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/permissions/HankeKayttajaServiceITest.kt index b34bd63c9..0d6d1059a 100644 --- a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/permissions/HankeKayttajaServiceITest.kt +++ b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/permissions/HankeKayttajaServiceITest.kt @@ -13,26 +13,40 @@ import assertk.assertions.hasSize import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isIn +import assertk.assertions.isNotEmpty import assertk.assertions.isNotNull import assertk.assertions.isNull +import assertk.assertions.isTrue import assertk.assertions.matches import assertk.assertions.messageContains -import com.fasterxml.jackson.databind.util.ClassUtil.hasClass +import com.ninjasquad.springmockk.MockkBean import fi.hel.haitaton.hanke.DatabaseTest +import fi.hel.haitaton.hanke.email.EmailSenderService +import fi.hel.haitaton.hanke.email.HankeInvitationData import fi.hel.haitaton.hanke.factory.AlluDataFactory +import fi.hel.haitaton.hanke.factory.AlluDataFactory.Companion.defaultApplicationName +import fi.hel.haitaton.hanke.factory.AlluDataFactory.Companion.teppoEmail import fi.hel.haitaton.hanke.factory.AlluDataFactory.Companion.withContacts import fi.hel.haitaton.hanke.factory.HankeFactory import fi.hel.haitaton.hanke.factory.HankeFactory.Companion.withGeneratedOmistaja import fi.hel.haitaton.hanke.factory.HankeFactory.Companion.withYhteystiedot import fi.hel.haitaton.hanke.factory.HankeYhteystietoFactory +import fi.hel.haitaton.hanke.factory.TEPPO_TESTI import fi.hel.haitaton.hanke.logging.AuditLogRepository import fi.hel.haitaton.hanke.logging.ObjectType import fi.hel.haitaton.hanke.logging.Operation import fi.hel.haitaton.hanke.logging.UserRole import fi.hel.haitaton.hanke.test.Asserts.isRecent import fi.hel.haitaton.hanke.toChangeLogJsonString +import io.mockk.checkUnnecessaryStub +import io.mockk.clearAllMocks +import io.mockk.confirmVerified +import io.mockk.justRun +import io.mockk.verify import java.time.OffsetDateTime import java.util.UUID +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired @@ -51,6 +65,7 @@ const val kayttajaTunnistePattern = "[a-zA-z0-9]{24}" class HankeKayttajaServiceITest : DatabaseTest() { @Autowired private lateinit var hankeKayttajaService: HankeKayttajaService + @Autowired private lateinit var permissionService: PermissionService @Autowired private lateinit var hankeFactory: HankeFactory @@ -59,7 +74,18 @@ class HankeKayttajaServiceITest : DatabaseTest() { @Autowired private lateinit var permissionRepository: PermissionRepository @Autowired private lateinit var auditLogRepository: AuditLogRepository - @Autowired private lateinit var permissionService: PermissionService + @MockkBean private lateinit var emailSenderService: EmailSenderService + + @BeforeEach + fun setup() { + clearAllMocks() + } + + @AfterEach + fun tearDown() { + checkUnnecessaryStub() + confirmVerified(emailSenderService) + } @Nested inner class GetKayttajatByHankeId { @@ -95,6 +121,61 @@ class HankeKayttajaServiceITest : DatabaseTest() { } } + @Nested + inner class GetKayttajaByCurrentUser { + + @Test + fun `When user exists should return current hanke user`() { + val hankeWithApplications = hankeFactory.saveGenerated(userId = USERNAME) + + val result: HankeKayttajaDto? = + hankeKayttajaService.getKayttajaByCurrentUser( + hankeId = hankeWithApplications.hanke.id!! + ) + + assertThat(result).isNotNull() + with(result!!) { + assertThat(id).isNotNull() + assertThat(sahkoposti).isEqualTo(teppoEmail) + assertThat(nimi).isEqualTo(TEPPO_TESTI) + assertThat(kayttooikeustaso).isEqualTo(Kayttooikeustaso.KAIKKI_OIKEUDET) + assertThat(tunnistautunut).isTrue() + } + } + + @Test + fun `When no hanke should return null`() { + val result: HankeKayttajaDto? = + hankeKayttajaService.getKayttajaByCurrentUser(hankeId = 123) + + assertThat(result).isNull() + } + + @Test + fun `When no related permission should return null`() { + val hanke = hankeFactory.save() + permissionRepository.deleteAll() + + val result: HankeKayttajaDto? = + hankeKayttajaService.getKayttajaByCurrentUser(hankeId = hanke.id!!) + + assertThat(result).isNull() + } + + @Test + fun `When no kayttaja should return null`() { + val hankeWithApplications = hankeFactory.saveGenerated(userId = USERNAME) + val hankeId = hankeWithApplications.hanke.id!! + val jou = hankeKayttajaService.getKayttajaByCurrentUser(hankeId)!! + hankeKayttajaRepository.deleteById(jou.id) + + val result: HankeKayttajaDto? = + hankeKayttajaService.getKayttajaByCurrentUser(hankeId = hankeId) + + assertThat(result).isNull() + } + } + @Nested inner class AddHankeFounder { private val perustaja = HankeFactory.defaultPerustaja @@ -371,6 +452,27 @@ class HankeKayttajaServiceITest : DatabaseTest() { "ali.kontakti@meili.com", ) } + + @Test + fun `Sends emails for new hanke users`() { + val hanke = hankeFactory.saveGenerated(userId = USERNAME).hanke + val hankeWithYhteystiedot = hanke.withYhteystiedot() // 4 sub contacts + val capturedEmails = mutableListOf() + justRun { emailSenderService.sendHankeInvitationEmail(capture(capturedEmails)) } + + hankeKayttajaService.saveNewTokensFromHanke(hankeWithYhteystiedot) + + verify(exactly = 4) { emailSenderService.sendHankeInvitationEmail(any()) } + assertThat(capturedEmails).each { inv -> + inv.transform { it.inviterName }.isEqualTo(TEPPO_TESTI) + inv.transform { it.inviterEmail }.isEqualTo(teppoEmail) + inv.transform { it.recipientEmail } + .isIn("yhteys-email1", "yhteys-email2", "yhteys-email3", "yhteys-email4") + inv.transform { it.hankeTunnus }.isEqualTo(hanke.hankeTunnus!!) + inv.transform { it.hankeNimi }.isEqualTo(defaultApplicationName) + inv.transform { it.invitationToken }.isNotEmpty() + } + } } @Nested diff --git a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/HankeController.kt b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/HankeController.kt index e93e622f0..1ba74350f 100644 --- a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/HankeController.kt +++ b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/HankeController.kt @@ -182,7 +182,11 @@ When Hanke is created: if (hanke == null) { throw HankeArgumentException("No hanke given when creating hanke") } - val sanitizedHanke = hanke.copy(id = null, generated = false) + val sanitizedHanke = + hanke.apply { + id = null + generated = false + } val userId = currentUserId() logger.info { "Creating Hanke for user $userId: ${hanke.toLogString()} " } diff --git a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/email/EmailSenderService.kt b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/email/EmailSenderService.kt index 88d6ed3d5..e77f01f8e 100644 --- a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/email/EmailSenderService.kt +++ b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/email/EmailSenderService.kt @@ -27,7 +27,7 @@ data class EmailFilterProperties( ) data class ApplicationInvitationData( - val inviterName: String?, + val inviterName: String, val inviterEmail: String, val recipientEmail: String, val applicationType: ApplicationType, @@ -37,7 +37,7 @@ data class ApplicationInvitationData( ) data class HankeInvitationData( - val inviterName: String?, + val inviterName: String, val inviterEmail: String, val recipientEmail: String, val hankeTunnus: String, @@ -80,7 +80,7 @@ class EmailSenderService( val templateData = mapOf( "baseUrl" to emailConfig.baseUrl, - "inviterInfo" to defineInviterInfo(data.inviterName, data.inviterEmail), + "inviterInfo" to inviterInfo(data.inviterName, data.inviterEmail), "hankeTunnus" to data.hankeTunnus, "hankeNimi" to data.hankeNimi, "invitationToken" to data.invitationToken, @@ -97,7 +97,7 @@ class EmailSenderService( val templateData = mapOf( "baseUrl" to emailConfig.baseUrl, - "inviterInfo" to defineInviterInfo(data.inviterName, data.inviterEmail), + "inviterInfo" to inviterInfo(data.inviterName, data.inviterEmail), "applicationType" to applicationTypeText, "applicationIdentifier" to data.applicationIdentifier, "hankeTunnus" to data.hankeTunnus, @@ -127,8 +127,7 @@ class EmailSenderService( mailSender.send(mimeMessage) } - private fun defineInviterInfo(name: String?, email: String): String = - if (name.isNullOrBlank()) "Asioija $email" else "$name ($email)" + private fun inviterInfo(name: String, email: String): String = "$name ($email)" private fun convertApplicationTypeFinnish(type: ApplicationType): String = when (type) { diff --git a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/permissions/HankeKayttaja.kt b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/permissions/HankeKayttaja.kt index 3ac487eef..e5e64bbc7 100644 --- a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/permissions/HankeKayttaja.kt +++ b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/permissions/HankeKayttaja.kt @@ -68,4 +68,6 @@ interface HankeKayttajaRepository : JpaRepository { hankeId: Int, sahkopostit: List ): List + + fun findByPermissionId(permissionId: Int): HankeKayttajaEntity? } diff --git a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/permissions/HankeKayttajaService.kt b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/permissions/HankeKayttajaService.kt index 842499883..02303dba1 100644 --- a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/permissions/HankeKayttajaService.kt +++ b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/permissions/HankeKayttajaService.kt @@ -4,8 +4,11 @@ import fi.hel.haitaton.hanke.HankeArgumentException import fi.hel.haitaton.hanke.application.ApplicationEntity import fi.hel.haitaton.hanke.configuration.Feature import fi.hel.haitaton.hanke.configuration.FeatureFlags +import fi.hel.haitaton.hanke.currentUserId import fi.hel.haitaton.hanke.domain.Hanke import fi.hel.haitaton.hanke.domain.Perustaja +import fi.hel.haitaton.hanke.email.EmailSenderService +import fi.hel.haitaton.hanke.email.HankeInvitationData import fi.hel.haitaton.hanke.logging.HankeKayttajaLoggingService import java.util.UUID import mu.KotlinLogging @@ -21,12 +24,33 @@ class HankeKayttajaService( private val permissionService: PermissionService, private val featureFlags: FeatureFlags, private val logService: HankeKayttajaLoggingService, + private val emailSenderService: EmailSenderService, ) { @Transactional(readOnly = true) fun getKayttajatByHankeId(hankeId: Int): List = hankeKayttajaRepository.findByHankeId(hankeId).map { it.toDto() } + @Transactional(readOnly = true) + fun getKayttajaByCurrentUser(hankeId: Int): HankeKayttajaDto? { + val currentUserId = currentUserId() + val permission = permissionService.findPermission(hankeId, currentUserId) + if (permission == null) { + logger.warn { + "UserId=$currentUserId does not have a permission instance for HankeId=$hankeId" + } + return null + } + + val hankeKayttaja = hankeKayttajaRepository.findByPermissionId(permission.id) + if (hankeKayttaja == null) { + logger.warn { "No kayttaja instance found (hankeId=$hankeId, userId=$currentUserId) " } + return null + } + + return hankeKayttaja.toDto() + } + @Transactional fun saveNewTokensFromApplication(application: ApplicationEntity, hankeId: Int) { if (featureFlags.isDisabled(Feature.USER_MANAGEMENT)) { @@ -41,7 +65,9 @@ class HankeKayttajaService( .flatMap { it.contacts } .mapNotNull { userContactOrNull(it.fullName(), it.email) } - filterNewContacts(hankeId, contacts).forEach { contact -> createToken(hankeId, contact) } + filterNewContacts(hankeId, contacts).forEach { contact -> + createHankeKayttaja(hankeId, contact) + } } @Transactional @@ -59,7 +85,12 @@ class HankeKayttajaService( .flatMap { it.alikontaktit } .mapNotNull { userContactOrNull(it.fullName(), it.email) } - filterNewContacts(hankeId, contacts).forEach { contact -> createToken(hankeId, contact) } + filterNewContacts(hankeId, contacts) + .map { contact -> createHankeKayttaja(hankeId, contact) } + .also { kayttajaList -> + val inviter = getKayttajaByCurrentUser(hankeId) + sendHankeInvitationEmails(hanke, inviter, kayttajaList) + } } @Transactional @@ -222,13 +253,13 @@ class HankeKayttajaService( } } - private fun createToken(hankeId: Int, contact: UserContact) { + private fun createHankeKayttaja(hankeId: Int, contact: UserContact): HankeKayttajaEntity { logger.info { "Creating a new user token, hankeId=$hankeId" } val token = KayttajaTunnisteEntity.create() val kayttajaTunnisteEntity = kayttajaTunnisteRepository.save(token) logger.info { "Saved the new user token, id=${kayttajaTunnisteEntity.id}" } - saveUser( + return saveUser( HankeKayttajaEntity( hankeId = hankeId, nimi = contact.name, @@ -239,11 +270,39 @@ class HankeKayttajaService( ) } - private fun saveUser(hankeKayttajaEntity: HankeKayttajaEntity) { - val user = hankeKayttajaRepository.save(hankeKayttajaEntity) - logger.info { "Saved the user information, id=${user.id}" } + private fun sendHankeInvitationEmails( + hanke: Hanke, + inviter: HankeKayttajaDto?, + kayttajat: List + ) { + logger.info { "Sending Hanke invitations." } + + if (inviter == null || kayttajat.isEmpty()) { + logger.info { + "Inviter=${inviter?.id}, kayttajat size=${kayttajat.size}. Won't send invitations." + } + return + } + + kayttajat.forEach { recipient -> + emailSenderService.sendHankeInvitationEmail( + HankeInvitationData( + inviterName = inviter.nimi, + inviterEmail = inviter.sahkoposti, + recipientEmail = recipient.sahkoposti, + hankeTunnus = hanke.hankeTunnus!!, + hankeNimi = hanke.nimi!!, + invitationToken = recipient.kayttajaTunniste!!.tunniste, + ) + ) + } } + private fun saveUser(hankeKayttajaEntity: HankeKayttajaEntity): HankeKayttajaEntity = + hankeKayttajaRepository.save(hankeKayttajaEntity).also { user -> + logger.info { "Saved the user information, id=${user.id}" } + } + private fun userContactOrNull(name: String?, email: String?): UserContact? { return when { name.isNullOrBlank() || email.isNullOrBlank() -> null diff --git a/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/factory/HankeFactory.kt b/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/factory/HankeFactory.kt index 606e61e1d..6a63c9b6c 100644 --- a/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/factory/HankeFactory.kt +++ b/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/factory/HankeFactory.kt @@ -7,6 +7,7 @@ import fi.hel.haitaton.hanke.HankeStatus import fi.hel.haitaton.hanke.SuunnitteluVaihe import fi.hel.haitaton.hanke.TyomaaTyyppi import fi.hel.haitaton.hanke.Vaihe +import fi.hel.haitaton.hanke.application.CableReportWithoutHanke import fi.hel.haitaton.hanke.domain.Hanke import fi.hel.haitaton.hanke.domain.HankeYhteystieto import fi.hel.haitaton.hanke.domain.Hankealue @@ -56,6 +57,12 @@ class HankeFactory( fun save(hanke: Hanke) = hankeService.createHanke(hanke) + fun saveGenerated( + cableReportWithoutHanke: CableReportWithoutHanke = + AlluDataFactory.cableReportWithoutHanke(), + userId: String + ) = hankeService.generateHankeWithApplication(cableReportWithoutHanke, userId) + companion object { const val defaultHankeTunnus = "HAI21-1" @@ -100,12 +107,6 @@ class HankeFactory( hankeStatus, ) - /** Create minimal Entity with identifier fields and mandatory fields. */ - fun createMinimalEntity( - id: Int? = defaultId, - hankeTunnus: String? = defaultHankeTunnus, - ) = HankeEntity(id = id, hankeTunnus = hankeTunnus) - /** * Add a hankealue with haitat to a test Hanke. *