Skip to content

Commit

Permalink
Add patch api
Browse files Browse the repository at this point in the history
  • Loading branch information
louptheron committed Aug 1, 2024
1 parent 7e2a3fe commit fedc024
Show file tree
Hide file tree
Showing 10 changed files with 245 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package fr.gouv.cnsp.monitorfish.config

/**
* Warning: All properties patched MUST be declared as `var`, and not `val`
*/
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class Patchable
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions

import com.neovisionaries.i18n.CountryCode
import fr.gouv.cnsp.monitorfish.config.Patchable
import fr.gouv.cnsp.monitorfish.domain.entities.mission.ControlUnit
import java.time.ZonedDateTime

Expand All @@ -17,7 +18,8 @@ data class MissionAction(
val faoAreas: List<String> = listOf(),
val actionType: MissionActionType,
val actionDatetimeUtc: ZonedDateTime,
val actionEndDatetimeUtc: ZonedDateTime? = null,
@Patchable
var actionEndDatetimeUtc: ZonedDateTime? = null,
val emitsVms: ControlCheck? = null,
val emitsAis: ControlCheck? = null,
val flightGoals: List<FlightGoal> = listOf(),
Expand Down Expand Up @@ -65,7 +67,8 @@ data class MissionAction(
val isComplianceWithWaterRegulationsControl: Boolean? = null,
val isSafetyEquipmentAndStandardsComplianceControl: Boolean? = null,
val isSeafarersControl: Boolean? = null,
val observationsByUnit: String? = null,
@Patchable
var observationsByUnit: String? = null,
) {
fun verify() {
val controlTypes = listOf(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions

import java.time.ZonedDateTime
import java.util.*

data class PatchableMissionAction(
val actionEndDatetimeUtc: Optional<ZonedDateTime>?,
val observationsByUnit: Optional<String>?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package fr.gouv.cnsp.monitorfish.domain.mappers

import fr.gouv.cnsp.monitorfish.config.Patchable
import fr.gouv.cnsp.monitorfish.config.UseCase
import java.util.*
import kotlin.reflect.KMutableProperty
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.memberProperties

@UseCase
class PatchEntity<T : Any, S : Any> {

/**
* Patches the target entity with values from the source entity.
*
* This function updates the target entity with values from the source entity for properties
* annotated with @Patchable. If a property in the source entity is null, the existing value
* in the target entity is retained. If a property in the source entity is an Optional, it
* is handled accordingly.
*
* @param target The target entity to be patched.
* @param source The source entity providing the patch values.
*/
fun execute(target: T, source: S): T {
val sourceProperties = source::class.memberProperties
val targetProperties = target::class.memberProperties

for (sourceProp in sourceProperties) {
val targetProp =
targetProperties.filter { it.hasAnnotation<Patchable>() }.find { it.name == sourceProp.name }
if (targetProp != null && targetProp is KMutableProperty<*>) {
val sourceValue = sourceProp.getter.call(source)
val existingValue = targetProp.getter.call(target)
val finalValue = if (sourceValue is Optional<*>) {
getValueFromOptional(existingValue, sourceValue)
} else {
sourceValue ?: existingValue
}

targetProp.setter.call(target, finalValue)
}
}

return target
}

private fun getValueFromOptional(existingValue: Any?, optional: Optional<*>?): Any? {
return when {
optional == null -> existingValue
optional.isPresent -> optional.get()
else -> null
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package fr.gouv.cnsp.monitorfish.domain.use_cases.mission.mission_actions

import fr.gouv.cnsp.monitorfish.config.UseCase
import fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions.MissionAction
import fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions.PatchableMissionAction
import fr.gouv.cnsp.monitorfish.domain.exceptions.BackendUsageErrorCode
import fr.gouv.cnsp.monitorfish.domain.exceptions.BackendUsageException
import fr.gouv.cnsp.monitorfish.domain.mappers.PatchEntity
import fr.gouv.cnsp.monitorfish.domain.repositories.MissionActionsRepository

@UseCase
class PatchMissionAction(
private val missionActionsRepository: MissionActionsRepository,
private val patchMissionAction: PatchEntity<MissionAction, PatchableMissionAction>,
) {

fun execute(id: Int, patchableEnvActionEntity: PatchableMissionAction): MissionAction {
return try {
val previousMissionAction = missionActionsRepository.findById(id)

val updatedMissionAction = patchMissionAction.execute(previousMissionAction, patchableEnvActionEntity)

missionActionsRepository.save(updatedMissionAction)
} catch (e: Exception) {
throw BackendUsageException(BackendUsageErrorCode.NOT_FOUND, "Action $id not found", e)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
package fr.gouv.cnsp.monitorfish.infrastructure.api.public_api

import fr.gouv.cnsp.monitorfish.domain.use_cases.mission.mission_actions.GetMissionActions
import fr.gouv.cnsp.monitorfish.domain.use_cases.mission.mission_actions.PatchMissionAction
import fr.gouv.cnsp.monitorfish.infrastructure.api.outputs.MissionActionDataOutput
import fr.gouv.cnsp.monitorfish.infrastructure.api.public_api.input.PatchableMissionActionDataInput
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import jakarta.websocket.server.PathParam
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/api/v1/mission_actions")
@Tag(name = "APIs for mission actions")
class PublicMissionActionsController(
private val getMissionActions: GetMissionActions,
private val patchMissionAction: PatchMissionAction,
) {

@GetMapping("")
Expand All @@ -26,4 +28,19 @@ class PublicMissionActionsController(
): List<MissionActionDataOutput> {
return getMissionActions.execute(missionId).map { MissionActionDataOutput.fromMissionAction(it) }
}

@PatchMapping(value = ["/{actionId}"], consumes = ["application/json"])
@Operation(summary = "Update a mission action")
@ResponseStatus(HttpStatus.OK)
fun updateMissionAction(
@PathParam("Action id")
@PathVariable(name = "actionId")
actionId: Int,
@RequestBody
actionInput: PatchableMissionActionDataInput,
): MissionActionDataOutput {
val updatedMissionAction = patchMissionAction.execute(actionId, actionInput.toPatchableMissionAction())

return MissionActionDataOutput.fromMissionAction(updatedMissionAction)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package fr.gouv.cnsp.monitorfish.infrastructure.api.public_api.input

import fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions.PatchableMissionAction
import java.time.ZonedDateTime
import java.util.*

data class PatchableMissionActionDataInput(
val actionEndDatetimeUtc: Optional<ZonedDateTime>?,
val observationsByUnit: Optional<String>?,
) {
fun toPatchableMissionAction(): PatchableMissionAction {
return PatchableMissionAction(
actionEndDatetimeUtc = actionEndDatetimeUtc,
observationsByUnit = observationsByUnit,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package fr.gouv.cnsp.monitorfish.domain.use_cases.mission.mission_actions

import com.neovisionaries.i18n.CountryCode
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.argumentCaptor
import com.nhaarman.mockitokotlin2.given
import com.nhaarman.mockitokotlin2.verify
import fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions.Completion
import fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions.MissionAction
import fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions.MissionActionType
import fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions.PatchableMissionAction
import fr.gouv.cnsp.monitorfish.domain.mappers.PatchEntity
import fr.gouv.cnsp.monitorfish.domain.repositories.MissionActionsRepository
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.test.context.junit.jupiter.SpringExtension
import java.time.ZonedDateTime
import java.util.*

@ExtendWith(SpringExtension::class)
class PatchMissionActionUTests {

@MockBean
private lateinit var missionActionsRepository: MissionActionsRepository

private val patchMissionAction: PatchEntity<MissionAction, PatchableMissionAction> = PatchEntity()

@Test
fun `execute Should patch an existing action`() {
// Given
val action = MissionAction(
id = null,
vesselId = null,
missionId = 1,
actionDatetimeUtc = ZonedDateTime.now(),
portLocode = "AEFAT",
actionType = MissionActionType.LAND_CONTROL,
gearOnboard = listOf(),
seizureAndDiversion = true,
isDeleted = false,
hasSomeGearsSeized = false,
hasSomeSpeciesSeized = false,
completedBy = "XYZ",
isFromPoseidon = false,
flagState = CountryCode.FR,
userTrigram = "LTH",
completion = Completion.TO_COMPLETE,
)
given(missionActionsRepository.findById(any())).willReturn(action)
val expectedDateTime = ZonedDateTime.now()
val patch = PatchableMissionAction(
actionEndDatetimeUtc = Optional.of(expectedDateTime),
observationsByUnit = Optional.of("An observation"),
)

// When
PatchMissionAction(missionActionsRepository, patchMissionAction).execute(123, patch)

// Then
argumentCaptor<MissionAction>().apply {
verify(missionActionsRepository).save(capture())

assertThat(allValues.first().observationsByUnit).isEqualTo("An observation")
assertThat(allValues.first().actionEndDatetimeUtc).isEqualTo(expectedDateTime)
assertThat(allValues.first().userTrigram).isEqualTo("LTH")
assertThat(allValues.first().actionType).isEqualTo(MissionActionType.LAND_CONTROL)
}
}
}
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ package fr.gouv.cnsp.monitorfish.infrastructure.api.public_api

import com.neovisionaries.i18n.CountryCode
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.given
import fr.gouv.cnsp.monitorfish.config.SentryConfig
import fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions.Completion
import fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions.MissionAction
import fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions.MissionActionType
import fr.gouv.cnsp.monitorfish.domain.use_cases.mission.mission_actions.GetMissionActions
import fr.gouv.cnsp.monitorfish.domain.use_cases.mission.mission_actions.PatchMissionAction
import fr.gouv.cnsp.monitorfish.infrastructure.database.repositories.TestUtils
import kotlinx.coroutines.runBlocking
import org.hamcrest.Matchers.equalTo
import org.junit.jupiter.api.Test
Expand All @@ -17,8 +20,10 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.context.annotation.Import
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import java.time.ZonedDateTime
Expand All @@ -34,6 +39,9 @@ class PublicMissionActionsControllerITests {
@MockBean
private lateinit var getMissionActions: GetMissionActions

@MockBean
private lateinit var patchMissionAction: PatchMissionAction

private fun <T> givenSuspended(block: suspend () -> T) = BDDMockito.given(runBlocking { block() })!!

@Test
Expand Down Expand Up @@ -69,4 +77,28 @@ class PublicMissionActionsControllerITests {
Mockito.verify(getMissionActions).execute(123)
}
}

@Test
fun `Should patch a mission action`() {
// Given
val dateTime = ZonedDateTime.parse("2022-05-05T03:04:05.000Z")
val newMission = TestUtils.getDummyMissionAction(dateTime).copy(flagState = CountryCode.UNDEFINED)
given(patchMissionAction.execute(any(), any())).willReturn(newMission)

// When
api.perform(
patch("/api/v1/mission_actions/123")
.content(
"""
{
"observationsByUnit": "OBSERVATION",
"actionEndDatetimeUtc": "2024-02-01T14:29:00Z"
}
""".trimIndent(),
)
.contentType(MediaType.APPLICATION_JSON),
)
// Then
.andExpect(status().isOk)
}
}

0 comments on commit fedc024

Please sign in to comment.