From fbd2852d9a0f69a2494e6c5afc3d48ac3246cee5 Mon Sep 17 00:00:00 2001 From: Tim Smelik Date: Thu, 9 Jan 2025 18:18:10 +0100 Subject: [PATCH] Suppress notifications when updating own work day (#390) (#395) * Suppress notifications when updating own work day (#390) * LocalDate#parse -> LocalDate#of for tests --- readme.md | 4 +- .../workday/controllers/WorkdayController.kt | 17 ++- .../flock/eco/workday/interfaces/Approve.kt | 14 ++- .../eco/workday/services/WorkDayService.kt | 5 +- .../services/email/WorkdayEmailService.kt | 43 ++++--- .../flock/eco/workday/utils/DateUtils.kt | 5 + .../eco/workday/forms/WorkDayFormFixture.kt | 15 +++ .../workday/forms/WorkDaySheetFormFixture.kt | 5 + .../eco/workday/model/AssignmentFixture.kt | 15 +++ .../flock/eco/workday/model/ClientFixture.kt | 8 ++ .../flock/eco/workday/model/WorkDayFixture.kt | 19 +++ .../eco/workday/model/WorkDaySheetFixture.kt | 5 + .../services/WorkDayServiceIntegrationTest.kt | 77 ++++++++++++ .../workday/services/WorkDayServiceTest.kt | 112 ++++++++---------- .../services/email/WorkdayEmailServiceTest.kt | 75 ++++++++++++ 15 files changed, 334 insertions(+), 85 deletions(-) create mode 100644 src/test/kotlin/community/flock/eco/workday/forms/WorkDayFormFixture.kt create mode 100644 src/test/kotlin/community/flock/eco/workday/forms/WorkDaySheetFormFixture.kt create mode 100644 src/test/kotlin/community/flock/eco/workday/model/AssignmentFixture.kt create mode 100644 src/test/kotlin/community/flock/eco/workday/model/ClientFixture.kt create mode 100644 src/test/kotlin/community/flock/eco/workday/model/WorkDayFixture.kt create mode 100644 src/test/kotlin/community/flock/eco/workday/model/WorkDaySheetFixture.kt create mode 100644 src/test/kotlin/community/flock/eco/workday/services/WorkDayServiceIntegrationTest.kt create mode 100644 src/test/kotlin/community/flock/eco/workday/services/email/WorkdayEmailServiceTest.kt diff --git a/readme.md b/readme.md index d8eeac53c..a02350a9c 100644 --- a/readme.md +++ b/readme.md @@ -59,11 +59,11 @@ Use `ktlint` to lint kotlin files or `eslint` for javascript files ```bash # check code style (it's also bound to "mvn verify") -$ ./mvnw antrun:run@ktlint +$ ./mvnw ktlint:check src/main/kotlin/Main.kt:10:10: Unused import # fix code style deviations (runs built-in formatter) -$ ./mvnw antrun:run@ktlint-format +$ ./mvnw ktlint:format # fix code styles for js files with eslint $ npm run lint diff --git a/src/main/kotlin/community/flock/eco/workday/controllers/WorkdayController.kt b/src/main/kotlin/community/flock/eco/workday/controllers/WorkdayController.kt index c7de5f70e..c46ccea6a 100644 --- a/src/main/kotlin/community/flock/eco/workday/controllers/WorkdayController.kt +++ b/src/main/kotlin/community/flock/eco/workday/controllers/WorkdayController.kt @@ -81,7 +81,13 @@ class WorkdayController( ) = service.findByCode(code) ?.applyAuthentication(authentication) ?.applyAllowedToUpdate(form.status, authentication.isAdmin()) - ?.run { service.update(code, form) } + ?.run { + service.update( + workDayCode = code, + form = form, + isOwnWorkDay = isOwnWorkDay(authentication), + ) + } .toResponse() @DeleteMapping("/{code}") @@ -125,11 +131,16 @@ class WorkdayController( private fun WorkDay.applyAuthentication(authentication: Authentication) = apply { - if (!(authentication.isAdmin() || this.assignment.person.isUser(authentication.name))) { - throw ResponseStatusException(UNAUTHORIZED, "User has not access to workday: ${this.code}") + if (!authentication.isAdmin() && !isOwnWorkDay(authentication)) { + throw ResponseStatusException( + UNAUTHORIZED, + "User has not access to workday: ${this.code}", + ) } } + private fun WorkDay.isOwnWorkDay(authentication: Authentication) = assignment.person.isUser(authentication.name) + private fun getMediaType(name: String): MediaType { val extension = java.io.File(name).extension.lowercase() val mime = org.springframework.boot.web.server.MimeMappings.DEFAULT.get(extension) diff --git a/src/main/kotlin/community/flock/eco/workday/interfaces/Approve.kt b/src/main/kotlin/community/flock/eco/workday/interfaces/Approve.kt index c9b2c7a11..071b4f155 100644 --- a/src/main/kotlin/community/flock/eco/workday/interfaces/Approve.kt +++ b/src/main/kotlin/community/flock/eco/workday/interfaces/Approve.kt @@ -8,15 +8,21 @@ interface Approve { val status: Status } -fun Approve.applyAllowedToUpdate( +fun T.applyAllowedToUpdate( status: Status, isAdmin: Boolean, -) { +) = apply { if (this.status !== Status.REQUESTED && !isAdmin) { - throw ResponseStatusException(HttpStatus.FORBIDDEN, "User is not allowed to change status") + throw ResponseStatusException( + HttpStatus.FORBIDDEN, + "User is not allowed to change status", + ) } if (status !== this.status && !isAdmin) { - throw ResponseStatusException(HttpStatus.FORBIDDEN, "User is not allowed to change status field") + throw ResponseStatusException( + HttpStatus.FORBIDDEN, + "User is not allowed to change status field", + ) } if (status !== this.status && !StatusTransition.check(this.status, status)) { throw ResponseStatusException(HttpStatus.FORBIDDEN, "This status change is not allowed") diff --git a/src/main/kotlin/community/flock/eco/workday/services/WorkDayService.kt b/src/main/kotlin/community/flock/eco/workday/services/WorkDayService.kt index 19ad2a993..8691e68e8 100644 --- a/src/main/kotlin/community/flock/eco/workday/services/WorkDayService.kt +++ b/src/main/kotlin/community/flock/eco/workday/services/WorkDayService.kt @@ -99,6 +99,7 @@ class WorkDayService( fun update( workDayCode: String, form: WorkDayForm, + isOwnWorkDay: Boolean, ): WorkDay { val currentWorkday = workDayRepository.findByCode(workDayCode).toNullable() return currentWorkday @@ -109,7 +110,9 @@ class WorkDayService( .save() } .also { - emailService.sendUpdate(currentWorkday!!, it) + if (!isOwnWorkDay) { + emailService.sendUpdate(it) + } } } diff --git a/src/main/kotlin/community/flock/eco/workday/services/email/WorkdayEmailService.kt b/src/main/kotlin/community/flock/eco/workday/services/email/WorkdayEmailService.kt index 16fdac9ed..a698325a8 100644 --- a/src/main/kotlin/community/flock/eco/workday/services/email/WorkdayEmailService.kt +++ b/src/main/kotlin/community/flock/eco/workday/services/email/WorkdayEmailService.kt @@ -4,6 +4,7 @@ import community.flock.eco.workday.config.properties.MailjetTemplateProperties import community.flock.eco.workday.model.Person import community.flock.eco.workday.model.Status import community.flock.eco.workday.model.WorkDay +import community.flock.eco.workday.utils.DateUtils.toHumanReadable import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.context.i18n.LocaleContextHolder @@ -12,28 +13,40 @@ import java.time.YearMonth import java.time.format.TextStyle @Service -class WorkdayEmailService(private val emailService: EmailService, private val mailjetTemplateProperties: MailjetTemplateProperties) { +class WorkdayEmailService( + private val emailService: EmailService, + private val mailjetTemplateProperties: MailjetTemplateProperties, +) { private val log: Logger = LoggerFactory.getLogger(WorkdayEmailService::class.java) - fun sendUpdate( - old: WorkDay, - new: WorkDay, - ) { - val recipient = new.assignment.person + fun sendUpdate(workDay: WorkDay) { + val recipient = workDay.assignment.person - var subject = "Update in Workday." - var emailMessage = "Er is een update in Workday." + val subject = + "Workday update (${workDay.from.toHumanReadable()} t/m ${workDay.to.toHumanReadable()} bij ${workDay.assignment.client.name})" - if (old.status !== new.status) { - subject = "Status update in Workday!" - emailMessage = "De status van je Workday is veranderd.\n\n" + - "Vorige status: ${old.status}.\n" + - "Nieuwe status: ${new.status}." - } + val project = workDay.assignment.project?.name?.replaceFirstChar { it.uppercase() } ?: "-" + + val emailMessage = + """ + Je workday is bijgewerkt. + + Klant: ${workDay.assignment.client.name} + Rol: ${workDay.assignment.role ?: "-"} + Project: $project + + Van: ${workDay.from.toHumanReadable()} + Tot en met: ${workDay.to.toHumanReadable()} + + Totaal aantal gewerkte uren: ${workDay.hours} + + Status: ${workDay.status} + """.trimIndent() log.info("Email generated for workday update for ${recipient.email}") - val templateVariables = emailService.createTemplateVariables(recipient.firstname, emailMessage) + val templateVariables = + emailService.createTemplateVariables(recipient.firstname, emailMessage) emailService.sendEmailMessage( recipient.receiveEmail, recipient.email, diff --git a/src/main/kotlin/community/flock/eco/workday/utils/DateUtils.kt b/src/main/kotlin/community/flock/eco/workday/utils/DateUtils.kt index 38b9f4510..4f2e337cc 100644 --- a/src/main/kotlin/community/flock/eco/workday/utils/DateUtils.kt +++ b/src/main/kotlin/community/flock/eco/workday/utils/DateUtils.kt @@ -6,6 +6,7 @@ import community.flock.eco.workday.services.countWorkDaysInPeriod import java.time.DayOfWeek import java.time.LocalDate import java.time.YearMonth +import java.time.format.DateTimeFormatter import java.time.temporal.ChronoUnit object DateUtils { @@ -45,4 +46,8 @@ object DateUtils { val to = YearMonth.of(this.year, this.month).atEndOfMonth() return countWorkDaysInPeriod(from, to) } + + val humanReadableDateFormat = DateTimeFormatter.ofPattern("dd-MM-yyyy") + + fun LocalDate.toHumanReadable() = format(humanReadableDateFormat) } diff --git a/src/test/kotlin/community/flock/eco/workday/forms/WorkDayFormFixture.kt b/src/test/kotlin/community/flock/eco/workday/forms/WorkDayFormFixture.kt new file mode 100644 index 000000000..074202932 --- /dev/null +++ b/src/test/kotlin/community/flock/eco/workday/forms/WorkDayFormFixture.kt @@ -0,0 +1,15 @@ +package community.flock.eco.workday.forms + +import java.time.LocalDate + +fun aWorkDayForm() = + WorkDayForm( + from = LocalDate.of(2020, 1, 1), + to = LocalDate.of(2020, 3, 31), + assignmentCode = "some-assignment-code", + hours = 50.0, + sheets = + listOf( + aWorkDaySheetForm(), + ), + ) diff --git a/src/test/kotlin/community/flock/eco/workday/forms/WorkDaySheetFormFixture.kt b/src/test/kotlin/community/flock/eco/workday/forms/WorkDaySheetFormFixture.kt new file mode 100644 index 000000000..2982c89e7 --- /dev/null +++ b/src/test/kotlin/community/flock/eco/workday/forms/WorkDaySheetFormFixture.kt @@ -0,0 +1,5 @@ +package community.flock.eco.workday.forms + +import java.util.UUID + +fun aWorkDaySheetForm() = WorkDaySheetForm("some-work-day-sheet", UUID.fromString("e03c0587-bb84-49f7-aaa8-34cfc76b4c8b")) diff --git a/src/test/kotlin/community/flock/eco/workday/model/AssignmentFixture.kt b/src/test/kotlin/community/flock/eco/workday/model/AssignmentFixture.kt new file mode 100644 index 000000000..ecb9e7d1e --- /dev/null +++ b/src/test/kotlin/community/flock/eco/workday/model/AssignmentFixture.kt @@ -0,0 +1,15 @@ +package community.flock.eco.workday.model + +import java.time.LocalDate + +fun anAssignment( + person: Person = aPerson(), + client: Client = aClient(), +) = Assignment( + from = LocalDate.of(2024, 1, 1), + to = LocalDate.of(2024, 12, 31), + hourlyRate = 100.0, + hoursPerWeek = 40, + client = client, + person = person, +) diff --git a/src/test/kotlin/community/flock/eco/workday/model/ClientFixture.kt b/src/test/kotlin/community/flock/eco/workday/model/ClientFixture.kt new file mode 100644 index 000000000..4f7342189 --- /dev/null +++ b/src/test/kotlin/community/flock/eco/workday/model/ClientFixture.kt @@ -0,0 +1,8 @@ +package community.flock.eco.workday.model + +fun aClient() = + Client( + id = 5, + code = "27620486-77f5-4484-b155-6e318bd24921", + name = "DHL", + ) diff --git a/src/test/kotlin/community/flock/eco/workday/model/WorkDayFixture.kt b/src/test/kotlin/community/flock/eco/workday/model/WorkDayFixture.kt new file mode 100644 index 000000000..dda81e10e --- /dev/null +++ b/src/test/kotlin/community/flock/eco/workday/model/WorkDayFixture.kt @@ -0,0 +1,19 @@ +package community.flock.eco.workday.model + +import java.time.LocalDate + +fun aWorkDay(assignment: Assignment = anAssignment()) = + WorkDay( + id = 3, + code = "41b23e2e-bb80-45d3-aac5-f764aa7b2fc3", + from = LocalDate.of(2024, 1, 1), + to = LocalDate.of(2024, 1, 31), + hours = 160.0, + days = listOf(), + assignment = assignment, + status = Status.REQUESTED, + sheets = + listOf( + aWorkDaySheet(), + ), + ) diff --git a/src/test/kotlin/community/flock/eco/workday/model/WorkDaySheetFixture.kt b/src/test/kotlin/community/flock/eco/workday/model/WorkDaySheetFixture.kt new file mode 100644 index 000000000..86c36b2a9 --- /dev/null +++ b/src/test/kotlin/community/flock/eco/workday/model/WorkDaySheetFixture.kt @@ -0,0 +1,5 @@ +package community.flock.eco.workday.model + +import java.util.UUID + +fun aWorkDaySheet() = WorkDaySheet("some-sheet", UUID.fromString("51b23e2e-bb80-45d3-aac5-f764aa7b2fc3")) diff --git a/src/test/kotlin/community/flock/eco/workday/services/WorkDayServiceIntegrationTest.kt b/src/test/kotlin/community/flock/eco/workday/services/WorkDayServiceIntegrationTest.kt new file mode 100644 index 000000000..69b0faebf --- /dev/null +++ b/src/test/kotlin/community/flock/eco/workday/services/WorkDayServiceIntegrationTest.kt @@ -0,0 +1,77 @@ +package community.flock.eco.workday.services + +import community.flock.eco.workday.ApplicationConfiguration +import community.flock.eco.workday.config.AppTestConfig +import community.flock.eco.workday.forms.WorkDayForm +import community.flock.eco.workday.helpers.CreateHelper +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase +import org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureDataJpa +import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureWebClient +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.annotation.Import +import org.springframework.test.context.ActiveProfiles +import java.time.LocalDate +import javax.transaction.Transactional +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +@SpringBootTest(classes = [ApplicationConfiguration::class, AppTestConfig::class]) +@AutoConfigureTestDatabase +@AutoConfigureDataJpa +@AutoConfigureWebClient +@Transactional +@Import(CreateHelper::class) +@ActiveProfiles(profiles = ["test"]) +class WorkDayServiceIntegrationTest( + @Autowired private val workDayService: WorkDayService, + @Autowired private val createHelper: CreateHelper, +) { + @Test + fun `Create, update and delete work day`() { + val from = LocalDate.of(2020, 1, 1) + val to = LocalDate.of(2020, 3, 31) + val client = createHelper.createClient() + val person = createHelper.createPerson() + val assignment = createHelper.createAssignment(client, person, from, to) + + val createForm = + WorkDayForm( + from = from, + to = to, + assignmentCode = assignment.code, + hours = 50.0, + sheets = listOf(), + ) + + val created = workDayService.create(createForm) + assertNotNull(created.id) + assertEquals(50.0, created.hours) + + val updateForm = + WorkDayForm( + from = from, + to = to, + assignmentCode = assignment.code, + hours = 25.0, + sheets = listOf(), + ) + val updated = + workDayService.update( + workDayCode = created.code, + form = updateForm, + isOwnWorkDay = false, + ) + assertNotNull(updated.id) + assertEquals(25.0, updated.hours) + assertEquals(created.code, updated.code) + + assertNotNull(workDayService.findByCode(created.code)) + + workDayService.deleteByCode(created.code) + + assertNull(workDayService.findByCode(created.code)) + } +} diff --git a/src/test/kotlin/community/flock/eco/workday/services/WorkDayServiceTest.kt b/src/test/kotlin/community/flock/eco/workday/services/WorkDayServiceTest.kt index 78286ea30..c7a39c8b6 100644 --- a/src/test/kotlin/community/flock/eco/workday/services/WorkDayServiceTest.kt +++ b/src/test/kotlin/community/flock/eco/workday/services/WorkDayServiceTest.kt @@ -1,72 +1,64 @@ package community.flock.eco.workday.services -import community.flock.eco.workday.ApplicationConfiguration -import community.flock.eco.workday.config.AppTestConfig -import community.flock.eco.workday.forms.WorkDayForm -import community.flock.eco.workday.helpers.CreateHelper +import community.flock.eco.workday.forms.aWorkDayForm +import community.flock.eco.workday.model.Assignment +import community.flock.eco.workday.model.WorkDay +import community.flock.eco.workday.model.aWorkDay +import community.flock.eco.workday.model.anAssignment +import community.flock.eco.workday.repository.WorkDayRepository +import community.flock.eco.workday.services.email.WorkdayEmailService +import io.mockk.Called +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +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 -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase -import org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureDataJpa -import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureWebClient -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.context.annotation.Import -import org.springframework.test.context.ActiveProfiles -import java.time.LocalDate -import javax.transaction.Transactional -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNull +import java.util.Optional +import javax.persistence.EntityManager -@SpringBootTest(classes = [ApplicationConfiguration::class, AppTestConfig::class]) -@AutoConfigureTestDatabase -@AutoConfigureDataJpa -@AutoConfigureWebClient -@Transactional -@Import(CreateHelper::class) -@ActiveProfiles(profiles = ["test"]) -class WorkDayServiceTest( - @Autowired private val workDayService: WorkDayService, - @Autowired private val createHelper: CreateHelper, -) { - @Test - fun `creat update delete workday`() { - val from = LocalDate.of(2020, 1, 1) - val to = LocalDate.of(2020, 3, 31) - val client = createHelper.createClient() - val person = createHelper.createPerson() - val assignment = createHelper.createAssignment(client, person, from, to) +private const val BUCKET_NAME = "some-bucket-name" - val createForm = - WorkDayForm( - from = from, - to = to, - assignmentCode = assignment.code, - hours = 50.0, - sheets = listOf(), - ) +class WorkDayServiceTest { + private val workDayRepository: WorkDayRepository = mockk() + private val assignmentService: AssignmentService = mockk() + private val emailService: WorkdayEmailService = mockk() + private val entityManager: EntityManager = mockk() - val created = workDayService.create(createForm) - assertNotNull(created.id) - assertEquals(50.0, created.hours) + private val service = + WorkDayService( + workDayRepository, + assignmentService, + entityManager, + emailService, + BUCKET_NAME, + ) - val updateForm = - WorkDayForm( - from = from, - to = to, - assignmentCode = assignment.code, - hours = 25.0, - sheets = listOf(), - ) - val updated = workDayService.update(created.code, updateForm) - assertNotNull(updated.id) - assertEquals(25.0, updated.hours) - assertEquals(created.code, updated.code) + @Nested + inner class Notifications { + private val assignment: Assignment = anAssignment() + private val workDay: WorkDay = aWorkDay(assignment) + private val form = aWorkDayForm() + private val workDayCode = workDay.code - assertNotNull(workDayService.findByCode(created.code)) + @BeforeEach + fun beforeEach() { + every { workDayRepository.findByCode(workDayCode) } returns Optional.of(workDay) + every { assignmentService.findByCode(form.assignmentCode) } returns assignment + every { workDayRepository.save(workDay) } returns workDay + every { emailService.sendUpdate(workDay) } returns Unit + } - workDayService.deleteByCode(created.code) + @Test + fun `Do not send notification when updating own work day`() { + service.update(workDayCode, form, true) + verify { emailService wasNot Called } + } - assertNull(workDayService.findByCode(created.code)) + @Test + fun `Send notification when updating someone else's work day`() { + service.update(workDayCode, form, false) + verify { emailService.sendUpdate(workDay) } + } } } diff --git a/src/test/kotlin/community/flock/eco/workday/services/email/WorkdayEmailServiceTest.kt b/src/test/kotlin/community/flock/eco/workday/services/email/WorkdayEmailServiceTest.kt new file mode 100644 index 000000000..929168c87 --- /dev/null +++ b/src/test/kotlin/community/flock/eco/workday/services/email/WorkdayEmailServiceTest.kt @@ -0,0 +1,75 @@ +package community.flock.eco.workday.services.email + +import community.flock.eco.workday.config.properties.MailjetTemplateProperties +import community.flock.eco.workday.model.Status +import community.flock.eco.workday.model.WorkDay +import community.flock.eco.workday.model.aWorkDaySheet +import community.flock.eco.workday.model.anAssignment +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.json.JSONObject +import org.junit.jupiter.api.Test +import java.time.LocalDate + +class WorkdayEmailServiceTest { + private val emailService: EmailService = mockk(relaxed = true) + private val mailjetTemplateProperties: MailjetTemplateProperties = mockk() + + private val service = WorkdayEmailService(emailService, mailjetTemplateProperties) + + @Test + fun `Send email`() { + val workDay = + WorkDay( + 5, + hours = 40.0, + from = LocalDate.of(2024, 1, 1), + to = LocalDate.of(2024, 1, 31), + assignment = anAssignment(), + status = Status.REQUESTED, + sheets = + listOf( + aWorkDaySheet(), + ), + ) + + val expectedEmailMessage = + """ + Je workday is bijgewerkt. + + Klant: DHL + Rol: - + Project: - + + Van: 01-01-2024 + Tot en met: 31-01-2024 + + Totaal aantal gewerkte uren: 40.0 + + Status: REQUESTED + """.trimIndent() + val templateVariables = JSONObject() + every { + emailService.createTemplateVariables( + workDay.assignment.person.firstname, + expectedEmailMessage, + ) + }.returns(templateVariables) + + val templateId = 3 + every { mailjetTemplateProperties.updateTemplateId }.returns(templateId) + + service.sendUpdate(workDay) + + verify { + emailService.sendEmailMessage( + workDay.assignment.person.receiveEmail, + workDay.assignment.person.email, + "Workday update (01-01-2024 t/m 31-01-2024 bij DHL)", + templateVariables, + templateId, + ) + } + } +}