This commit is contained in:
Дмитрий Соловьев
2026-05-25 14:23:52 +03:00
parent b3a6012ebb
commit d48ddd2657
1066 changed files with 104601 additions and 3 deletions
@@ -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)
}
}
@@ -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)
}
}
@@ -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())
}
}
@@ -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
}
}
@@ -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)
}
}
@@ -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
}
}
@@ -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)
}
}
@@ -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