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.
*