From b556a3f6db7dddcd1d268f50cdba821e7a6e1c68 Mon Sep 17 00:00:00 2001 From: Topias Heinonen Date: Fri, 17 Nov 2023 14:11:08 +0200 Subject: [PATCH] HAI Remove @MockkBean and @SpykBean annotations Configure the Spring Context in integration tests to use a mockk instance of CableReportService so the mock can be `@Autowired`. CableReportService was defined in several integration tests as a `@MockkBean` dependency. The actual implementation was only used in `CableReportServiceITests`, which constructs its own custom instance. The other class that was injected with `@MockkBean` was `EmailSenderService`. Since there's an easy-to-use extension for mocking an actual email server, it felt better to use that instead of mocking the service universally. Also, unlike `CableReportServiceITests`, `EmailSenderServiceITest` uses the Spring context, so a context with the actual implementation is needed anyway. Each unique combination of `@MockkBeans` and `@SpykBeans` (or the Mockito equilevants) makes Spring create a new context for the test. While not a huge time sink, creating (and destroying) the contexts adds up. We can clearly see how many different contexts are created from the number of SQL connection pools that need to be shut down at the end of the tests. There are situations when they have to be used, but using alternative approaches is usually better. New contexts are also created whenever the configuration parameters are different between tests. This is sometimes unavoidable when we need to test how the application behaves with different configurations. Ever since #397 we've been able to use application.test.properties to configure the test environment, so we don't need to use test-specific properties to diverge from the production configuration. There was a properties parameter like remaining in `TestDataServiceITest`, so that was removed. There's one more service test with custom properties (`EmailSenderServicePropertiesITest`), but in that test we're interested in testing that the properties have the effect we want. And the properties are different from how we generally want to configure EmailSenderService in tests. So the custom properties are unavoidable. Controller tests are cheaper to create with varying configurations. Since only a very limited context is created for running them, recreating it is not very expensive. So testing things like feature flags on the controller level doesn't seem problematic. --- .../hel/haitaton/hanke/HankeServiceITests.kt | 4 +- .../application/ApplicationServiceITest.kt | 62 ++++----- .../ApplicationAttachmentServiceITest.kt | 3 +- .../common/AttachmentUploadServiceITest.kt | 3 +- .../HankeAttachmentContentServiceITest.kt | 2 +- .../hanke/HankeAttachmentServiceITests.kt | 26 ---- .../configuration/MockCableReportService.kt | 11 ++ .../permissions/HankeKayttajaServiceITest.kt | 127 +++++++++--------- .../hanke/testdata/TestDataServiceITest.kt | 2 +- .../hanke/configuration/Configuration.kt | 2 + .../hanke/email/EmailSenderService.kt | 22 +-- .../hanke/email/EmailSenderServiceTest.kt | 2 + .../haitaton/hanke/factory/AlluDataFactory.kt | 2 +- .../hanke/factory/UserContactFactory.kt | 2 +- .../fi/hel/haitaton/hanke/test/Asserts.kt | 7 + .../resources/application-test.properties | 1 + 16 files changed, 134 insertions(+), 144 deletions(-) create mode 100644 services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/configuration/MockCableReportService.kt diff --git a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/HankeServiceITests.kt b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/HankeServiceITests.kt index b5743565d..fc84f1e33 100644 --- a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/HankeServiceITests.kt +++ b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/HankeServiceITests.kt @@ -13,7 +13,6 @@ import assertk.assertions.isNotNull import assertk.assertions.isNull import assertk.assertions.messageContains import assertk.assertions.prop -import com.ninjasquad.springmockk.MockkBean import fi.hel.haitaton.hanke.allu.ApplicationStatus import fi.hel.haitaton.hanke.allu.CableReportService import fi.hel.haitaton.hanke.application.ALLU_USER_CANCELLATION_MSG @@ -123,10 +122,9 @@ class HankeServiceITests( @Autowired private val fileClient: MockFileClient, @Autowired private val hankeFactory: HankeFactory, @Autowired private val hankeAttachmentFactory: HankeAttachmentFactory, + @Autowired private val cableReportService: CableReportService, ) : DatabaseTest() { - @MockkBean private lateinit var cableReportService: CableReportService - @BeforeEach fun clearMocks() { clearAllMocks() diff --git a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/application/ApplicationServiceITest.kt b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/application/ApplicationServiceITest.kt index b948db959..a1998eec1 100644 --- a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/application/ApplicationServiceITest.kt +++ b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/application/ApplicationServiceITest.kt @@ -3,6 +3,7 @@ package fi.hel.haitaton.hanke.application import assertk.Assert import assertk.all import assertk.assertThat +import assertk.assertions.contains import assertk.assertions.containsExactly import assertk.assertions.containsExactlyInAnyOrder import assertk.assertions.each @@ -21,8 +22,6 @@ import assertk.assertions.prop import com.icegreen.greenmail.configuration.GreenMailConfiguration import com.icegreen.greenmail.junit5.GreenMailExtension import com.icegreen.greenmail.util.ServerSetupTest -import com.ninjasquad.springmockk.MockkBean -import com.ninjasquad.springmockk.SpykBean import fi.hel.haitaton.hanke.DatabaseTest import fi.hel.haitaton.hanke.HankeEntity import fi.hel.haitaton.hanke.HankeNotFoundException @@ -43,8 +42,8 @@ import fi.hel.haitaton.hanke.application.ApplicationType.CABLE_REPORT import fi.hel.haitaton.hanke.asJsonResource import fi.hel.haitaton.hanke.asUtc import fi.hel.haitaton.hanke.domain.Hankealue -import fi.hel.haitaton.hanke.email.ApplicationNotificationData -import fi.hel.haitaton.hanke.email.EmailSenderService +import fi.hel.haitaton.hanke.email.EmailSenderService.Companion.translations +import fi.hel.haitaton.hanke.email.textBody import fi.hel.haitaton.hanke.factory.AlluDataFactory import fi.hel.haitaton.hanke.factory.AlluDataFactory.Companion.asianHoitajaCustomerContact import fi.hel.haitaton.hanke.factory.AlluDataFactory.Companion.cableReportWithoutHanke @@ -82,6 +81,7 @@ import fi.hel.haitaton.hanke.permissions.Kayttooikeustaso import fi.hel.haitaton.hanke.permissions.Kayttooikeustaso.HAKEMUSASIOINTI import fi.hel.haitaton.hanke.permissions.PermissionService import fi.hel.haitaton.hanke.permissions.kayttajaTunnistePattern +import fi.hel.haitaton.hanke.test.Asserts.hasReceivers import fi.hel.haitaton.hanke.test.Asserts.hasSingleGeometryWithCoordinates import fi.hel.haitaton.hanke.test.Asserts.isRecent import fi.hel.haitaton.hanke.test.AuditLogEntryEntityAsserts.hasUserActor @@ -98,6 +98,7 @@ import io.mockk.justRun import io.mockk.verify import io.mockk.verifyOrder import io.mockk.verifySequence +import jakarta.mail.internet.MimeMessage import java.time.OffsetDateTime import java.time.ZonedDateTime import org.geojson.Polygon @@ -132,12 +133,10 @@ private val dataWithoutAreas = createCableReportApplicationData(areas = listOf() @WithMockUser(USERNAME) class ApplicationServiceITest : DatabaseTest() { - @MockkBean private lateinit var cableReportServiceAllu: CableReportService - @SpykBean private lateinit var emailSenderService: EmailSenderService - @Autowired private lateinit var applicationService: ApplicationService @Autowired private lateinit var hankeService: HankeService @Autowired private lateinit var permissionService: PermissionService + @Autowired private lateinit var cableReportServiceAllu: CableReportService @Autowired private lateinit var applicationRepository: ApplicationRepository @Autowired private lateinit var hankeRepository: HankeRepository @@ -638,15 +637,10 @@ class ApplicationServiceITest : DatabaseTest() { USERNAME, ) setAlluFields(applicationRepository.findById(application.id!!).orElseThrow()) - val capturedNotifications = mutableListOf() justRun { cableReportServiceAllu.update(21, any()) } every { cableReportServiceAllu.getApplicationInformation(any()) } returns createAlluApplicationResponse(21) justRun { cableReportServiceAllu.addAttachment(21, any()) } - justRun { emailSenderService.sendHankeInvitationEmail(any()) } - justRun { - emailSenderService.sendApplicationNotificationEmail(capture(capturedNotifications)) - } val updatedApplication = cableReport.copy( representativeWithContacts = asianHoitajaCustomerContact, @@ -657,11 +651,11 @@ class ApplicationServiceITest : DatabaseTest() { applicationService.updateApplicationData(application.id!!, updatedApplication, USERNAME) + val capturedNotifications = getApplicationNotifications() assertThat(capturedNotifications) .areValid(application.applicationType, application.hankeTunnus) - assertThat(capturedNotifications) - .extracting { it.recipientEmail } - .containsExactlyInAnyOrder( + assertThat(capturedNotifications.toTypedArray()) + .hasReceivers( "new.mail@foo.fi", asianHoitajaCustomerContact.contacts[0].email, rakennuttajaCustomerContact.contacts[0].email @@ -671,8 +665,6 @@ class ApplicationServiceITest : DatabaseTest() { cableReportServiceAllu.update(21, any()) cableReportServiceAllu.addAttachment(any(), any()) } - verify(exactly = 3) { emailSenderService.sendHankeInvitationEmail(any()) } - verify(exactly = 3) { emailSenderService.sendApplicationNotificationEmail(any()) } } @Test @@ -714,7 +706,7 @@ class ApplicationServiceITest : DatabaseTest() { cableReportServiceAllu.update(21, any()) cableReportServiceAllu.addAttachment(any(), any()) } - verify { emailSenderService wasNot Called } + assertThat(greenMail.receivedMessages).isEmpty() } @Test @@ -1198,16 +1190,14 @@ class ApplicationServiceITest : DatabaseTest() { CableReportWithoutHanke(CABLE_REPORT, cableReportData), USERNAME, ) - val capturedEmails = mutableListOf() every { cableReportServiceAllu.create(any()) } returns 26 every { cableReportServiceAllu.getApplicationInformation(any()) } returns createAlluApplicationResponse(26) justRun { cableReportServiceAllu.addAttachment(26, any()) } - justRun { emailSenderService.sendHankeInvitationEmail(any()) } - justRun { emailSenderService.sendApplicationNotificationEmail(capture(capturedEmails)) } applicationService.sendApplication(application.id!!, USERNAME) + val capturedEmails = getApplicationNotifications() assertThat(capturedEmails).hasSize(3) // 4 contacts, but one is the sender assertThat(capturedEmails) .areValid(application.applicationType, application.hankeTunnus) @@ -1216,8 +1206,6 @@ class ApplicationServiceITest : DatabaseTest() { cableReportServiceAllu.addAttachment(any(), any()) cableReportServiceAllu.getApplicationInformation(any()) } - verify(exactly = 3) { emailSenderService.sendHankeInvitationEmail(any()) } - verify(exactly = 3) { emailSenderService.sendApplicationNotificationEmail(any()) } } @Test @@ -1856,16 +1844,24 @@ class ApplicationServiceITest : DatabaseTest() { } } - private fun Assert>.areValid( - type: ApplicationType, - hankeTunnus: String? - ) = each { data -> - data.transform { it.senderEmail }.isEqualTo(hakijaContact.email) - data.transform { it.senderName }.isEqualTo(hakijaContact.name) - data.transform { it.applicationIdentifier }.isEqualTo(defaultApplicationIdentifier) - data.transform { it.applicationType }.isEqualTo(type) - data.transform { it.recipientEmail }.isIn(*expectedRecipients) - data.transform { it.hankeTunnus }.isEqualTo(hankeTunnus) + private fun getApplicationNotifications() = + greenMail.receivedMessages.filter { + it.subject.startsWith("Haitaton: Sinut on lisätty hakemukselle") + } + + private fun Assert>.areValid(type: ApplicationType, hankeTunnus: String?) { + each { it.isValid(type, hankeTunnus) } + } + + private fun Assert.isValid(type: ApplicationType, hankeTunnus: String?) { + prop(MimeMessage::textBody).all { + contains("${hakijaContact.name} (${hakijaContact.email}) on tehnyt") + contains("hakemukselle $defaultApplicationIdentifier") + contains("on tehnyt ${type.translations().fi}") + contains("hankkeella $hankeTunnus") + } + + transform { it.allRecipients.first().toString() }.isIn(*expectedRecipients) } private fun initializedHanke(): HankeEntity = diff --git a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/attachment/application/ApplicationAttachmentServiceITest.kt b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/attachment/application/ApplicationAttachmentServiceITest.kt index 2dba3a8ae..96d743368 100644 --- a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/attachment/application/ApplicationAttachmentServiceITest.kt +++ b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/attachment/application/ApplicationAttachmentServiceITest.kt @@ -8,7 +8,6 @@ import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isNotNull import assertk.assertions.isPresent -import com.ninjasquad.springmockk.MockkBean import fi.hel.haitaton.hanke.ALLOWED_ATTACHMENT_COUNT import fi.hel.haitaton.hanke.DatabaseTest import fi.hel.haitaton.hanke.HankeEntity @@ -67,7 +66,7 @@ private const val ALLU_ID = 42 @ActiveProfiles("test") @WithMockUser(USERNAME) class ApplicationAttachmentServiceITest : DatabaseTest() { - @MockkBean private lateinit var cableReportService: CableReportService + @Autowired private lateinit var cableReportService: CableReportService @Autowired private lateinit var applicationAttachmentService: ApplicationAttachmentService @Autowired private lateinit var attachmentContentService: ApplicationAttachmentContentService @Autowired private lateinit var applicationAttachmentRepository: ApplicationAttachmentRepository diff --git a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/attachment/common/AttachmentUploadServiceITest.kt b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/attachment/common/AttachmentUploadServiceITest.kt index af2bffa95..560cea6de 100644 --- a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/attachment/common/AttachmentUploadServiceITest.kt +++ b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/attachment/common/AttachmentUploadServiceITest.kt @@ -22,6 +22,7 @@ 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.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.security.test.context.support.WithMockUser @@ -30,6 +31,7 @@ import org.springframework.test.context.ActiveProfiles @SpringBootTest @ActiveProfiles("test") @WithMockUser(USERNAME) +@ExtendWith(MockFileClientExtension::class) class AttachmentUploadServiceITest( @Autowired private val attachmentUploadService: AttachmentUploadService, @Autowired private val attachmentRepository: HankeAttachmentRepository, @@ -41,7 +43,6 @@ class AttachmentUploadServiceITest( @BeforeEach fun setup() { - fileClient.recreateContainers() mockClamAv = MockWebServer() mockClamAv.start(6789) } diff --git a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/attachment/hanke/HankeAttachmentContentServiceITest.kt b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/attachment/hanke/HankeAttachmentContentServiceITest.kt index f20d44ec0..cb345eb97 100644 --- a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/attachment/hanke/HankeAttachmentContentServiceITest.kt +++ b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/attachment/hanke/HankeAttachmentContentServiceITest.kt @@ -32,7 +32,7 @@ import org.springframework.http.MediaType import org.springframework.security.test.context.support.WithMockUser import org.springframework.test.context.ActiveProfiles -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@SpringBootTest @ActiveProfiles("test") @WithMockUser(USERNAME) @ExtendWith(MockFileClientExtension::class) diff --git a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/attachment/hanke/HankeAttachmentServiceITests.kt b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/attachment/hanke/HankeAttachmentServiceITests.kt index c8d233b86..2460c61a6 100644 --- a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/attachment/hanke/HankeAttachmentServiceITests.kt +++ b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/attachment/hanke/HankeAttachmentServiceITests.kt @@ -21,7 +21,6 @@ import fi.hel.haitaton.hanke.attachment.DEFAULT_DATA import fi.hel.haitaton.hanke.attachment.FILE_NAME_PDF import fi.hel.haitaton.hanke.attachment.USERNAME import fi.hel.haitaton.hanke.attachment.azure.Container.HANKE_LIITTEET -import fi.hel.haitaton.hanke.attachment.body import fi.hel.haitaton.hanke.attachment.common.AttachmentInvalidException import fi.hel.haitaton.hanke.attachment.common.AttachmentNotFoundException import fi.hel.haitaton.hanke.attachment.common.HankeAttachmentContentRepository @@ -29,17 +28,12 @@ import fi.hel.haitaton.hanke.attachment.common.HankeAttachmentEntity import fi.hel.haitaton.hanke.attachment.common.HankeAttachmentRepository import fi.hel.haitaton.hanke.attachment.common.MockFileClient import fi.hel.haitaton.hanke.attachment.common.MockFileClientExtension -import fi.hel.haitaton.hanke.attachment.response -import fi.hel.haitaton.hanke.attachment.successResult import fi.hel.haitaton.hanke.factory.AttachmentFactory import fi.hel.haitaton.hanke.factory.HankeAttachmentFactory import fi.hel.haitaton.hanke.factory.HankeFactory import fi.hel.haitaton.hanke.factory.HankeIdentifierFactory import java.time.OffsetDateTime import java.util.UUID -import okhttp3.mockwebserver.MockWebServer -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.junit.jupiter.api.extension.ExtendWith @@ -61,25 +55,10 @@ class HankeAttachmentServiceITests( @Autowired private val hankeAttachmentFactory: HankeAttachmentFactory, ) : DatabaseTest() { - private lateinit var mockClamAv: MockWebServer - - @BeforeEach - fun setup() { - mockClamAv = MockWebServer() - mockClamAv.start(6789) - } - - @AfterEach - fun tearDown() { - mockClamAv.shutdown() - } - @Nested inner class GetMetadataList { @Test fun `getMetadataList should return related metadata list`() { - mockClamAv.enqueue(response(body(results = successResult()))) - mockClamAv.enqueue(response(body(results = successResult()))) val hanke = hankeFactory.saveEntity() (1..2).forEach { _ -> hankeAttachmentRepository.save( @@ -150,7 +129,6 @@ class HankeAttachmentServiceITests( assertThat(result.fileName).isEqualTo(FILE_NAME_PDF) assertThat(result.contentType).isEqualTo(APPLICATION_PDF_VALUE) assertThat(result.bytes).isEqualTo(DEFAULT_DATA) - assertThat(mockClamAv.requestCount).isEqualTo(0) } } } @@ -159,7 +137,6 @@ class HankeAttachmentServiceITests( inner class SaveAttachment { @Test fun `Should return metadata of saved attachment`() { - mockClamAv.enqueue(response(body(results = successResult()))) val hanke = hankeFactory.save() val blobPath = blobPath(hanke.id) @@ -188,7 +165,6 @@ class HankeAttachmentServiceITests( @Test fun `Should throw if attachment amount is exceeded`() { - mockClamAv.enqueue(response(body(results = successResult()))) val hanke = hankeFactory.saveEntity() (1..ALLOWED_ATTACHMENT_COUNT) .map { AttachmentFactory.hankeAttachmentEntity(hanke = hanke) } @@ -210,8 +186,6 @@ class HankeAttachmentServiceITests( @Test fun `Should fail when there is no related hanke`() { - mockClamAv.enqueue(response(body(results = successResult()))) - assertFailure { hankeAttachmentService.saveAttachment( hankeTunnus = "HAI-123", diff --git a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/configuration/MockCableReportService.kt b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/configuration/MockCableReportService.kt new file mode 100644 index 000000000..233e20ea8 --- /dev/null +++ b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/configuration/MockCableReportService.kt @@ -0,0 +1,11 @@ +package fi.hel.haitaton.hanke.configuration + +import fi.hel.haitaton.hanke.allu.CableReportService +import io.mockk.mockk +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class MockCableReportService { + @Bean fun cableReportService(): CableReportService = mockk() +} 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 dd1770d71..9a3548722 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 @@ -4,8 +4,10 @@ import assertk.Assert import assertk.all import assertk.assertFailure import assertk.assertThat +import assertk.assertions.contains import assertk.assertions.containsExactly import assertk.assertions.containsExactlyInAnyOrder +import assertk.assertions.containsMatch import assertk.assertions.each import assertk.assertions.exactly import assertk.assertions.first @@ -14,28 +16,27 @@ import assertk.assertions.hasSize import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isIn -import assertk.assertions.isNotEmpty import assertk.assertions.isNotEqualTo import assertk.assertions.isNotNull import assertk.assertions.isNull import assertk.assertions.matches import assertk.assertions.messageContains import assertk.assertions.prop -import com.ninjasquad.springmockk.MockkBean +import com.icegreen.greenmail.configuration.GreenMailConfiguration +import com.icegreen.greenmail.junit5.GreenMailExtension +import com.icegreen.greenmail.util.ServerSetupTest import fi.hel.haitaton.hanke.DatabaseTest import fi.hel.haitaton.hanke.application.ApplicationRepository import fi.hel.haitaton.hanke.application.ApplicationType import fi.hel.haitaton.hanke.application.CableReportWithoutHanke -import fi.hel.haitaton.hanke.email.EmailSenderService -import fi.hel.haitaton.hanke.email.HankeInvitationData +import fi.hel.haitaton.hanke.domain.Hanke +import fi.hel.haitaton.hanke.email.textBody import fi.hel.haitaton.hanke.factory.AlluDataFactory.Companion.asianHoitajaCustomerContact import fi.hel.haitaton.hanke.factory.AlluDataFactory.Companion.createApplicationEntity import fi.hel.haitaton.hanke.factory.AlluDataFactory.Companion.createCableReportApplicationData import fi.hel.haitaton.hanke.factory.AlluDataFactory.Companion.createCompanyCustomer import fi.hel.haitaton.hanke.factory.AlluDataFactory.Companion.createContact import fi.hel.haitaton.hanke.factory.AlluDataFactory.Companion.defaultApplicationIdentifier -import fi.hel.haitaton.hanke.factory.AlluDataFactory.Companion.defaultApplicationName -import fi.hel.haitaton.hanke.factory.AlluDataFactory.Companion.expectedRecipients import fi.hel.haitaton.hanke.factory.AlluDataFactory.Companion.hakijaCustomerContact import fi.hel.haitaton.hanke.factory.AlluDataFactory.Companion.rakennuttajaCustomerContact import fi.hel.haitaton.hanke.factory.AlluDataFactory.Companion.suorittajaCustomerContact @@ -56,6 +57,7 @@ import fi.hel.haitaton.hanke.logging.AuditLogTarget 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.hasReceivers import fi.hel.haitaton.hanke.test.Asserts.isRecent import fi.hel.haitaton.hanke.test.AuditLogEntryEntityAsserts.auditEvent import fi.hel.haitaton.hanke.test.AuditLogEntryEntityAsserts.hasId @@ -65,18 +67,11 @@ import fi.hel.haitaton.hanke.test.AuditLogEntryEntityAsserts.hasUserActor import fi.hel.haitaton.hanke.test.AuditLogEntryEntityAsserts.isSuccess import fi.hel.haitaton.hanke.test.AuditLogEntryEntityAsserts.withTarget import fi.hel.haitaton.hanke.toChangeLogJsonString -import io.mockk.Called -import io.mockk.checkUnnecessaryStub -import io.mockk.clearAllMocks -import io.mockk.confirmVerified -import io.mockk.justRun -import io.mockk.verify -import io.mockk.verifySequence +import jakarta.mail.internet.MimeMessage 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.junit.jupiter.api.extension.RegisterExtension import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.security.test.context.support.WithMockUser @@ -102,17 +97,12 @@ class HankeKayttajaServiceITest : DatabaseTest() { @Autowired private lateinit var auditLogRepository: AuditLogRepository @Autowired private lateinit var applicationRepository: ApplicationRepository - @MockkBean private lateinit var emailSenderService: EmailSenderService - - @BeforeEach - fun setup() { - clearAllMocks() - } - - @AfterEach - fun tearDown() { - checkUnnecessaryStub() - confirmVerified(emailSenderService) + companion object { + @JvmField + @RegisterExtension + val greenMail: GreenMailExtension = + GreenMailExtension(ServerSetupTest.SMTP) + .withConfiguration(GreenMailConfiguration.aConfig().withDisabledAuthentication()) } @Nested @@ -382,7 +372,7 @@ class HankeKayttajaServiceITest : DatabaseTest() { currentKayttaja = null ) - verify { emailSenderService wasNot Called } + assertThat(greenMail.receivedMessages).isEmpty() } @Test @@ -401,8 +391,6 @@ class HankeKayttajaServiceITest : DatabaseTest() { applicationRepository.findAll().first().apply { applicationIdentifier = defaultApplicationIdentifier } - val capturedEmails = mutableListOf() - justRun { emailSenderService.sendHankeInvitationEmail(capture(capturedEmails)) } val inviter = with(hakijaContact) { HankeKayttajaFactory.createEntity(nimi = name, sahkoposti = email) @@ -417,16 +405,17 @@ class HankeKayttajaServiceITest : DatabaseTest() { inviter ) - assertThat(capturedEmails).each { inv -> - inv.transform { it.inviterEmail }.isEqualTo(hakijaContact.email) - inv.transform { it.inviterName }.isEqualTo(hakijaContact.name) - inv.transform { it.invitationToken }.isNotEmpty() - inv.transform { it.recipientEmail }.isIn(*expectedRecipients) - inv.transform { it.hankeTunnus }.isEqualTo(hanke.hankeTunnus) - inv.transform { it.hankeNimi }.isEqualTo(hanke.nimi) - } + val capturedEmails = greenMail.receivedMessages // 4 contacts but one is the sender - verify(exactly = 3) { emailSenderService.sendHankeInvitationEmail(any()) } + assertThat(capturedEmails).hasSize(3) + assertThat(capturedEmails) + .areValidHankeInvitations(hakijaContact.name, hakijaContact.email, hanke) + assertThat(capturedEmails) + .hasReceivers( + suorittajaCustomerContact.contacts[0].email, + asianHoitajaCustomerContact.contacts[0].email, + rakennuttajaCustomerContact.contacts[0].email, + ) } @Test @@ -694,21 +683,14 @@ class HankeKayttajaServiceITest : DatabaseTest() { fun `Sends emails for new hanke users`() { val hanke = hankeFactory.saveGenerated(userId = USERNAME) val hankeWithYhteystiedot = hanke.withYhteystiedot() // 4 sub contacts - val capturedEmails = mutableListOf() - justRun { emailSenderService.sendHankeInvitationEmail(capture(capturedEmails)) } hankeKayttajaService.saveNewTokensFromHanke(hankeWithYhteystiedot, USERNAME) - 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() - } + val capturedEmails = greenMail.receivedMessages + assertThat(capturedEmails).hasSize(4) + assertThat(capturedEmails).areValidHankeInvitations(TEPPO_TESTI, teppoEmail, hanke) + assertThat(capturedEmails) + .hasReceivers("yhteys-email1", "yhteys-email2", "yhteys-email3", "yhteys-email4") } } @@ -1251,14 +1233,12 @@ class HankeKayttajaServiceITest : DatabaseTest() { ) val kayttaja = kayttajaFactory.saveUser(hanke.id) assertThat(kayttajaTunnisteRepository.findAll()).isEmpty() - justRun { emailSenderService.sendHankeInvitationEmail(any()) } hankeKayttajaService.resendInvitation(kayttaja.id, USERNAME) val tunnisteet = kayttajaTunnisteRepository.findAll() assertThat(tunnisteet).hasSize(1) assertThat(tunnisteet).first().hasKayttajaWithId(kayttaja.id) - verifySequence { emailSenderService.sendHankeInvitationEmail(any()) } } @Test @@ -1273,7 +1253,6 @@ class HankeKayttajaServiceITest : DatabaseTest() { assertThat(kayttajaTunnisteRepository.findAll()).hasSize(1) val tunnisteId = kayttaja.kayttajaTunniste!!.id val tunniste = kayttaja.kayttajaTunniste!!.tunniste - justRun { emailSenderService.sendHankeInvitationEmail(any()) } hankeKayttajaService.resendInvitation(kayttaja.id, USERNAME) @@ -1284,7 +1263,6 @@ class HankeKayttajaServiceITest : DatabaseTest() { prop(KayttajaTunnisteEntity::id).isNotEqualTo(tunnisteId) prop(KayttajaTunnisteEntity::tunniste).isNotEqualTo(tunniste) } - verifySequence { emailSenderService.sendHankeInvitationEmail(any()) } } @Test @@ -1297,21 +1275,19 @@ class HankeKayttajaServiceITest : DatabaseTest() { nimi = "Current User" ) val kayttaja = kayttajaFactory.saveUser(hanke.id) - val capturedEmails = mutableListOf() - justRun { emailSenderService.sendHankeInvitationEmail(capture(capturedEmails)) } hankeKayttajaService.resendInvitation(kayttaja.id, USERNAME) + val capturedEmails = greenMail.receivedMessages assertThat(capturedEmails).hasSize(1) - assertThat(capturedEmails).first().all { - prop(HankeInvitationData::inviterEmail).isEqualTo("current@user") - prop(HankeInvitationData::inviterName).isEqualTo("Current User") - prop(HankeInvitationData::recipientEmail).isEqualTo(kayttaja.sahkoposti) - prop(HankeInvitationData::hankeTunnus).isEqualTo(hanke.hankeTunnus) - prop(HankeInvitationData::hankeNimi).isEqualTo(hanke.nimi) - prop(HankeInvitationData::invitationToken).isNotNull() - } - verifySequence { emailSenderService.sendHankeInvitationEmail(any()) } + assertThat(capturedEmails[0]) + .isValidHankeInvitation( + "Current User", + "current@user", + hanke.nimi, + hanke.hankeTunnus + ) + assertThat(capturedEmails).hasReceivers(kayttaja.sahkoposti) } } @@ -1338,6 +1314,27 @@ class HankeKayttajaServiceITest : DatabaseTest() { k.prop(HankeKayttajaEntity::kayttajaTunniste).isNotNull() } + private fun Assert>.areValidHankeInvitations( + inviterName: String, + inviterEmail: String, + hanke: Hanke + ) { + each { it.isValidHankeInvitation(inviterName, inviterEmail, hanke.nimi, hanke.hankeTunnus) } + } + + private fun Assert.isValidHankeInvitation( + inviterName: String, + inviterEmail: String, + hankeNimi: String, + hankeTunnus: String, + ) { + prop(MimeMessage::textBody).all { + contains("$inviterName ($inviterEmail) lisäsi sinut") + containsMatch("kutsu\\?id=$kayttajaTunnistePattern".toRegex()) + contains("hankkeelle $hankeNimi ($hankeTunnus)") + } + } + private val expectedNames = arrayOf( "yhteys-etu1 yhteys-suku1", diff --git a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/testdata/TestDataServiceITest.kt b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/testdata/TestDataServiceITest.kt index c5c0fa850..3ea1673c4 100644 --- a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/testdata/TestDataServiceITest.kt +++ b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/testdata/TestDataServiceITest.kt @@ -19,7 +19,7 @@ import org.springframework.test.context.ActiveProfiles private const val USERNAME = "testUser" -@SpringBootTest(properties = ["haitaton.testdata.enabled=true"]) +@SpringBootTest @ActiveProfiles("test") @WithMockUser(USERNAME) class TestDataServiceITest : DatabaseTest() { diff --git a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/configuration/Configuration.kt b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/configuration/Configuration.kt index 4d47fa613..2eb7751de 100644 --- a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/configuration/Configuration.kt +++ b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/configuration/Configuration.kt @@ -14,6 +14,7 @@ import org.springframework.beans.factory.annotation.Value import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Profile import org.springframework.http.client.reactive.ReactorClientHttpConnector import org.springframework.web.reactive.function.client.WebClient import reactor.netty.http.client.HttpClient @@ -33,6 +34,7 @@ class Configuration { @Bean fun ioDispatcher(): CoroutineDispatcher = Dispatchers.IO @Bean + @Profile("!test") fun cableReportService(webClientBuilder: WebClient.Builder): CableReportService { val webClient = webClientWithLargeBuffer( 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 d170b3dfb..71a5bf219 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 @@ -137,16 +137,6 @@ class EmailSenderService( mailSender.send(mimeMessage) } - private fun ApplicationType.translations() = - when (this) { - ApplicationType.CABLE_REPORT -> - Translations( - fi = "johtoselvityshakemuksen", - sv = "ledningsutredning", - en = "a cable report application", - ) - } - private fun signatures() = Translations( "$templatePath/common/signature-fi.mustache".getResourceAsText(), @@ -158,4 +148,16 @@ class EmailSenderService( private fun parseTemplate(path: String, contextObject: Any): String = Template.parse(path.getResource().openStream()).processToString(contextObject) + + companion object { + fun ApplicationType.translations() = + when (this) { + ApplicationType.CABLE_REPORT -> + Translations( + fi = "johtoselvityshakemuksen", + sv = "ledningsutredning", + en = "a cable report application", + ) + } + } } diff --git a/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/email/EmailSenderServiceTest.kt b/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/email/EmailSenderServiceTest.kt index deb1c0291..d93d01881 100644 --- a/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/email/EmailSenderServiceTest.kt +++ b/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/email/EmailSenderServiceTest.kt @@ -347,6 +347,8 @@ class EmailSenderServiceTest { } } +fun MimeMessage.textBody(): String = bodies().first + /** Returns a (text body, HTML body) pair. */ fun MimeMessage.bodies(): Pair { assertThat(content).isInstanceOf(MimeMultipart::class) diff --git a/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/factory/AlluDataFactory.kt b/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/factory/AlluDataFactory.kt index 4ce07b237..c19753aeb 100644 --- a/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/factory/AlluDataFactory.kt +++ b/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/factory/AlluDataFactory.kt @@ -41,7 +41,7 @@ class AlluDataFactory( const val teppoEmail = "teppo@example.test" val expectedRecipients = arrayOf( - "timo.työnsuorittaja@mail.com", + "timo.tyonsuorittaja@mail.com", "anssi.asianhoitaja@mail.com", "rane.rakennuttaja@mail.com", "new.mail@foo.fi", diff --git a/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/factory/UserContactFactory.kt b/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/factory/UserContactFactory.kt index 6fc099807..7f4ebe64b 100644 --- a/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/factory/UserContactFactory.kt +++ b/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/factory/UserContactFactory.kt @@ -10,5 +10,5 @@ object UserContactFactory { val asianhoitajaContact = UserContact("Anssi Asianhoitaja", "anssi.asianhoitaja@mail.com") - val suorittajaContact = UserContact("Timo Työnsuorittaja", "timo.työnsuorittaja@mail.com") + val suorittajaContact = UserContact("Timo Työnsuorittaja", "timo.tyonsuorittaja@mail.com") } diff --git a/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/test/Asserts.kt b/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/test/Asserts.kt index 6beec56ed..22370132e 100644 --- a/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/test/Asserts.kt +++ b/services/hanke-service/src/test/kotlin/fi/hel/haitaton/hanke/test/Asserts.kt @@ -2,6 +2,8 @@ package fi.hel.haitaton.hanke.test import assertk.Assert import assertk.all +import assertk.assertions.containsExactlyInAnyOrder +import assertk.assertions.extracting import assertk.assertions.first import assertk.assertions.hasSize import assertk.assertions.isBetween @@ -11,6 +13,7 @@ import assertk.assertions.isNotNull import assertk.assertions.prop import fi.hel.haitaton.hanke.domain.Hankealue import fi.hel.haitaton.hanke.domain.HasFeatures +import jakarta.mail.internet.MimeMessage import java.time.Duration import java.time.OffsetDateTime import java.time.ZonedDateTime @@ -56,4 +59,8 @@ object Asserts { prop(FeatureCollection::getFeatures).first().hasSameCoordinatesAs(other) } } + + fun Assert>.hasReceivers(vararg receivers: String?) { + extracting { it.allRecipients.first().toString() }.containsExactlyInAnyOrder(*receivers) + } } diff --git a/services/hanke-service/src/test/resources/application-test.properties b/services/hanke-service/src/test/resources/application-test.properties index 75ec21eef..dec44d652 100644 --- a/services/hanke-service/src/test/resources/application-test.properties +++ b/services/hanke-service/src/test/resources/application-test.properties @@ -6,6 +6,7 @@ haitaton.email.filter.use=false haitaton.email.from=no-reply@hel.fi haitaton.features.hanke-editing=true haitaton.features.user-management=true +haitaton.testdata.enabled=true spring.jpa.show-sql=false spring.mail.port=3025 spring.mail.properties.mail.debug=false