Init
This commit is contained in:
+24
@@ -0,0 +1,24 @@
|
||||
package space.nstart.pcp.pcp_mission_planing_service
|
||||
|
||||
import io.camunda.client.CamundaClient
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.boot.test.context.TestConfiguration
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Import
|
||||
import org.mockito.Mockito.mock
|
||||
|
||||
@SpringBootTest
|
||||
@Import(PcpBallisticsServiceApplicationTests.TestBeans::class)
|
||||
class PcpBallisticsServiceApplicationTests {
|
||||
|
||||
@Test
|
||||
fun contextLoads() {
|
||||
}
|
||||
|
||||
@TestConfiguration
|
||||
class TestBeans {
|
||||
@Bean
|
||||
fun camundaClient(): CamundaClient = mock(CamundaClient::class.java)
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package space.nstart.pcp.pcp_mission_planing_service.controller
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito.doReturn
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.verify
|
||||
import space.nstart.pcp.pcp_types_lib.dto.routes.SurveyModeStatus
|
||||
import space.nstart.pcp.pcp_mission_planing_service.service.MissionPlaningService
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.MissionSurveyCalculationResponseDTO
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class MissionControllerTest {
|
||||
|
||||
private val missionPlaningService = mock(MissionPlaningService::class.java)
|
||||
private val controller = MissionController(missionPlaningService)
|
||||
|
||||
@Test
|
||||
fun `calculate surveys endpoint accepts satellite and interval overrides`() {
|
||||
val missionId = UUID.randomUUID()
|
||||
val startedAt = LocalDateTime.of(2026, 4, 2, 10, 0)
|
||||
val responseBody = MissionSurveyCalculationResponseDTO(
|
||||
missionId = missionId,
|
||||
completed = true,
|
||||
startedAt = startedAt,
|
||||
finishedAt = startedAt.plusSeconds(2),
|
||||
durationMs = 2_000,
|
||||
comPlanSnapshotId = 99L,
|
||||
surveyModesCount = 3,
|
||||
manualModesCount = 1,
|
||||
complanModesCount = 1,
|
||||
slotsModesCount = 1,
|
||||
mixedModesCount = 0,
|
||||
bookedSlotIdsCount = 4
|
||||
)
|
||||
doReturn(responseBody).`when`(missionPlaningService).loadSlots(missionId, comPlanSnapshotId = 99L)
|
||||
|
||||
val response = controller.calculateSurveys(
|
||||
id = missionId,
|
||||
comPlanSnapshotId = 99L
|
||||
)
|
||||
|
||||
assertEquals(responseBody, response)
|
||||
verify(missionPlaningService).loadSlots(missionId, comPlanSnapshotId = 99L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `update survey mode status endpoint delegates to service`() {
|
||||
val response = controller.updateSurveyModeStatus(
|
||||
id = 15L,
|
||||
body = SurveyModeStatusUpdateRequest(status = SurveyModeStatus.PROCESSED)
|
||||
)
|
||||
|
||||
assertEquals(org.springframework.http.HttpStatus.NO_CONTENT, response.statusCode)
|
||||
verify(missionPlaningService).updateSurveyModeStatus(15L, SurveyModeStatus.PROCESSED)
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package space.nstart.pcp.pcp_mission_planing_service.controller
|
||||
|
||||
import jakarta.validation.Validation
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import space.nstart.pcp.pcp_types_lib.dto.routes.SurveyModeStatus
|
||||
|
||||
class SurveyModeStatusUpdateRequestTest {
|
||||
|
||||
private val validator = Validation.buildDefaultValidatorFactory().validator
|
||||
|
||||
@Test
|
||||
fun `validator accepts enum status field`() {
|
||||
val violations = validator.validate(
|
||||
SurveyModeStatusUpdateRequest(status = SurveyModeStatus.PROCESSED)
|
||||
)
|
||||
|
||||
assertTrue(violations.isEmpty())
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package space.nstart.pcp.pcp_mission_planing_service.service
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import org.apache.kafka.clients.producer.ProducerRecord
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.verify
|
||||
import org.springframework.beans.factory.ObjectProvider
|
||||
import org.springframework.kafka.core.KafkaTemplate
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.BookedSlotStatus
|
||||
import space.nstart.pcp.pcp_types_lib.message.PcpKafkaEvent
|
||||
import java.util.UUID
|
||||
|
||||
class BookedSlotsEventPublisherTest {
|
||||
|
||||
private val objectMapper = jacksonObjectMapper().findAndRegisterModules()
|
||||
private val kafkaTemplate = mock(KafkaTemplate::class.java) as KafkaTemplate<String, Any>
|
||||
private val publisher = BookedSlotsEventPublisher(
|
||||
kafkaTemplateProvider = SingleObjectProvider(kafkaTemplate),
|
||||
objectMapperProvider = SingleObjectProvider(objectMapper),
|
||||
topic = "pcp.slots.status.v1",
|
||||
applicationName = "pcp-mission-planing-service"
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `publish sends booked slots lifecycle event to kafka`() {
|
||||
val missionId = UUID.randomUUID()
|
||||
|
||||
publisher.publish(
|
||||
bookedSlotIds = listOf(10L, 20L, 10L),
|
||||
status = BookedSlotStatus.PROCESSED,
|
||||
missionId = missionId,
|
||||
modeId = 77L,
|
||||
satelliteId = 88L
|
||||
)
|
||||
|
||||
val captor = ArgumentCaptor.forClass(ProducerRecord::class.java) as ArgumentCaptor<ProducerRecord<String, Any>>
|
||||
verify(kafkaTemplate).send(captor.capture())
|
||||
|
||||
val record = captor.value
|
||||
assertEquals("pcp.slots.status.v1", record.topic())
|
||||
assertEquals(missionId.toString(), record.key())
|
||||
val typeHeader = record.headers().lastHeader("type")
|
||||
assertEquals(PcpKafkaEvent.BookedSlotsStatusChangedEvent.name, typeHeader.value().decodeToString())
|
||||
|
||||
val payload = objectMapper.readTree(record.value() as String)
|
||||
assertEquals(PcpKafkaEvent.BookedSlotsStatusChangedEvent.name, payload.path("type").asText())
|
||||
assertEquals("pcp-mission-planing-service", payload.path("data").path("sourceService").asText())
|
||||
assertEquals("PROCESSED", payload.path("data").path("status").asText())
|
||||
assertEquals(2, payload.path("data").path("bookedSlotIds").size())
|
||||
assertTrue(payload.path("data").path("bookedSlotIds").map { it.asLong() }.containsAll(listOf(10L, 20L)))
|
||||
}
|
||||
|
||||
private class SingleObjectProvider<T : Any>(private val value: T) : ObjectProvider<T> {
|
||||
override fun getObject(vararg args: Any?): T = value
|
||||
override fun getIfAvailable(): T = value
|
||||
override fun getIfUnique(): T = value
|
||||
override fun getObject(): T = value
|
||||
}
|
||||
}
|
||||
+459
@@ -0,0 +1,459 @@
|
||||
package space.nstart.pcp.pcp_mission_planing_service.service
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.ArgumentMatchers.anyList
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.verifyNoInteractions
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import space.nstart.pcp.pcp_types_lib.configuration.CustomErrorException
|
||||
import space.nstart.pcp.pcp_types_lib.dto.routes.AngleRangeDto
|
||||
import space.nstart.pcp.pcp_types_lib.dto.routes.RoutePassportDto
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.BookedSlotStatus
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.DropModeResponseDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SatelliteModeResponseDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SatelliteModeSnapshotResponseDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SurveyModeResponseDTO
|
||||
import space.nstart.pcp.pcp_mission_planing_service.entity.DropModeEntity
|
||||
import space.nstart.pcp.pcp_mission_planing_service.entity.DropModeSurveyEntity
|
||||
import space.nstart.pcp.pcp_mission_planing_service.entity.MissionEntity
|
||||
import space.nstart.pcp.pcp_mission_planing_service.entity.ModeBookingEntity
|
||||
import space.nstart.pcp.pcp_mission_planing_service.entity.PlanEntity
|
||||
import space.nstart.pcp.pcp_mission_planing_service.entity.SurveyModeEntity
|
||||
import space.nstart.pcp.pcp_mission_planing_service.entity.SurveyModeSource
|
||||
import space.nstart.pcp.pcp_types_lib.dto.routes.SurveyModeStatus
|
||||
import space.nstart.pcp.pcp_mission_planing_service.repository.DropModeRepository
|
||||
import space.nstart.pcp.pcp_mission_planing_service.repository.DropModeSurveyRepository
|
||||
import space.nstart.pcp.pcp_mission_planing_service.repository.MissionRepository
|
||||
import space.nstart.pcp.pcp_mission_planing_service.repository.ModeBookingRepository
|
||||
import space.nstart.pcp.pcp_mission_planing_service.repository.ModeRepository
|
||||
import space.nstart.pcp.pcp_mission_planing_service.repository.PlanRepository
|
||||
import space.nstart.pcp.pcp_mission_planing_service.repository.SurveyModeRepository
|
||||
import space.nstart.pcp.pcp_mission_planing_service.service.catalog.SatelliteCatalogClient
|
||||
import space.nstart.pcp.pcp_mission_planing_service.service.satellite_type.DefaultSatelliteMissionType
|
||||
import space.nstart.pcp.pcp_mission_planing_service.service.satellite_type.EmissioSatelliteMissionType
|
||||
import space.nstart.pcp.pcp_mission_planing_service.service.satellite_type.ReflixioSatelliteMissionType
|
||||
import space.nstart.pcp.pcp_mission_planing_service.service.satellite_type.SatelliteMissionTypeFactory
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteDTO
|
||||
import java.time.LocalDateTime
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.util.UUID
|
||||
import java.math.BigDecimal
|
||||
|
||||
class MissionPlaningServiceTest {
|
||||
|
||||
private val missionRepository = mock(MissionRepository::class.java)
|
||||
private val modeRepository = mock(ModeRepository::class.java)
|
||||
private val planRepository = mock(PlanRepository::class.java)
|
||||
private val surveyModeRepository = mock(SurveyModeRepository::class.java)
|
||||
private val dropModeRepository = mock(DropModeRepository::class.java)
|
||||
private val dropModeSurveyRepository = mock(DropModeSurveyRepository::class.java)
|
||||
private val modeBookingRepository = mock(ModeBookingRepository::class.java)
|
||||
private val bookedSlotsEventPublisher = mock(BookedSlotsEventPublisher::class.java)
|
||||
private val ballisticsService = mock(BallisticsService::class.java)
|
||||
private val complexMissionService = mock(ComplexMissionService::class.java)
|
||||
private val satelliteCatalogClient = mock(SatelliteCatalogClient::class.java)
|
||||
private val defaultSatelliteMissionType = DefaultSatelliteMissionType()
|
||||
private val satelliteMissionTypeFactory = SatelliteMissionTypeFactory(
|
||||
listOf(defaultSatelliteMissionType, EmissioSatelliteMissionType(), ReflixioSatelliteMissionType()),
|
||||
defaultSatelliteMissionType
|
||||
)
|
||||
|
||||
private val service = MissionPlaningService(
|
||||
missionRepository = missionRepository,
|
||||
modeRepository = modeRepository,
|
||||
planRepository = planRepository,
|
||||
surveyModeRepository = surveyModeRepository,
|
||||
dropModeRepository = dropModeRepository,
|
||||
dropModeSurveyRepository = dropModeSurveyRepository,
|
||||
modeBookingRepository = modeBookingRepository,
|
||||
bookedSlotsEventPublisher = bookedSlotsEventPublisher,
|
||||
ballisticsService = ballisticsService,
|
||||
complexMissionService = complexMissionService,
|
||||
satelliteCatalogClient = satelliteCatalogClient,
|
||||
satelliteMissionTypeFactory = satelliteMissionTypeFactory
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `modesBySatelliteAndInterval returns only intersecting modes`() {
|
||||
val mission = MissionEntity(
|
||||
missionId = UUID.randomUUID(),
|
||||
satelliteId = 77
|
||||
)
|
||||
val plan = PlanEntity(id = 101L, mission = mission, planNumber = 1)
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 16, 10, 0, 0)
|
||||
val intervalStop = LocalDateTime.of(2026, 3, 16, 10, 30, 0)
|
||||
|
||||
val overlappingSurvey = SurveyModeEntity(
|
||||
id = 1L,
|
||||
plan = plan,
|
||||
timeStart = intervalStart.minusMinutes(5).atOffset(ZoneOffset.UTC),
|
||||
revolution = 10,
|
||||
lat = 55.0,
|
||||
long = 37.0,
|
||||
duration = 600.0,
|
||||
contourWkt = "POLYGON((0 0,1 0,1 1,0 0))",
|
||||
roll = 2.0,
|
||||
source = SurveyModeSource.SLOT
|
||||
)
|
||||
val inWindowSurvey = SurveyModeEntity(
|
||||
id = 2L,
|
||||
plan = plan,
|
||||
timeStart = intervalStart.plusMinutes(10).atOffset(ZoneOffset.UTC),
|
||||
revolution = 11,
|
||||
lat = 56.0,
|
||||
long = 38.0,
|
||||
duration = 300.0,
|
||||
contourWkt = "POLYGON((0 0,1 0,1 1,0 0))",
|
||||
roll = 1.0,
|
||||
source = SurveyModeSource.SLOT
|
||||
)
|
||||
val inWindowDrop = DropModeEntity(
|
||||
id = 3L,
|
||||
plan = plan,
|
||||
timeStart = intervalStart.plusMinutes(20).atOffset(ZoneOffset.UTC),
|
||||
revolution = 12,
|
||||
station = "station-1",
|
||||
duration = 120.0
|
||||
)
|
||||
|
||||
`when`(
|
||||
modeRepository.findAllByPlan_Mission_SatelliteIdAndTimeStartIsNotNullAndTimeStartLessThanEqualOrderByTimeStartAsc(
|
||||
77,
|
||||
intervalStop.atOffset(ZoneOffset.UTC)
|
||||
)
|
||||
).thenReturn(listOf(overlappingSurvey, inWindowSurvey, inWindowDrop))
|
||||
`when`(modeBookingRepository.findAllByMode_IdIn(listOf(1L, 2L))).thenReturn(
|
||||
listOf(
|
||||
ModeBookingEntity(mode = overlappingSurvey, slotId = 1001L),
|
||||
ModeBookingEntity(mode = overlappingSurvey, slotId = 1001L),
|
||||
ModeBookingEntity(mode = inWindowSurvey, slotId = 2002L)
|
||||
)
|
||||
)
|
||||
`when`(dropModeSurveyRepository.findAllByDropMode_IdOrderBySurveyMode_TimeStartAsc(3L)).thenReturn(
|
||||
listOf(
|
||||
DropModeSurveyEntity(
|
||||
dropMode = inWindowDrop,
|
||||
surveyMode = inWindowSurvey
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val result = service.modesBySatelliteAndInterval(77, intervalStart, intervalStop)
|
||||
|
||||
assertEquals(listOf(1L, 2L, 3L), result.map { it.id })
|
||||
assertEquals(listOf(1001L), (result[0] as SurveyModeResponseDTO).bookedSlotIds)
|
||||
assertEquals(listOf(2002L), (result[1] as SurveyModeResponseDTO).bookedSlotIds)
|
||||
assertEquals(listOf(2L), (result[2] as DropModeResponseDTO).surveys)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `modesBySatelliteAndInterval rejects inverted interval`() {
|
||||
val exception = assertThrows(ResponseStatusException::class.java) {
|
||||
service.modesBySatelliteAndInterval(
|
||||
satelliteId = 77,
|
||||
timeStart = LocalDateTime.of(2026, 3, 16, 10, 30, 0),
|
||||
timeStop = LocalDateTime.of(2026, 3, 16, 10, 0, 0)
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(HttpStatus.BAD_REQUEST, exception.statusCode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `findMode searches by satellite number revolution and time start with one second tolerance`() {
|
||||
val routePassport = RoutePassportDto(
|
||||
routeId = UUID.randomUUID(),
|
||||
kaShort = "22",
|
||||
kaFull = "SATELLITE-22",
|
||||
routeNameFull = "ROUTE-001",
|
||||
routeNameShort = "R-001",
|
||||
orbitNumber = 321,
|
||||
orbitState = "ASC",
|
||||
intervalBegin = LocalDateTime.of(2026, 4, 3, 12, 0, 0),
|
||||
intervalEnd = LocalDateTime.of(2026, 4, 3, 12, 5, 0),
|
||||
rollAngle = AngleRangeDto(BigDecimal.ZERO, BigDecimal.ONE),
|
||||
visirAngle = AngleRangeDto(BigDecimal.ZERO, BigDecimal.ONE),
|
||||
resolutionRange = BigDecimal.ONE,
|
||||
resolutionAzimuth = BigDecimal.ONE,
|
||||
polarisation = listOf("VV"),
|
||||
processingLevel = "L1",
|
||||
geometry = "POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))",
|
||||
)
|
||||
val exactMode = SurveyModeEntity(
|
||||
id = 10L,
|
||||
timeStart = routePassport.intervalBegin.atOffset(ZoneOffset.UTC),
|
||||
revolution = 321
|
||||
)
|
||||
val nearMode = SurveyModeEntity(
|
||||
id = 11L,
|
||||
timeStart = routePassport.intervalBegin.plusSeconds(1).atOffset(ZoneOffset.UTC),
|
||||
revolution = 321
|
||||
)
|
||||
|
||||
`when`(
|
||||
surveyModeRepository.findAllBySatelliteNumberAndRevolutionAndTimeStartBetween(
|
||||
"22",
|
||||
321,
|
||||
OffsetDateTime.of(routePassport.intervalBegin.minusSeconds(1), ZoneOffset.UTC),
|
||||
OffsetDateTime.of(routePassport.intervalBegin.plusSeconds(1), ZoneOffset.UTC)
|
||||
)
|
||||
).thenReturn(listOf(nearMode, exactMode))
|
||||
|
||||
val result = service.findMode(routePassport)
|
||||
|
||||
assertEquals(10L, result?.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadSlots reads persisted stage4 modes and preserves source bookings and traceability`() {
|
||||
val missionId = UUID.randomUUID()
|
||||
val missionStart = LocalDateTime.of(2026, 3, 18, 10, 0)
|
||||
val missionStop = LocalDateTime.of(2026, 3, 18, 12, 0)
|
||||
val mission = MissionEntity(
|
||||
missionId = missionId,
|
||||
satelliteId = 77,
|
||||
missionStart = missionStart,
|
||||
missionStop = missionStop
|
||||
)
|
||||
val plan = PlanEntity(id = 101L, mission = mission, planStart = missionStart, planStop = missionStop, planNumber = 1)
|
||||
val stage4Mode = SatelliteModeResponseDTO(
|
||||
id = 9001L,
|
||||
satelliteId = 77,
|
||||
source = "MANUAL",
|
||||
startTime = missionStart.plusMinutes(5),
|
||||
endTime = missionStart.plusMinutes(15),
|
||||
revolution = 321,
|
||||
lat = 55.0,
|
||||
longitude = 37.0,
|
||||
duration = 600.0,
|
||||
contourWkt = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||
roll = 4.0,
|
||||
bookedSlotIds = listOf(501L, 502L),
|
||||
snapshotId = 7001L
|
||||
)
|
||||
val snapshot = SatelliteModeSnapshotResponseDTO(
|
||||
id = 7001L,
|
||||
satelliteId = 77,
|
||||
intervalStart = missionStart.minusHours(1),
|
||||
intervalEnd = missionStop.plusHours(1),
|
||||
createdAt = missionStart.minusHours(1),
|
||||
isActive = true
|
||||
)
|
||||
var savedSurveyModes: List<SurveyModeEntity> = emptyList()
|
||||
var savedModeBookings: List<ModeBookingEntity> = emptyList()
|
||||
|
||||
`when`(missionRepository.findById(missionId)).thenReturn(java.util.Optional.of(mission))
|
||||
`when`(planRepository.findFirstByMissionMissionIdAndPlanNumber(missionId, 1)).thenReturn(java.util.Optional.of(plan))
|
||||
`when`(satelliteCatalogClient.getSatellite(77L)).thenReturn(SatelliteDTO(id = 77L, typeCode = "Emissio"))
|
||||
`when`(complexMissionService.activeSnapshot(77L)).thenReturn(snapshot)
|
||||
`when`(complexMissionService.snapshotModes(snapshot.id, missionStart, missionStop)).thenReturn(listOf(stage4Mode))
|
||||
`when`(surveyModeRepository.saveAll(anyList())).thenAnswer {
|
||||
savedSurveyModes = it.arguments[0] as List<SurveyModeEntity>
|
||||
savedSurveyModes
|
||||
}
|
||||
`when`(modeBookingRepository.saveAll(anyList())).thenAnswer {
|
||||
savedModeBookings = it.arguments[0] as List<ModeBookingEntity>
|
||||
savedModeBookings
|
||||
}
|
||||
|
||||
val response = service.loadSlots(missionId)
|
||||
|
||||
verify(complexMissionService).activeSnapshot(77L)
|
||||
verify(complexMissionService).snapshotModes(snapshot.id, missionStart, missionStop)
|
||||
verifyNoInteractions(bookedSlotsEventPublisher)
|
||||
|
||||
val savedSurvey = savedSurveyModes.single()
|
||||
assertEquals(SurveyModeSource.MANUAL, savedSurvey.source)
|
||||
assertEquals(9001L, savedSurvey.satelliteModeId)
|
||||
assertEquals(SurveyModeStatus.PLANNED, savedSurvey.status)
|
||||
assertEquals(snapshot.id, plan.stage4SnapshotId)
|
||||
assertEquals(listOf(501L, 502L), savedModeBookings.map { it.slotId })
|
||||
assertEquals(true, response.completed)
|
||||
assertEquals(snapshot.id, response.comPlanSnapshotId)
|
||||
assertEquals(1, response.surveyModesCount)
|
||||
assertEquals(1, response.manualModesCount)
|
||||
assertEquals(0, response.complanModesCount)
|
||||
assertEquals(0, response.slotsModesCount)
|
||||
assertEquals(0, response.mixedModesCount)
|
||||
assertEquals(2, response.bookedSlotIdsCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadSlots uses explicit satellite and interval overrides for stage5 calculation`() {
|
||||
val missionId = UUID.randomUUID()
|
||||
val mission = MissionEntity(
|
||||
missionId = missionId,
|
||||
satelliteId = 77,
|
||||
missionStart = LocalDateTime.of(2026, 3, 19, 10, 0),
|
||||
missionStop = LocalDateTime.of(2026, 3, 19, 12, 0)
|
||||
)
|
||||
val overrideStart = LocalDateTime.of(2026, 3, 20, 8, 0)
|
||||
val overrideStop = LocalDateTime.of(2026, 3, 20, 9, 0)
|
||||
val overrideSatelliteId = 88L
|
||||
val plan = PlanEntity(id = 111L, mission = mission, planNumber = 1)
|
||||
val snapshot = SatelliteModeSnapshotResponseDTO(
|
||||
id = 7002L,
|
||||
satelliteId = overrideSatelliteId,
|
||||
intervalStart = overrideStart,
|
||||
intervalEnd = overrideStop,
|
||||
createdAt = overrideStart.minusHours(1),
|
||||
isActive = true
|
||||
)
|
||||
|
||||
`when`(missionRepository.findById(missionId)).thenReturn(java.util.Optional.of(mission))
|
||||
`when`(planRepository.findFirstByMissionMissionIdAndPlanNumber(missionId, 1))
|
||||
.thenReturn(java.util.Optional.of(plan))
|
||||
`when`(satelliteCatalogClient.getSatellite(overrideSatelliteId)).thenReturn(SatelliteDTO(id = overrideSatelliteId, typeCode = "Reflixio"))
|
||||
`when`(complexMissionService.activeSnapshot(overrideSatelliteId)).thenReturn(snapshot)
|
||||
`when`(complexMissionService.snapshotModes(snapshot.id, overrideStart, overrideStop)).thenReturn(emptyList())
|
||||
|
||||
val response = service.loadSlots(missionId, overrideSatelliteId, overrideStart, overrideStop)
|
||||
|
||||
verify(complexMissionService).activeSnapshot(overrideSatelliteId)
|
||||
verify(complexMissionService).snapshotModes(snapshot.id, overrideStart, overrideStop)
|
||||
verify(missionRepository).save(mission)
|
||||
assertEquals(overrideSatelliteId, mission.satelliteId)
|
||||
assertEquals(overrideStart, mission.missionStart)
|
||||
assertEquals(overrideStop, mission.missionStop)
|
||||
assertEquals(snapshot.id, plan.stage4SnapshotId)
|
||||
assertEquals(snapshot.id, response.comPlanSnapshotId)
|
||||
assertEquals(0, response.surveyModesCount)
|
||||
verifyNoInteractions(bookedSlotsEventPublisher)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadSlots uses explicit com plan snapshot as primary source`() {
|
||||
val missionId = UUID.randomUUID()
|
||||
val missionStart = LocalDateTime.of(2026, 3, 21, 10, 0)
|
||||
val missionStop = LocalDateTime.of(2026, 3, 21, 12, 0)
|
||||
val mission = MissionEntity(
|
||||
missionId = missionId,
|
||||
satelliteId = 77,
|
||||
missionStart = missionStart,
|
||||
missionStop = missionStop
|
||||
)
|
||||
val plan = PlanEntity(id = 112L, mission = mission, planStart = missionStart, planStop = missionStop, planNumber = 1)
|
||||
val snapshot = SatelliteModeSnapshotResponseDTO(
|
||||
id = 7003L,
|
||||
satelliteId = 77,
|
||||
intervalStart = missionStart,
|
||||
intervalEnd = missionStop,
|
||||
createdAt = missionStart.minusHours(1),
|
||||
isActive = false
|
||||
)
|
||||
|
||||
`when`(missionRepository.findById(missionId)).thenReturn(java.util.Optional.of(mission))
|
||||
`when`(planRepository.findFirstByMissionMissionIdAndPlanNumber(missionId, 1)).thenReturn(java.util.Optional.of(plan))
|
||||
`when`(satelliteCatalogClient.getSatellite(77L)).thenReturn(SatelliteDTO(id = 77L, typeCode = ""))
|
||||
`when`(complexMissionService.snapshot(snapshot.id)).thenReturn(snapshot)
|
||||
`when`(complexMissionService.snapshotModes(snapshot.id, missionStart, missionStop)).thenReturn(emptyList())
|
||||
|
||||
val response = service.loadSlots(missionId, comPlanSnapshotId = snapshot.id)
|
||||
|
||||
verify(complexMissionService).snapshot(snapshot.id)
|
||||
verify(complexMissionService).snapshotModes(snapshot.id, missionStart, missionStop)
|
||||
verify(complexMissionService, never()).activeSnapshot(77L)
|
||||
assertEquals(snapshot.id, plan.stage4SnapshotId)
|
||||
assertEquals(snapshot.id, response.comPlanSnapshotId)
|
||||
assertEquals(0, response.surveyModesCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadSlots fails when active stage4 snapshot is not found`() {
|
||||
val missionId = UUID.randomUUID()
|
||||
val missionStart = LocalDateTime.of(2026, 3, 22, 10, 0)
|
||||
val missionStop = LocalDateTime.of(2026, 3, 22, 12, 0)
|
||||
val mission = MissionEntity(
|
||||
missionId = missionId,
|
||||
satelliteId = 77,
|
||||
missionStart = missionStart,
|
||||
missionStop = missionStop
|
||||
)
|
||||
val plan = PlanEntity(id = 113L, mission = mission, planStart = missionStart, planStop = missionStop, planNumber = 1)
|
||||
|
||||
`when`(missionRepository.findById(missionId)).thenReturn(java.util.Optional.of(mission))
|
||||
`when`(satelliteCatalogClient.getSatellite(77L)).thenReturn(SatelliteDTO(id = 77L, typeCode = "UNKNOWN"))
|
||||
`when`(planRepository.findFirstByMissionMissionIdAndPlanNumber(missionId, 1)).thenReturn(java.util.Optional.of(plan))
|
||||
`when`(complexMissionService.activeSnapshot(77L)).thenReturn(null)
|
||||
|
||||
val exception = assertThrows(CustomErrorException::class.java) {
|
||||
service.loadSlots(missionId)
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
"Active stage4 snapshot not found for satelliteId=77 and interval=[$missionStart - $missionStop]",
|
||||
exception.message
|
||||
)
|
||||
verify(complexMissionService).activeSnapshot(77L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `confirmMission publishes processed booked slots event`() {
|
||||
val missionId = UUID.randomUUID()
|
||||
val mission = MissionEntity(missionId = missionId, satelliteId = 77)
|
||||
val surveyMode = SurveyModeEntity(id = 11L)
|
||||
|
||||
`when`(missionRepository.findById(missionId)).thenReturn(java.util.Optional.of(mission))
|
||||
`when`(missionRepository.save(mission)).thenReturn(mission)
|
||||
`when`(surveyModeRepository.findAllByPlan_Mission_MissionIdOrderByTimeStartAsc(missionId))
|
||||
.thenReturn(listOf(surveyMode))
|
||||
`when`(modeBookingRepository.findAllByMode_IdIn(listOf(11L)))
|
||||
.thenReturn(listOf(ModeBookingEntity(mode = surveyMode, slotId = 1001L)))
|
||||
|
||||
val result = service.confirmMission(missionId)
|
||||
|
||||
assertEquals(listOf(1001L), result)
|
||||
verify(bookedSlotsEventPublisher).publish(
|
||||
listOf(1001L),
|
||||
BookedSlotStatus.PROCESSED,
|
||||
missionId,
|
||||
null,
|
||||
77L
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateSurveyModeStatus publishes terminal booked slot event`() {
|
||||
val missionId = UUID.randomUUID()
|
||||
val mission = MissionEntity(missionId = missionId, satelliteId = 77)
|
||||
val plan = PlanEntity(id = 101L, mission = mission, planNumber = 1)
|
||||
val surveyMode = SurveyModeEntity(id = 99L, plan = plan, status = SurveyModeStatus.PLANNED)
|
||||
|
||||
`when`(surveyModeRepository.findById(99L)).thenReturn(java.util.Optional.of(surveyMode))
|
||||
`when`(surveyModeRepository.save(surveyMode)).thenReturn(surveyMode)
|
||||
`when`(modeBookingRepository.findAllByMode_IdIn(listOf(99L)))
|
||||
.thenReturn(listOf(ModeBookingEntity(mode = surveyMode, slotId = 501L)))
|
||||
|
||||
service.updateSurveyModeStatus(99L, SurveyModeStatus.PROCESSED)
|
||||
|
||||
assertEquals(SurveyModeStatus.PROCESSED, surveyMode.status)
|
||||
verify(bookedSlotsEventPublisher).publish(
|
||||
listOf(501L),
|
||||
BookedSlotStatus.FINISHED,
|
||||
missionId,
|
||||
99L,
|
||||
77L
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateSurveyModeStatus without bookings does not publish event`() {
|
||||
val surveyMode = SurveyModeEntity(id = 77L, status = SurveyModeStatus.PLANNED)
|
||||
|
||||
`when`(surveyModeRepository.findById(77L)).thenReturn(java.util.Optional.of(surveyMode))
|
||||
`when`(surveyModeRepository.save(surveyMode)).thenReturn(surveyMode)
|
||||
`when`(modeBookingRepository.findAllByMode_IdIn(listOf(77L))).thenReturn(emptyList())
|
||||
|
||||
service.updateSurveyModeStatus(77L, SurveyModeStatus.FAILED)
|
||||
|
||||
assertEquals(SurveyModeStatus.FAILED, surveyMode.status)
|
||||
verifyNoInteractions(bookedSlotsEventPublisher)
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package space.nstart.pcp.pcp_mission_planing_service.service
|
||||
|
||||
import ch.qos.logback.classic.Level
|
||||
import ch.qos.logback.classic.Logger
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent
|
||||
import ch.qos.logback.core.read.ListAppender
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.verifyNoInteractions
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.ObjectProvider
|
||||
import space.nstart.pcp.pcp_mission_planing_service.entity.SurveyModeEntity
|
||||
import space.nstart.pcp.pcp_types_lib.dto.routes.AngleRangeDto
|
||||
import space.nstart.pcp.pcp_types_lib.dto.routes.RoutePassportDto
|
||||
import space.nstart.pcp.pcp_types_lib.dto.routes.SurveyModeStatus
|
||||
import space.nstart.pcp.pcp_types_lib.message.KafkaMessage
|
||||
import space.nstart.pcp.pcp_types_lib.message.PcpKafkaEvent
|
||||
import java.math.BigDecimal
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
class ModeStatusChangedKafkaListenerTest {
|
||||
|
||||
private val objectMapper = jacksonObjectMapper().findAndRegisterModules()
|
||||
private val missionPlaningService = mock(MissionPlaningService::class.java)
|
||||
private val listener = ModeStatusChangedKafkaListener(
|
||||
objectMapperProvider = SingleObjectProvider(objectMapper),
|
||||
missionPlaningService = missionPlaningService,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `consume finds mode and updates its status`() {
|
||||
val routePassport = routePassportDto(routeStatus = SurveyModeStatus.PROCESSED)
|
||||
val mode = SurveyModeEntity(id = 15L)
|
||||
val appender = attachAppender()
|
||||
val payload = objectMapper.writeValueAsString(
|
||||
KafkaMessage(
|
||||
type = PcpKafkaEvent.ModeStatusChangedEvent,
|
||||
data = routePassport,
|
||||
).apply {
|
||||
source = "route-processing"
|
||||
}
|
||||
)
|
||||
`when`(missionPlaningService.findMode(routePassport)).thenReturn(mode)
|
||||
|
||||
listener.consume(payload)
|
||||
|
||||
verify(missionPlaningService).findMode(routePassport)
|
||||
verify(missionPlaningService).updateSurveyModeStatus(15L, SurveyModeStatus.PROCESSED)
|
||||
val infoLog = appender.list.last { it.level == Level.INFO }
|
||||
assertTrue(infoLog.formattedMessage.contains("Mode status changed event processed"))
|
||||
assertTrue(infoLog.formattedMessage.contains("surveyModeId=15"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `consume logs error when mode is not found`() {
|
||||
val routePassport = routePassportDto(routeStatus = SurveyModeStatus.FAILED)
|
||||
val appender = attachAppender()
|
||||
val payload = objectMapper.writeValueAsString(
|
||||
KafkaMessage(
|
||||
type = PcpKafkaEvent.ModeStatusChangedEvent,
|
||||
data = routePassport,
|
||||
).apply {
|
||||
source = "route-processing"
|
||||
}
|
||||
)
|
||||
`when`(missionPlaningService.findMode(routePassport)).thenReturn(null)
|
||||
|
||||
listener.consume(payload)
|
||||
|
||||
verify(missionPlaningService).findMode(routePassport)
|
||||
verify(missionPlaningService, never()).updateSurveyModeStatus(15L, SurveyModeStatus.FAILED)
|
||||
val errorLog = appender.list.last { it.level == Level.ERROR }
|
||||
assertTrue(errorLog.formattedMessage.contains("Survey mode not found for ModeStatusChangedEvent"))
|
||||
assertTrue(errorLog.formattedMessage.contains("routeStatus=FAILED"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `consume ignores unsupported kafka event type`() {
|
||||
val appender = attachAppender()
|
||||
val payload = objectMapper.writeValueAsString(
|
||||
KafkaMessage(
|
||||
type = PcpKafkaEvent.RouteGeoRefEvent,
|
||||
data = routePassportDto(),
|
||||
)
|
||||
)
|
||||
|
||||
listener.consume(payload)
|
||||
|
||||
val debugLog = appender.list.last { it.level == Level.DEBUG }
|
||||
assertEquals("Ignoring Kafka message with unsupported type RouteGeoRefEvent", debugLog.formattedMessage)
|
||||
verifyNoInteractions(missionPlaningService)
|
||||
}
|
||||
|
||||
private fun attachAppender(): ListAppender<ILoggingEvent> {
|
||||
val logger = LoggerFactory.getLogger(ModeStatusChangedKafkaListener::class.java) as Logger
|
||||
logger.detachAndStopAllAppenders()
|
||||
val appender = ListAppender<ILoggingEvent>()
|
||||
appender.start()
|
||||
logger.addAppender(appender)
|
||||
logger.level = Level.DEBUG
|
||||
return appender
|
||||
}
|
||||
|
||||
private fun routePassportDto(routeStatus: SurveyModeStatus = SurveyModeStatus.PROCESSED): RoutePassportDto =
|
||||
RoutePassportDto(
|
||||
routeId = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
|
||||
kaShort = "22",
|
||||
kaFull = "SATELLITE-22",
|
||||
routeNameFull = "ROUTE-001",
|
||||
routeNameShort = "R-001",
|
||||
orbitNumber = 42,
|
||||
orbitState = "ASC",
|
||||
intervalBegin = LocalDateTime.of(2026, 4, 3, 12, 0),
|
||||
intervalEnd = LocalDateTime.of(2026, 4, 3, 12, 5),
|
||||
rollAngle = AngleRangeDto(BigDecimal.ZERO, BigDecimal.ONE),
|
||||
visirAngle = AngleRangeDto(BigDecimal.ZERO, BigDecimal.ONE),
|
||||
resolutionRange = BigDecimal.ONE,
|
||||
resolutionAzimuth = BigDecimal.ONE,
|
||||
polarisation = listOf("VV"),
|
||||
processingLevel = "L1",
|
||||
geometry = "POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))",
|
||||
routeStatus = routeStatus,
|
||||
)
|
||||
|
||||
private class SingleObjectProvider<T : Any>(private val value: T) : ObjectProvider<T> {
|
||||
override fun getObject(vararg args: Any?): T = value
|
||||
override fun getIfAvailable(): T = value
|
||||
override fun getIfUnique(): T = value
|
||||
override fun getObject(): T = value
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package space.nstart.pcp.pcp_mission_planing_service.service
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.`when`
|
||||
import space.nstart.pcp.pcp_mission_planing_service.repository.MissionRepository
|
||||
import space.nstart.pcp.pcp_mission_planing_service.repository.ModeRepository
|
||||
import space.nstart.pcp.pcp_mission_planing_service.repository.PlanRepository
|
||||
|
||||
class SatelliteDeletedServiceTest {
|
||||
|
||||
@Test
|
||||
fun `deleteSatelliteData removes missions and cascaded plans and modes`() {
|
||||
val missionRepository = mock(MissionRepository::class.java)
|
||||
val planRepository = mock(PlanRepository::class.java)
|
||||
val modeRepository = mock(ModeRepository::class.java)
|
||||
val service = SatelliteDeletedService(missionRepository, planRepository, modeRepository)
|
||||
|
||||
`when`(missionRepository.countBySatelliteId(56756L)).thenReturn(2L)
|
||||
`when`(planRepository.countByMissionSatelliteId(56756L)).thenReturn(3L)
|
||||
`when`(modeRepository.countByPlan_Mission_SatelliteId(56756L)).thenReturn(5L)
|
||||
`when`(missionRepository.deleteAllBySatelliteId(56756L)).thenReturn(2L)
|
||||
|
||||
val summary = service.deleteSatelliteData(56756L)
|
||||
|
||||
assertEquals(56756L, summary.satelliteId)
|
||||
assertEquals(2L, summary.missionsDeleted)
|
||||
assertEquals(3L, summary.plansDeletedByCascade)
|
||||
assertEquals(5L, summary.modesDeletedByCascade)
|
||||
verify(missionRepository).deleteAllBySatelliteId(56756L)
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package space.nstart.pcp.pcp_mission_planing_service.service.satellite_type
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertSame
|
||||
import org.junit.jupiter.api.Test
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteDTO
|
||||
|
||||
class SatelliteMissionTypeFactoryTest {
|
||||
private val defaultType = DefaultSatelliteMissionType()
|
||||
private val emissioType = EmissioSatelliteMissionType()
|
||||
private val reflixioType = ReflixioSatelliteMissionType()
|
||||
private val factory = SatelliteMissionTypeFactory(
|
||||
missionTypes = listOf(defaultType, emissioType, reflixioType),
|
||||
defaultMissionType = defaultType
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `returns Emissio type by satellite dto typeCode ignoring case`() {
|
||||
val type = factory.getBySatellite(SatelliteDTO(id = 1, typeCode = "Emissio"))
|
||||
|
||||
assertSame(emissioType, type)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns Reflixio type by satellite dto typeCode ignoring case`() {
|
||||
val type = factory.getBySatellite(SatelliteDTO(id = 2, typeCode = "reflixio"))
|
||||
|
||||
assertSame(reflixioType, type)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns default type for blank or unknown typeCode`() {
|
||||
assertSame(defaultType, factory.getBySatellite(SatelliteDTO(id = 3, typeCode = "")))
|
||||
assertSame(defaultType, factory.getBySatellite(SatelliteDTO(id = 4, typeCode = "unknown")))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
spring:
|
||||
config:
|
||||
import: "optional:configserver:"
|
||||
cloud:
|
||||
config:
|
||||
enabled: false
|
||||
datasource:
|
||||
url: jdbc:h2:mem:pcp_mission_planing;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE
|
||||
driver-class-name: org.h2.Driver
|
||||
username: sa
|
||||
password:
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: create-drop
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.H2Dialect
|
||||
flyway:
|
||||
enabled: false
|
||||
|
||||
camunda:
|
||||
client:
|
||||
enabled: false
|
||||
|
||||
settings:
|
||||
ballistics-service: http://localhost:7003
|
||||
complex-mission-service: http://localhost:7002
|
||||
satellite-catalog-service: http://localhost:7013
|
||||
stations-service: http://localhost:7009
|
||||
Reference in New Issue
Block a user