Init
This commit is contained in:
+13
@@ -0,0 +1,13 @@
|
||||
package space.nstart.pcp.pcp_satellites_service
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
|
||||
@SpringBootTest
|
||||
class PcpBallisticsServiceApplicationTests {
|
||||
|
||||
@Test
|
||||
fun contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.controller
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.mockito.Mockito.doReturn
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.verifyNoInteractions
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.boot.test.system.CapturedOutput
|
||||
import org.springframework.boot.test.system.OutputCaptureExtension
|
||||
import org.springframework.boot.test.web.server.LocalServerPort
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunResponseDTO
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunStatus
|
||||
import space.nstart.pcp.pcp_satellites_service.service.ComplexMissionService
|
||||
import space.nstart.pcp.pcp_satellites_service.service.ComplexPlanRunOrchestrationService
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.ComplexPlanProcessRequestDTO
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import kotlin.test.assertContains
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@ExtendWith(OutputCaptureExtension::class)
|
||||
class ComplexPlanControllerTest {
|
||||
|
||||
@LocalServerPort
|
||||
private var port: Int = 0
|
||||
|
||||
@MockitoBean
|
||||
private lateinit var complexMissionService: ComplexMissionService
|
||||
|
||||
@MockitoBean
|
||||
private lateinit var complexPlanRunOrchestrationService: ComplexPlanRunOrchestrationService
|
||||
|
||||
@Test
|
||||
fun `process endpoint uses POST and logs response body as json`(output: CapturedOutput) {
|
||||
val request = ComplexPlanProcessRequestDTO(
|
||||
intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0),
|
||||
intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0),
|
||||
satelliteIds = listOf(22L)
|
||||
)
|
||||
doReturn(
|
||||
ComplexPlanRunResponseDTO(
|
||||
runId = 1L,
|
||||
status = ComplexPlanRunStatus.COMPLETED,
|
||||
intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0),
|
||||
intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0),
|
||||
satelliteIds = listOf(22L),
|
||||
requestedBy = "tester",
|
||||
createdAt = LocalDateTime.of(2026, 3, 26, 9, 59).atOffset(ZoneOffset.UTC),
|
||||
startedAt = LocalDateTime.of(2026, 3, 26, 10, 0).atOffset(ZoneOffset.UTC),
|
||||
finishedAt = LocalDateTime.of(2026, 3, 26, 10, 1).atOffset(ZoneOffset.UTC),
|
||||
durationMs = 60000,
|
||||
errorMessage = null,
|
||||
calculatedModesCount = 1,
|
||||
bookedModesCount = 1,
|
||||
complanModesCount = 0,
|
||||
mixedModesCount = 0,
|
||||
affectedSatellitesCount = 1,
|
||||
processedCellsCount = 1,
|
||||
bookedSlotLinksCount = 1,
|
||||
snapshotIds = listOf(1001L),
|
||||
requestPayload = "{}"
|
||||
)
|
||||
).`when`(complexPlanRunOrchestrationService).process(request, "tester")
|
||||
|
||||
WebClient.create("http://localhost:$port")
|
||||
.post()
|
||||
.uri("/api/com-plan/process")
|
||||
.header("X-Requested-By", "tester")
|
||||
.bodyValue(
|
||||
mapOf(
|
||||
"intervalStart" to "2026-03-26T10:00:00",
|
||||
"intervalEnd" to "2026-03-26T12:00:00",
|
||||
"satelliteIds" to listOf(22)
|
||||
)
|
||||
)
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java)
|
||||
.block()
|
||||
|
||||
verify(complexPlanRunOrchestrationService).process(
|
||||
request,
|
||||
"tester"
|
||||
)
|
||||
assertContains(output.out, "Complex plan process completed: runId=1, status=COMPLETED")
|
||||
assertContains(output.out, "responseBody={")
|
||||
assertContains(output.out, """"runId":1""")
|
||||
assertContains(output.out, """"status":"COMPLETED"""")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process endpoint rejects sun outside allowed range`() {
|
||||
val response = WebClient.create("http://localhost:$port")
|
||||
.post()
|
||||
.uri("/api/com-plan/process")
|
||||
.bodyValue(
|
||||
mapOf(
|
||||
"intervalStart" to "2026-03-26T10:00:00",
|
||||
"intervalEnd" to "2026-03-26T12:00:00",
|
||||
"satelliteIds" to listOf(22),
|
||||
"sun" to 90.1
|
||||
)
|
||||
)
|
||||
.exchangeToMono { clientResponse ->
|
||||
clientResponse.bodyToMono(String::class.java)
|
||||
.defaultIfEmpty("")
|
||||
.map { body -> clientResponse.statusCode().value() to body }
|
||||
}
|
||||
.block()!!
|
||||
|
||||
assertEquals(400, response.first)
|
||||
assertTrue(response.second.contains("sun"))
|
||||
verifyNoInteractions(complexPlanRunOrchestrationService)
|
||||
}
|
||||
}
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.controller
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.boot.test.web.server.LocalServerPort
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunCreateDTO
|
||||
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunResponseDTO
|
||||
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunStatisticsDTO
|
||||
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunSummaryDTO
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunStatus
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeSource
|
||||
import space.nstart.pcp.pcp_satellites_service.model.mode.SurveyMode
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.ComplexPlanRunRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeBookedSlotRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeCellRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeSnapshotRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.service.ComplexPlanRunPersistenceService
|
||||
import space.nstart.pcp.pcp_satellites_service.service.SatelliteModePersistenceService
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SatelliteModeResponseDTO
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
class ComplexPlanRunApiTest {
|
||||
|
||||
@LocalServerPort
|
||||
private var port: Int = 0
|
||||
|
||||
@Autowired
|
||||
private lateinit var complexPlanRunPersistenceService: ComplexPlanRunPersistenceService
|
||||
|
||||
@Autowired
|
||||
private lateinit var complexPlanRunRepository: ComplexPlanRunRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var satelliteModeRepository: SatelliteModeRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var satelliteModeBookedSlotRepository: SatelliteModeBookedSlotRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var satelliteModeCellRepository: SatelliteModeCellRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var satelliteModeSnapshotRepository: SatelliteModeSnapshotRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var satelliteModePersistenceService: SatelliteModePersistenceService
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
satelliteModeBookedSlotRepository.deleteAll()
|
||||
satelliteModeCellRepository.deleteAll()
|
||||
satelliteModeRepository.deleteAll()
|
||||
satelliteModeSnapshotRepository.deleteAll()
|
||||
complexPlanRunRepository.deleteAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `run details endpoint returns persisted run`() {
|
||||
val runId = createCompletedRun(listOf(22L), "api-user")
|
||||
|
||||
val response = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/api/com-plan/runs/$runId")
|
||||
.retrieve()
|
||||
.bodyToMono(ComplexPlanRunResponseDTO::class.java)
|
||||
.block()!!
|
||||
|
||||
assertEquals(runId, response.runId)
|
||||
assertEquals(ComplexPlanRunStatus.COMPLETED, response.status)
|
||||
assertEquals(listOf(22L), response.satelliteIds)
|
||||
assertEquals("api-user", response.requestedBy)
|
||||
assertEquals(3, response.calculatedModesCount)
|
||||
assertEquals(10.0, response.totalRequestsArea)
|
||||
assertEquals(4.0, response.coveredArea)
|
||||
assertEquals(40.0, response.coveredAreaPercent)
|
||||
assertEquals(ZoneOffset.UTC, response.createdAt.offset)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `runs endpoint supports status and satellite filter`() {
|
||||
val completedRunId = createCompletedRun(listOf(22L), "one")
|
||||
createFailedRun(listOf(55L))
|
||||
|
||||
val response = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/api/com-plan/runs?status=COMPLETED&satelliteId=22")
|
||||
.retrieve()
|
||||
.bodyToFlux(ComplexPlanRunSummaryDTO::class.java)
|
||||
.collectList()
|
||||
.block()
|
||||
.orEmpty()
|
||||
|
||||
assertEquals(1, response.size)
|
||||
assertEquals(completedRunId, response.single().runId)
|
||||
assertEquals(ComplexPlanRunStatus.COMPLETED, response.single().status)
|
||||
assertEquals(listOf(22L), response.single().satelliteIds)
|
||||
assertEquals(40.0, response.single().coveredAreaPercent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `delete run endpoint removes persisted run`() {
|
||||
val runId = createCompletedRun(listOf(22L), "api-user")
|
||||
|
||||
WebClient.create("http://localhost:$port")
|
||||
.delete()
|
||||
.uri("/api/com-plan/runs/$runId")
|
||||
.retrieve()
|
||||
.toBodilessEntity()
|
||||
.block()
|
||||
|
||||
assertFalse(complexPlanRunRepository.existsById(runId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `run modes endpoint returns modes sorted by time`() {
|
||||
val runId = createCompletedRun(listOf(22L), "api-user")
|
||||
val run = complexPlanRunRepository.findById(runId).orElseThrow()
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0)
|
||||
val intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0)
|
||||
satelliteModePersistenceService.saveModesForInterval(
|
||||
satelliteIds = listOf(22L),
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
surveysBySatelliteId = mapOf(
|
||||
22L to listOf(
|
||||
SurveyMode(
|
||||
revolution = 2,
|
||||
time = intervalStart.plusMinutes(20),
|
||||
timeStop = intervalStart.plusMinutes(30),
|
||||
roll = 5.0,
|
||||
contourWKT = "POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0))",
|
||||
source = SatelliteModeSource.COMPLAN
|
||||
),
|
||||
SurveyMode(
|
||||
revolution = 1,
|
||||
time = intervalStart.plusMinutes(5),
|
||||
timeStop = intervalStart.plusMinutes(10),
|
||||
roll = 4.0,
|
||||
contourWKT = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||
source = SatelliteModeSource.COMPLAN,
|
||||
cellNums = listOf(42L)
|
||||
)
|
||||
)
|
||||
),
|
||||
complexPlanRun = run
|
||||
)
|
||||
|
||||
val response = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/api/com-plan/runs/$runId/modes")
|
||||
.retrieve()
|
||||
.bodyToFlux(SatelliteModeResponseDTO::class.java)
|
||||
.collectList()
|
||||
.block()
|
||||
.orEmpty()
|
||||
|
||||
assertEquals(listOf(1L, 2L), response.map { it.revolution })
|
||||
assertEquals(listOf(42L), response.first().cellNums)
|
||||
}
|
||||
|
||||
private fun createCompletedRun(satelliteIds: List<Long>, requestedBy: String?): Long {
|
||||
val run = complexPlanRunPersistenceService.createRun(
|
||||
ComplexPlanRunCreateDTO(
|
||||
intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0),
|
||||
intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0),
|
||||
satelliteIds = satelliteIds,
|
||||
requestedBy = requestedBy,
|
||||
requestPayload = "{}"
|
||||
)
|
||||
)
|
||||
complexPlanRunPersistenceService.markRunning(run.id!!)
|
||||
complexPlanRunPersistenceService.markCompleted(
|
||||
run.id!!,
|
||||
ComplexPlanRunStatisticsDTO(
|
||||
calculatedModesCount = 3,
|
||||
bookedModesCount = 1,
|
||||
complanModesCount = 1,
|
||||
mixedModesCount = 1,
|
||||
affectedSatellitesCount = satelliteIds.size,
|
||||
processedCellsCount = 2,
|
||||
bookedSlotLinksCount = 4,
|
||||
totalRequestsArea = 10.0,
|
||||
coveredArea = 4.0,
|
||||
coveredAreaPercent = 40.0
|
||||
)
|
||||
)
|
||||
return run.id!!
|
||||
}
|
||||
|
||||
private fun createFailedRun(satelliteIds: List<Long>): Long {
|
||||
val run = complexPlanRunPersistenceService.createRun(
|
||||
ComplexPlanRunCreateDTO(
|
||||
intervalStart = LocalDateTime.of(2026, 3, 26, 15, 0),
|
||||
intervalEnd = LocalDateTime.of(2026, 3, 26, 16, 0),
|
||||
satelliteIds = satelliteIds,
|
||||
requestedBy = null,
|
||||
requestPayload = "{}"
|
||||
)
|
||||
)
|
||||
complexPlanRunPersistenceService.markRunning(run.id!!)
|
||||
complexPlanRunPersistenceService.markFailed(run.id!!, "boom")
|
||||
return run.id!!
|
||||
}
|
||||
}
|
||||
+352
@@ -0,0 +1,352 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.controller
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.boot.test.web.server.LocalServerPort
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunCreateDTO
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeSource
|
||||
import space.nstart.pcp.pcp_satellites_service.model.mode.SurveyMode
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.ComplexPlanRunRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeBookedSlotRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeCellRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeSnapshotRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.service.ComplexPlanRunPersistenceService
|
||||
import space.nstart.pcp.pcp_satellites_service.service.SatelliteModePersistenceService
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SatelliteModeResponseDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SatelliteModeSnapshotResponseDTO
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
class SatelliteModeApiTest {
|
||||
|
||||
@LocalServerPort
|
||||
private var port: Int = 0
|
||||
|
||||
@Autowired
|
||||
private lateinit var satelliteModePersistenceService: SatelliteModePersistenceService
|
||||
|
||||
@Autowired
|
||||
private lateinit var satelliteModeRepository: SatelliteModeRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var satelliteModeBookedSlotRepository: SatelliteModeBookedSlotRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var satelliteModeCellRepository: SatelliteModeCellRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var satelliteModeSnapshotRepository: SatelliteModeSnapshotRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var complexPlanRunPersistenceService: ComplexPlanRunPersistenceService
|
||||
|
||||
@Autowired
|
||||
private lateinit var complexPlanRunRepository: ComplexPlanRunRepository
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
satelliteModeBookedSlotRepository.deleteAll()
|
||||
satelliteModeCellRepository.deleteAll()
|
||||
satelliteModeRepository.deleteAll()
|
||||
satelliteModeSnapshotRepository.deleteAll()
|
||||
complexPlanRunRepository.deleteAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `modes endpoint returns interval-filtered modes from active snapshot regardless of snapshot interval`() {
|
||||
val snapshotStart = LocalDateTime.of(2026, 3, 26, 8, 0)
|
||||
val snapshotEnd = LocalDateTime.of(2026, 3, 26, 14, 0)
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0)
|
||||
val intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0)
|
||||
satelliteModePersistenceService.saveModesForInterval(
|
||||
satelliteIds = listOf(22L),
|
||||
intervalStart = snapshotStart,
|
||||
intervalEnd = snapshotEnd,
|
||||
surveysBySatelliteId = mapOf(
|
||||
22L to listOf(
|
||||
SurveyMode(
|
||||
revolution = 100,
|
||||
time = intervalStart.plusMinutes(1),
|
||||
timeStop = intervalStart.plusMinutes(10),
|
||||
roll = 5.0,
|
||||
contourWKT = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||
source = SatelliteModeSource.COMPLAN,
|
||||
bookedSlotIds = listOf(777L),
|
||||
cellNums = listOf(42L, 43L)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val response = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/api/satellites/modes?intervalStart=2026-03-26T10:00:00&intervalEnd=2026-03-26T12:00:00&satelliteId=22")
|
||||
.retrieve()
|
||||
.bodyToFlux(SatelliteModeResponseDTO::class.java)
|
||||
.collectList()
|
||||
.block()
|
||||
.orEmpty()
|
||||
|
||||
assertEquals(1, response.size)
|
||||
assertEquals(22L, response.single().satelliteId)
|
||||
assertEquals("COMPLAN", response.single().source)
|
||||
assertEquals(listOf(777L), response.single().bookedSlotIds)
|
||||
assertEquals(listOf(42L, 43L), response.single().cellNums)
|
||||
assertNotNull(response.single().snapshotId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `satellite scoped modes endpoint returns interval-filtered modes from active snapshot`() {
|
||||
val snapshotStart = LocalDateTime.of(2026, 3, 27, 8, 0)
|
||||
val snapshotEnd = LocalDateTime.of(2026, 3, 27, 14, 0)
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 27, 10, 0)
|
||||
val intervalEnd = LocalDateTime.of(2026, 3, 27, 12, 0)
|
||||
satelliteModePersistenceService.saveModesForInterval(
|
||||
satelliteIds = listOf(22L),
|
||||
intervalStart = snapshotStart,
|
||||
intervalEnd = snapshotEnd,
|
||||
surveysBySatelliteId = mapOf(
|
||||
22L to listOf(
|
||||
SurveyMode(
|
||||
revolution = 101,
|
||||
time = intervalStart.plusMinutes(2),
|
||||
timeStop = intervalStart.plusMinutes(8),
|
||||
roll = 6.0,
|
||||
contourWKT = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||
source = SatelliteModeSource.MANUAL,
|
||||
bookedSlotIds = listOf(778L),
|
||||
cellNums = listOf(44L)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val response = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/api/satellites/22/modes?intervalStart=2026-03-27T10:00:00&intervalEnd=2026-03-27T12:00:00")
|
||||
.retrieve()
|
||||
.bodyToFlux(SatelliteModeResponseDTO::class.java)
|
||||
.collectList()
|
||||
.block()
|
||||
.orEmpty()
|
||||
|
||||
assertEquals(1, response.size)
|
||||
assertEquals("MANUAL", response.single().source)
|
||||
assertEquals(listOf(778L), response.single().bookedSlotIds)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `satellite scoped modes endpoint returns modes for requested runId`() {
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 27, 10, 0)
|
||||
val intervalEnd = LocalDateTime.of(2026, 3, 27, 12, 0)
|
||||
val firstRun = complexPlanRunPersistenceService.createRun(
|
||||
ComplexPlanRunCreateDTO(
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
satelliteIds = listOf(22L),
|
||||
requestedBy = "tester",
|
||||
requestPayload = "{}"
|
||||
)
|
||||
)
|
||||
val secondRun = complexPlanRunPersistenceService.createRun(
|
||||
ComplexPlanRunCreateDTO(
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
satelliteIds = listOf(22L),
|
||||
requestedBy = "tester",
|
||||
requestPayload = "{}"
|
||||
)
|
||||
)
|
||||
|
||||
satelliteModePersistenceService.saveModesForInterval(
|
||||
satelliteIds = listOf(22L),
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
surveysBySatelliteId = mapOf(
|
||||
22L to listOf(
|
||||
SurveyMode(
|
||||
revolution = 201,
|
||||
time = intervalStart.plusMinutes(2),
|
||||
timeStop = intervalStart.plusMinutes(8),
|
||||
roll = 6.0,
|
||||
contourWKT = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||
source = SatelliteModeSource.COMPLAN
|
||||
)
|
||||
)
|
||||
),
|
||||
complexPlanRun = firstRun
|
||||
)
|
||||
satelliteModePersistenceService.saveModesForInterval(
|
||||
satelliteIds = listOf(22L),
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
surveysBySatelliteId = mapOf(
|
||||
22L to listOf(
|
||||
SurveyMode(
|
||||
revolution = 202,
|
||||
time = intervalStart.plusMinutes(3),
|
||||
timeStop = intervalStart.plusMinutes(9),
|
||||
roll = 7.0,
|
||||
contourWKT = "POLYGON ((0 0, 2 0, 2 1, 0 1, 0 0))",
|
||||
source = SatelliteModeSource.MANUAL
|
||||
)
|
||||
)
|
||||
),
|
||||
complexPlanRun = secondRun
|
||||
)
|
||||
|
||||
val response = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri(
|
||||
"/api/satellites/22/modes" +
|
||||
"?intervalStart=2026-03-27T10:00:00" +
|
||||
"&intervalEnd=2026-03-27T12:00:00" +
|
||||
"&runId=${firstRun.id}"
|
||||
)
|
||||
.retrieve()
|
||||
.bodyToFlux(SatelliteModeResponseDTO::class.java)
|
||||
.collectList()
|
||||
.block()
|
||||
.orEmpty()
|
||||
|
||||
assertEquals(1, response.size)
|
||||
assertEquals(201L, response.single().revolution)
|
||||
assertEquals("COMPLAN", response.single().source)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `active snapshot endpoint ignores interval and resolves by satellite only`() {
|
||||
val snapshotStart = LocalDateTime.of(2026, 3, 30, 8, 0)
|
||||
val snapshotEnd = LocalDateTime.of(2026, 3, 30, 14, 0)
|
||||
val queryStart = LocalDateTime.of(2026, 3, 30, 10, 0)
|
||||
val queryEnd = LocalDateTime.of(2026, 3, 30, 11, 0)
|
||||
val snapshotId = satelliteModePersistenceService.saveModesForInterval(
|
||||
satelliteIds = listOf(22L),
|
||||
intervalStart = snapshotStart,
|
||||
intervalEnd = snapshotEnd,
|
||||
surveysBySatelliteId = mapOf(
|
||||
22L to listOf(
|
||||
SurveyMode(
|
||||
revolution = 301,
|
||||
time = queryStart.plusMinutes(5),
|
||||
timeStop = queryStart.plusMinutes(15),
|
||||
roll = 5.0,
|
||||
contourWKT = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||
source = SatelliteModeSource.COMPLAN
|
||||
)
|
||||
)
|
||||
)
|
||||
).snapshotIds.single()
|
||||
|
||||
val response = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/api/satellites/22/snapshots/active?intervalStart=$queryStart&intervalEnd=$queryEnd")
|
||||
.retrieve()
|
||||
.bodyToMono(SatelliteModeSnapshotResponseDTO::class.java)
|
||||
.block()!!
|
||||
|
||||
assertEquals(snapshotId, response.id)
|
||||
assertEquals(22L, response.satelliteId)
|
||||
assertEquals(snapshotStart, response.intervalStart)
|
||||
assertEquals(snapshotEnd, response.intervalEnd)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `snapshot endpoints return metadata and modes for explicit snapshot`() {
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 28, 10, 0)
|
||||
val intervalEnd = LocalDateTime.of(2026, 3, 28, 12, 0)
|
||||
val snapshotId = satelliteModePersistenceService.saveModesForInterval(
|
||||
satelliteIds = listOf(22L),
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
surveysBySatelliteId = mapOf(
|
||||
22L to listOf(
|
||||
SurveyMode(
|
||||
revolution = 102,
|
||||
time = intervalStart.plusMinutes(3),
|
||||
timeStop = intervalStart.plusMinutes(9),
|
||||
roll = 6.0,
|
||||
contourWKT = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||
source = SatelliteModeSource.COMPLAN
|
||||
)
|
||||
)
|
||||
)
|
||||
).snapshotIds.single()
|
||||
|
||||
val snapshot = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/api/satellites/snapshots/$snapshotId")
|
||||
.retrieve()
|
||||
.bodyToMono(SatelliteModeSnapshotResponseDTO::class.java)
|
||||
.block()!!
|
||||
val modes = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/api/satellites/snapshots/$snapshotId/modes")
|
||||
.retrieve()
|
||||
.bodyToFlux(SatelliteModeResponseDTO::class.java)
|
||||
.collectList()
|
||||
.block()
|
||||
.orEmpty()
|
||||
|
||||
assertEquals(snapshotId, snapshot.id)
|
||||
assertEquals(22L, snapshot.satelliteId)
|
||||
assertEquals(intervalStart, snapshot.intervalStart)
|
||||
assertEquals(intervalEnd, snapshot.intervalEnd)
|
||||
assertEquals(true, snapshot.isActive)
|
||||
assertEquals(listOf(snapshotId), modes.mapNotNull { it.snapshotId }.distinct())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `snapshot modes endpoint filters by requested interval`() {
|
||||
val snapshotStart = LocalDateTime.of(2026, 3, 29, 8, 0)
|
||||
val snapshotEnd = LocalDateTime.of(2026, 3, 29, 14, 0)
|
||||
val requestedStart = LocalDateTime.of(2026, 3, 29, 10, 0)
|
||||
val requestedEnd = LocalDateTime.of(2026, 3, 29, 11, 0)
|
||||
val snapshotId = satelliteModePersistenceService.saveModesForInterval(
|
||||
satelliteIds = listOf(22L),
|
||||
intervalStart = snapshotStart,
|
||||
intervalEnd = snapshotEnd,
|
||||
surveysBySatelliteId = mapOf(
|
||||
22L to listOf(
|
||||
SurveyMode(
|
||||
revolution = 201,
|
||||
time = snapshotStart.plusMinutes(10),
|
||||
timeStop = snapshotStart.plusMinutes(20),
|
||||
roll = 5.0,
|
||||
contourWKT = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||
source = SatelliteModeSource.COMPLAN
|
||||
),
|
||||
SurveyMode(
|
||||
revolution = 202,
|
||||
time = requestedStart.plusMinutes(5),
|
||||
timeStop = requestedStart.plusMinutes(15),
|
||||
roll = 5.0,
|
||||
contourWKT = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||
source = SatelliteModeSource.MANUAL
|
||||
)
|
||||
)
|
||||
)
|
||||
).snapshotIds.single()
|
||||
|
||||
val modes = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri(
|
||||
"/api/satellites/snapshots/$snapshotId/modes?intervalStart=$requestedStart&intervalEnd=$requestedEnd"
|
||||
)
|
||||
.retrieve()
|
||||
.bodyToFlux(SatelliteModeResponseDTO::class.java)
|
||||
.collectList()
|
||||
.block()
|
||||
.orEmpty()
|
||||
|
||||
assertEquals(1, modes.size)
|
||||
assertEquals(202L, modes.single().revolution)
|
||||
assertEquals(snapshotId, modes.single().snapshotId)
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.dto
|
||||
|
||||
import jakarta.validation.Validation
|
||||
import org.junit.jupiter.api.Test
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.ComplexPlanProcessRequestDTO
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ComplexPlanProcessRequestDTOValidationTest {
|
||||
|
||||
private val validator = Validation.buildDefaultValidatorFactory().validator
|
||||
private val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0)
|
||||
private val intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0)
|
||||
|
||||
@Test
|
||||
fun `sun null is valid`() {
|
||||
assertTrue(validate(null).isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sun minus 90 is valid`() {
|
||||
assertTrue(validate(-90.0).isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sun 90 is valid`() {
|
||||
assertTrue(validate(90.0).isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sun below minus 90 is invalid`() {
|
||||
val violations = validate(-90.1)
|
||||
|
||||
assertEquals(1, violations.size)
|
||||
assertEquals("sun", violations.single().propertyPath.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sun above 90 is invalid`() {
|
||||
val violations = validate(90.1)
|
||||
|
||||
assertEquals(1, violations.size)
|
||||
assertEquals("sun", violations.single().propertyPath.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `aggregation counts null are valid`() {
|
||||
assertTrue(validate(sun = null, countLat = null, countLong = null).isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `aggregation counts one are valid`() {
|
||||
assertTrue(validate(sun = null, countLat = 1, countLong = 1).isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `countLat below one is invalid`() {
|
||||
val violations = validate(sun = null, countLat = 0, countLong = 1)
|
||||
|
||||
assertEquals(1, violations.size)
|
||||
assertEquals("countLat", violations.single().propertyPath.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `countLong below one is invalid`() {
|
||||
val violations = validate(sun = null, countLat = 1, countLong = 0)
|
||||
|
||||
assertEquals(1, violations.size)
|
||||
assertEquals("countLong", violations.single().propertyPath.toString())
|
||||
}
|
||||
|
||||
private fun validate(
|
||||
sun: Double?,
|
||||
countLat: Int? = null,
|
||||
countLong: Int? = null
|
||||
) =
|
||||
validator.validate(
|
||||
ComplexPlanProcessRequestDTO(
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
satelliteIds = listOf(22L),
|
||||
sun = sun,
|
||||
countLat = countLat,
|
||||
countLong = countLong
|
||||
)
|
||||
)
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.model.mode
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.LocalDateTime
|
||||
|
||||
class SurveyModeTest {
|
||||
|
||||
@Test
|
||||
fun `toDTO preserves slot ids and coordinates`() {
|
||||
val timeStart = LocalDateTime.of(2026, 3, 31, 12, 0)
|
||||
val timeStop = timeStart.plusMinutes(10)
|
||||
val surveyMode = SurveyMode(
|
||||
revolution = 42,
|
||||
time = timeStart,
|
||||
timeStop = timeStop,
|
||||
roll = 3.5,
|
||||
latitude = 55.75,
|
||||
longitude = 37.62,
|
||||
duration = 600.0,
|
||||
contourWKT = "POLYGON ((0 0, 1 0, 1 1, 0 0))",
|
||||
bookedSlotIds = listOf(10L, 20L)
|
||||
)
|
||||
|
||||
val dto = surveyMode.toDTO()
|
||||
|
||||
assertEquals(timeStart, dto.time)
|
||||
assertEquals(timeStop, dto.timStop)
|
||||
assertEquals(listOf(10L, 20L), dto.slotIds)
|
||||
assertEquals(55.75, dto.latitude)
|
||||
assertEquals(37.62, dto.longitude)
|
||||
}
|
||||
}
|
||||
+776
@@ -0,0 +1,776 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.ArgumentMatchers.anyList
|
||||
import org.mockito.ArgumentMatchers.anyMap
|
||||
import org.mockito.ArgumentMatchers.any
|
||||
import org.mockito.ArgumentMatchers.eq
|
||||
import org.mockito.ArgumentMatchers.isNull
|
||||
import org.mockito.Mockito.doReturn
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.Mockito.inOrder
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.times
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.stubbing.Answer
|
||||
import space.nstart.pcp.pcp_satellites_service.configuration.ComplexPlanProperties
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeSource
|
||||
import space.nstart.pcp.pcp_satellites_service.model.BLS
|
||||
import space.nstart.pcp.pcp_satellites_service.model.LongTermMission
|
||||
import space.nstart.pcp.pcp_satellites_service.model.SatelliteModel
|
||||
import space.nstart.pcp.pcp_satellites_service.model.mode.SurveyMode
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.CellRequestDOT
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.CoverageCalculationSource
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.EarthCellWithRequestsDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.OccupiedIntervalDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SurveySlotDTO
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ComplexPlanCalculationServiceTest {
|
||||
|
||||
private fun <T> anyListValue(): List<T> = anyList<T>() ?: emptyList()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun anySurveyMap(): Map<Long, List<SurveyMode>> =
|
||||
anyMap<Long, List<SurveyMode>>() as Map<Long, List<SurveyMode>>? ?: emptyMap()
|
||||
private fun <T> eqValue(value: T): T = eq(value) ?: value
|
||||
private fun captureMap(captor: ArgumentCaptor<Map<Long, List<SurveyMode>>>): Map<Long, List<SurveyMode>> =
|
||||
captor.capture() ?: emptyMap()
|
||||
private fun anyLocalDateTime(): LocalDateTime = any(LocalDateTime::class.java) ?: LocalDateTime.MIN
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun anyMutableLongList(): MutableList<Long> =
|
||||
(any(MutableList::class.java) as MutableList<Long>?) ?: mutableListOf()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun anyOccupiedIntervals(): List<OccupiedIntervalDTO> =
|
||||
(any(List::class.java) as List<OccupiedIntervalDTO>?) ?: emptyList()
|
||||
private fun captureOccupiedIntervals(captor: ArgumentCaptor<List<OccupiedIntervalDTO>>): List<OccupiedIntervalDTO> =
|
||||
captor.capture() ?: emptyList()
|
||||
private fun props(chunkSize: Int = 100) = ComplexPlanProperties().apply { batchChunkSize = chunkSize }
|
||||
private fun coverageBatchClient(): CoverageBatchClient =
|
||||
mock(CoverageBatchClient::class.java).also {
|
||||
`when`(it.source).thenReturn(CoverageCalculationSource.SLOTS)
|
||||
}
|
||||
private fun coverageResolver(vararg clients: CoverageCalculationClient) =
|
||||
CoverageCalculationClientResolver(clients.toList())
|
||||
|
||||
@Test
|
||||
fun `process calculates first and then atomically replaces persisted result`() {
|
||||
val coverageBatchClient = coverageBatchClient()
|
||||
val earthGridService = mock(EarthGridService::class.java)
|
||||
val satelliteService = SatelliteService(testSatelliteCatalogClient())
|
||||
val complexPlanQueryService = mock(ComplexPlanQueryService::class.java)
|
||||
val complexPlanPersistenceService = mock(ComplexPlanPersistenceService::class.java)
|
||||
val surveyPlacementService = SurveyPlacementService()
|
||||
|
||||
val service = ComplexPlanCalculationService(
|
||||
coverageBatchClient = coverageBatchClient,
|
||||
coverageCalculationClientResolver = coverageResolver(coverageBatchClient),
|
||||
earthGridService = earthGridService,
|
||||
satelliteService = satelliteService,
|
||||
complexPlanQueryService = complexPlanQueryService,
|
||||
complexPlanPersistenceService = complexPlanPersistenceService,
|
||||
complexPlanProperties = props(),
|
||||
surveyPlacementService = surveyPlacementService
|
||||
)
|
||||
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0)
|
||||
val intervalEnd = LocalDateTime.of(2026, 3, 27, 11, 0)
|
||||
val cell = EarthCellWithRequestsDTO(
|
||||
id = 100,
|
||||
num = 42,
|
||||
contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||
requests = emptyList()
|
||||
)
|
||||
|
||||
`when`(complexPlanQueryService.loadConstraintModes(listOf(22L), intervalStart, intervalEnd))
|
||||
.thenReturn(emptyMap())
|
||||
`when`(earthGridService.cells(isNull(), isNull())).thenReturn(listOf(cell))
|
||||
`when`(coverageBatchClient.plannedModes(listOf(22L), intervalStart, intervalEnd))
|
||||
.thenReturn(
|
||||
mapOf(
|
||||
22L to listOf(
|
||||
SurveySlotDTO(
|
||||
satelliteId = 22,
|
||||
tn = intervalStart.plusMinutes(5),
|
||||
tk = intervalStart.plusMinutes(15),
|
||||
roll = 10.0,
|
||||
contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||
revolution = 501,
|
||||
slotIds = listOf(9001)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
doReturn(
|
||||
mapOf(
|
||||
100L to listOf(
|
||||
SlotDTO(
|
||||
cycle = 0,
|
||||
satelliteId = 22,
|
||||
tn = intervalStart.plusMinutes(10),
|
||||
tk = intervalStart.plusMinutes(20),
|
||||
roll = 10.0,
|
||||
contour = "POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0))",
|
||||
revolution = 501,
|
||||
latitude = 10.0,
|
||||
longitude = 20.0
|
||||
)
|
||||
)
|
||||
)
|
||||
).`when`(coverageBatchClient).coverageModes(
|
||||
anyListValue(),
|
||||
anyLocalDateTime(),
|
||||
anyLocalDateTime(),
|
||||
anyListValue(),
|
||||
anyOccupiedIntervals(),
|
||||
isNull(),
|
||||
isNull()
|
||||
)
|
||||
doReturn(SatelliteModeSaveResult(calculatedModesCount = 1, bookedModesCount = 1, affectedSatellitesCount = 1))
|
||||
.`when`(complexPlanPersistenceService).replaceModesForInterval(
|
||||
eqValue(listOf(22L)),
|
||||
eqValue(intervalStart),
|
||||
eqValue(intervalEnd),
|
||||
anySurveyMap(),
|
||||
isNull()
|
||||
)
|
||||
|
||||
service.process(intervalStart, intervalEnd, listOf(22L))
|
||||
|
||||
val inOrder = inOrder(complexPlanPersistenceService, complexPlanQueryService, coverageBatchClient)
|
||||
inOrder.verify(complexPlanQueryService).loadConstraintModes(listOf(22L), intervalStart, intervalEnd)
|
||||
inOrder.verify(coverageBatchClient).plannedModes(listOf(22L), intervalStart, intervalEnd)
|
||||
inOrder.verify(coverageBatchClient).coverageModes(
|
||||
anyListValue(),
|
||||
eqValue(intervalStart),
|
||||
eqValue(intervalEnd),
|
||||
eqValue(listOf(22L)),
|
||||
anyOccupiedIntervals(),
|
||||
isNull(),
|
||||
isNull()
|
||||
)
|
||||
inOrder.verify(complexPlanPersistenceService).replaceModesForInterval(
|
||||
eqValue(listOf(22L)),
|
||||
eqValue(intervalStart),
|
||||
eqValue(intervalEnd),
|
||||
anySurveyMap(),
|
||||
isNull()
|
||||
)
|
||||
|
||||
val surveysCaptor = ArgumentCaptor.forClass(Map::class.java) as ArgumentCaptor<Map<Long, List<SurveyMode>>>
|
||||
verify(complexPlanPersistenceService).replaceModesForInterval(
|
||||
eqValue(listOf(22L)),
|
||||
eqValue(intervalStart),
|
||||
eqValue(intervalEnd),
|
||||
captureMap(surveysCaptor),
|
||||
isNull()
|
||||
)
|
||||
|
||||
val savedSurvey = surveysCaptor.value.getValue(22L).single()
|
||||
assertEquals(SatelliteModeSource.SLOTS, savedSurvey.source)
|
||||
assertEquals(listOf(9001L), savedSurvey.bookedSlotIds)
|
||||
assertTrue(savedSurvey.time <= intervalStart.plusMinutes(5))
|
||||
assertTrue(savedSurvey.timeStop >= intervalStart.plusMinutes(15))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process returns covered area percent by survey area union`() {
|
||||
val coverageBatchClient = coverageBatchClient()
|
||||
val earthGridService = mock(EarthGridService::class.java)
|
||||
val satelliteService = SatelliteService(testSatelliteCatalogClient())
|
||||
val complexPlanQueryService = mock(ComplexPlanQueryService::class.java)
|
||||
val complexPlanPersistenceService = mock(ComplexPlanPersistenceService::class.java)
|
||||
val surveyPlacementService = SurveyPlacementService()
|
||||
|
||||
val service = ComplexPlanCalculationService(
|
||||
coverageBatchClient = coverageBatchClient,
|
||||
coverageCalculationClientResolver = coverageResolver(coverageBatchClient),
|
||||
earthGridService = earthGridService,
|
||||
satelliteService = satelliteService,
|
||||
complexPlanQueryService = complexPlanQueryService,
|
||||
complexPlanPersistenceService = complexPlanPersistenceService,
|
||||
complexPlanProperties = props(),
|
||||
surveyPlacementService = surveyPlacementService
|
||||
)
|
||||
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0)
|
||||
val intervalEnd = LocalDateTime.of(2026, 3, 27, 11, 0)
|
||||
val requestContour = "POLYGON ((0 0, 2 0, 2 1, 0 1, 0 0))"
|
||||
val coveredContour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"
|
||||
val cell = EarthCellWithRequestsDTO(
|
||||
id = 100,
|
||||
num = 42,
|
||||
contour = requestContour,
|
||||
requests = listOf(
|
||||
CellRequestDOT(
|
||||
requestId = "request-1",
|
||||
contour = requestContour
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
`when`(complexPlanQueryService.loadConstraintModes(listOf(22L), intervalStart, intervalEnd))
|
||||
.thenReturn(emptyMap())
|
||||
`when`(earthGridService.cells(isNull(), isNull())).thenReturn(listOf(cell))
|
||||
`when`(coverageBatchClient.plannedModes(listOf(22L), intervalStart, intervalEnd)).thenReturn(emptyMap())
|
||||
doReturn(
|
||||
mapOf(
|
||||
100L to listOf(
|
||||
SlotDTO(
|
||||
cycle = 0,
|
||||
satelliteId = 22,
|
||||
tn = intervalStart.plusMinutes(10),
|
||||
tk = intervalStart.plusMinutes(20),
|
||||
roll = 10.0,
|
||||
contour = coveredContour,
|
||||
revolution = 501,
|
||||
latitude = 10.0,
|
||||
longitude = 20.0
|
||||
)
|
||||
)
|
||||
)
|
||||
).`when`(coverageBatchClient).coverageModes(
|
||||
anyListValue(),
|
||||
anyLocalDateTime(),
|
||||
anyLocalDateTime(),
|
||||
anyListValue(),
|
||||
anyOccupiedIntervals(),
|
||||
isNull(),
|
||||
isNull()
|
||||
)
|
||||
doReturn(SatelliteModeSaveResult(calculatedModesCount = 1, complanModesCount = 1, affectedSatellitesCount = 1))
|
||||
.`when`(complexPlanPersistenceService).replaceModesForInterval(
|
||||
eqValue(listOf(22L)),
|
||||
eqValue(intervalStart),
|
||||
eqValue(intervalEnd),
|
||||
anySurveyMap(),
|
||||
isNull()
|
||||
)
|
||||
|
||||
val result = service.process(intervalStart, intervalEnd, listOf(22L))
|
||||
|
||||
assertEquals(2.0, result.totalRequestsArea, 0.0001)
|
||||
assertEquals(1.0, result.coveredArea, 0.0001)
|
||||
assertEquals(50.0, result.coveredAreaPercent, 0.0001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process calculates total requests area from all request cell fragments`() {
|
||||
val coverageBatchClient = coverageBatchClient()
|
||||
val earthGridService = mock(EarthGridService::class.java)
|
||||
val satelliteService = SatelliteService(testSatelliteCatalogClient())
|
||||
val complexPlanQueryService = mock(ComplexPlanQueryService::class.java)
|
||||
val complexPlanPersistenceService = mock(ComplexPlanPersistenceService::class.java)
|
||||
val surveyPlacementService = SurveyPlacementService()
|
||||
|
||||
val service = ComplexPlanCalculationService(
|
||||
coverageBatchClient = coverageBatchClient,
|
||||
coverageCalculationClientResolver = coverageResolver(coverageBatchClient),
|
||||
earthGridService = earthGridService,
|
||||
satelliteService = satelliteService,
|
||||
complexPlanQueryService = complexPlanQueryService,
|
||||
complexPlanPersistenceService = complexPlanPersistenceService,
|
||||
complexPlanProperties = props(),
|
||||
surveyPlacementService = surveyPlacementService
|
||||
)
|
||||
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0)
|
||||
val intervalEnd = LocalDateTime.of(2026, 3, 27, 11, 0)
|
||||
val firstRequestFragment = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"
|
||||
val secondRequestFragment = "POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0))"
|
||||
val cells = listOf(
|
||||
EarthCellWithRequestsDTO(
|
||||
id = 100,
|
||||
num = 42,
|
||||
contour = firstRequestFragment,
|
||||
requests = listOf(
|
||||
CellRequestDOT(
|
||||
id = 1001,
|
||||
requestId = "request-1",
|
||||
contour = firstRequestFragment
|
||||
)
|
||||
)
|
||||
),
|
||||
EarthCellWithRequestsDTO(
|
||||
id = 101,
|
||||
num = 43,
|
||||
contour = secondRequestFragment,
|
||||
requests = listOf(
|
||||
CellRequestDOT(
|
||||
id = 1002,
|
||||
requestId = "request-1",
|
||||
contour = secondRequestFragment
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
`when`(complexPlanQueryService.loadConstraintModes(listOf(22L), intervalStart, intervalEnd))
|
||||
.thenReturn(emptyMap())
|
||||
`when`(earthGridService.cells(isNull(), isNull())).thenReturn(cells)
|
||||
`when`(coverageBatchClient.plannedModes(listOf(22L), intervalStart, intervalEnd)).thenReturn(emptyMap())
|
||||
doReturn(emptyMap<Long, List<SlotDTO>>())
|
||||
.`when`(coverageBatchClient).coverageModes(
|
||||
anyListValue(),
|
||||
anyLocalDateTime(),
|
||||
anyLocalDateTime(),
|
||||
anyListValue(),
|
||||
anyOccupiedIntervals(),
|
||||
isNull(),
|
||||
isNull()
|
||||
)
|
||||
doReturn(SatelliteModeSaveResult())
|
||||
.`when`(complexPlanPersistenceService).replaceModesForInterval(
|
||||
eqValue(listOf(22L)),
|
||||
eqValue(intervalStart),
|
||||
eqValue(intervalEnd),
|
||||
anySurveyMap(),
|
||||
isNull()
|
||||
)
|
||||
|
||||
val result = service.process(intervalStart, intervalEnd, listOf(22L))
|
||||
|
||||
assertEquals(2.0, result.totalRequestsArea, 0.0001)
|
||||
assertEquals(0.0, result.coveredArea, 0.0001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process requests coverage by chunks and stops before next chunk when resource is exhausted`() {
|
||||
val coverageBatchClient = coverageBatchClient()
|
||||
val earthGridService = mock(EarthGridService::class.java)
|
||||
val satelliteService = mock(SatelliteService::class.java)
|
||||
val complexPlanQueryService = mock(ComplexPlanQueryService::class.java)
|
||||
val complexPlanPersistenceService = mock(ComplexPlanPersistenceService::class.java)
|
||||
val surveyPlacementService = SurveyPlacementService()
|
||||
|
||||
val service = ComplexPlanCalculationService(
|
||||
coverageBatchClient = coverageBatchClient,
|
||||
coverageCalculationClientResolver = coverageResolver(coverageBatchClient),
|
||||
earthGridService = earthGridService,
|
||||
satelliteService = satelliteService,
|
||||
complexPlanQueryService = complexPlanQueryService,
|
||||
complexPlanPersistenceService = complexPlanPersistenceService,
|
||||
complexPlanProperties = props(chunkSize = 2),
|
||||
surveyPlacementService = surveyPlacementService
|
||||
)
|
||||
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0)
|
||||
val intervalEnd = LocalDateTime.of(2026, 3, 27, 10, 0)
|
||||
val cells = listOf(
|
||||
EarthCellWithRequestsDTO(id = 1, num = 1, importance = 10.0, contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", requests = emptyList()),
|
||||
EarthCellWithRequestsDTO(id = 2, num = 2, importance = 9.0, contour = "POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0))", requests = emptyList()),
|
||||
EarthCellWithRequestsDTO(id = 3, num = 3, importance = 8.0, contour = "POLYGON ((2 0, 3 0, 3 1, 2 1, 2 0))", requests = emptyList())
|
||||
)
|
||||
|
||||
`when`(satelliteService.satellites).thenReturn(
|
||||
listOf(
|
||||
SatelliteModel(
|
||||
satelliteId = 22L,
|
||||
name = "T",
|
||||
bls = BLS(durationMax = 900.0, dailyMaxDuration = 605.0, revolutionMaxDuration = 2_000.0)
|
||||
)
|
||||
)
|
||||
)
|
||||
`when`(complexPlanQueryService.loadConstraintModes(listOf(22L), intervalStart, intervalEnd))
|
||||
.thenReturn(emptyMap())
|
||||
`when`(earthGridService.cells(isNull(), isNull())).thenReturn(cells)
|
||||
`when`(coverageBatchClient.plannedModes(listOf(22L), intervalStart, intervalEnd)).thenReturn(emptyMap())
|
||||
doReturn(
|
||||
mapOf(
|
||||
1L to listOf(
|
||||
SlotDTO(
|
||||
cycle = 0,
|
||||
satelliteId = 22,
|
||||
tn = intervalStart.plusMinutes(5),
|
||||
tk = intervalStart.plusMinutes(15),
|
||||
roll = 8.0,
|
||||
contour = "POLYGON ((0 0, 2 0, 2 1, 0 1, 0 0))",
|
||||
revolution = 501,
|
||||
latitude = 10.0,
|
||||
longitude = 20.0
|
||||
)
|
||||
)
|
||||
)
|
||||
).`when`(coverageBatchClient).coverageModes(
|
||||
anyListValue(),
|
||||
anyLocalDateTime(),
|
||||
anyLocalDateTime(),
|
||||
anyListValue(),
|
||||
anyOccupiedIntervals(),
|
||||
isNull(),
|
||||
isNull()
|
||||
)
|
||||
doReturn(SatelliteModeSaveResult()).`when`(complexPlanPersistenceService).replaceModesForInterval(
|
||||
eqValue(listOf(22L)),
|
||||
eqValue(intervalStart),
|
||||
eqValue(intervalEnd),
|
||||
anySurveyMap(),
|
||||
isNull()
|
||||
)
|
||||
|
||||
service.process(intervalStart, intervalEnd, listOf(22L))
|
||||
|
||||
verify(coverageBatchClient, times(1))
|
||||
.coverageModes(
|
||||
anyListValue(),
|
||||
eqValue(intervalStart),
|
||||
eqValue(intervalEnd),
|
||||
eqValue(listOf(22L)),
|
||||
anyOccupiedIntervals(),
|
||||
isNull(),
|
||||
isNull()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process passes occupied intervals snapshot and refreshes it between chunks`() {
|
||||
val coverageBatchClient = coverageBatchClient()
|
||||
val earthGridService = mock(EarthGridService::class.java)
|
||||
val satelliteService = mock(SatelliteService::class.java)
|
||||
val complexPlanQueryService = mock(ComplexPlanQueryService::class.java)
|
||||
val complexPlanPersistenceService = mock(ComplexPlanPersistenceService::class.java)
|
||||
val surveyPlacementService = SurveyPlacementService()
|
||||
|
||||
val service = ComplexPlanCalculationService(
|
||||
coverageBatchClient = coverageBatchClient,
|
||||
coverageCalculationClientResolver = coverageResolver(coverageBatchClient),
|
||||
earthGridService = earthGridService,
|
||||
satelliteService = satelliteService,
|
||||
complexPlanQueryService = complexPlanQueryService,
|
||||
complexPlanPersistenceService = complexPlanPersistenceService,
|
||||
complexPlanProperties = props(chunkSize = 1),
|
||||
surveyPlacementService = surveyPlacementService
|
||||
)
|
||||
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0)
|
||||
val intervalEnd = LocalDateTime.of(2026, 3, 27, 10, 0)
|
||||
val satellite = SatelliteModel(
|
||||
satelliteId = 22L,
|
||||
name = "T",
|
||||
longMission = LongTermMission(),
|
||||
bls = BLS(durationMax = 900.0, dailyMaxDuration = 2_000.0, revolutionMaxDuration = 2_000.0)
|
||||
)
|
||||
val firstCell = EarthCellWithRequestsDTO(
|
||||
id = 1,
|
||||
num = 101,
|
||||
importance = 10.0,
|
||||
contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||
requests = emptyList()
|
||||
)
|
||||
val secondCell = EarthCellWithRequestsDTO(
|
||||
id = 2,
|
||||
num = 102,
|
||||
importance = 9.0,
|
||||
contour = "POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0))",
|
||||
requests = emptyList()
|
||||
)
|
||||
|
||||
`when`(satelliteService.satellites).thenReturn(listOf(satellite))
|
||||
`when`(satelliteService.hasResources(eqValue(intervalStart), eqValue(intervalEnd), eqValue(listOf(22L)), anyMutableLongList()))
|
||||
.thenAnswer(Answer {
|
||||
val available = it.arguments[3] as MutableList<Long>
|
||||
available.clear()
|
||||
available += 22L
|
||||
true
|
||||
})
|
||||
`when`(complexPlanQueryService.loadConstraintModes(listOf(22L), intervalStart, intervalEnd))
|
||||
.thenReturn(
|
||||
mapOf(
|
||||
22L to mutableListOf(
|
||||
SurveyMode(
|
||||
revolution = 500,
|
||||
time = intervalStart.minusMinutes(20),
|
||||
timeStop = intervalStart.minusMinutes(10),
|
||||
roll = 12.5,
|
||||
duration = 600.0,
|
||||
contourWKT = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||
source = SatelliteModeSource.SLOTS
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
`when`(earthGridService.cells(isNull(), isNull())).thenReturn(listOf(firstCell, secondCell))
|
||||
`when`(coverageBatchClient.plannedModes(listOf(22L), intervalStart, intervalEnd)).thenReturn(emptyMap())
|
||||
doReturn(
|
||||
mapOf(
|
||||
1L to listOf(
|
||||
SlotDTO(
|
||||
cycle = 0,
|
||||
satelliteId = 22,
|
||||
tn = intervalStart.plusMinutes(5),
|
||||
tk = intervalStart.plusMinutes(15),
|
||||
roll = 8.0,
|
||||
contour = "POLYGON ((2 0, 3 0, 3 1, 2 1, 2 0))",
|
||||
revolution = 501,
|
||||
latitude = 10.0,
|
||||
longitude = 20.0
|
||||
)
|
||||
)
|
||||
),
|
||||
emptyMap<Long, List<SlotDTO>>()
|
||||
).`when`(coverageBatchClient).coverageModes(
|
||||
anyListValue(),
|
||||
anyLocalDateTime(),
|
||||
anyLocalDateTime(),
|
||||
anyListValue(),
|
||||
anyOccupiedIntervals(),
|
||||
isNull(),
|
||||
isNull()
|
||||
)
|
||||
doReturn(SatelliteModeSaveResult()).`when`(complexPlanPersistenceService).replaceModesForInterval(
|
||||
eqValue(listOf(22L)),
|
||||
eqValue(intervalStart),
|
||||
eqValue(intervalEnd),
|
||||
anySurveyMap(),
|
||||
isNull()
|
||||
)
|
||||
|
||||
service.process(intervalStart, intervalEnd, listOf(22L))
|
||||
|
||||
val occupiedCaptor = ArgumentCaptor.forClass(List::class.java) as ArgumentCaptor<List<OccupiedIntervalDTO>>
|
||||
verify(coverageBatchClient, times(2)).coverageModes(
|
||||
anyListValue(),
|
||||
eqValue(intervalStart),
|
||||
eqValue(intervalEnd),
|
||||
eqValue(listOf(22L)),
|
||||
captureOccupiedIntervals(occupiedCaptor),
|
||||
isNull(),
|
||||
isNull()
|
||||
)
|
||||
|
||||
val firstSnapshot = occupiedCaptor.allValues[0]
|
||||
val secondSnapshot = occupiedCaptor.allValues[1]
|
||||
|
||||
assertEquals(1, firstSnapshot.size)
|
||||
assertEquals(2, secondSnapshot.size)
|
||||
assertTrue(secondSnapshot.any { it.roll == 8.0 && it.startTime == intervalStart.plusMinutes(5) && it.endTime == intervalStart.plusMinutes(15) })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process failure before replace does not touch persisted result`() {
|
||||
val coverageBatchClient = coverageBatchClient()
|
||||
val earthGridService = mock(EarthGridService::class.java)
|
||||
val satelliteService = mock(SatelliteService::class.java)
|
||||
val complexPlanQueryService = mock(ComplexPlanQueryService::class.java)
|
||||
val complexPlanPersistenceService = mock(ComplexPlanPersistenceService::class.java)
|
||||
val surveyPlacementService = SurveyPlacementService()
|
||||
|
||||
val service = ComplexPlanCalculationService(
|
||||
coverageBatchClient = coverageBatchClient,
|
||||
coverageCalculationClientResolver = coverageResolver(coverageBatchClient),
|
||||
earthGridService = earthGridService,
|
||||
satelliteService = satelliteService,
|
||||
complexPlanQueryService = complexPlanQueryService,
|
||||
complexPlanPersistenceService = complexPlanPersistenceService,
|
||||
complexPlanProperties = props(),
|
||||
surveyPlacementService = surveyPlacementService
|
||||
)
|
||||
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0)
|
||||
val intervalEnd = LocalDateTime.of(2026, 3, 27, 12, 0)
|
||||
|
||||
`when`(satelliteService.satellites).thenReturn(
|
||||
listOf(
|
||||
SatelliteModel(
|
||||
satelliteId = 22L,
|
||||
name = "T",
|
||||
bls = BLS(durationMax = 900.0, dailyMaxDuration = 2_000.0, revolutionMaxDuration = 2_000.0)
|
||||
)
|
||||
)
|
||||
)
|
||||
`when`(complexPlanQueryService.loadConstraintModes(listOf(22L), intervalStart, intervalEnd))
|
||||
.thenReturn(emptyMap())
|
||||
org.mockito.Mockito.doThrow(RuntimeException("cells boom"))
|
||||
.`when`(earthGridService).cells(isNull(), isNull())
|
||||
|
||||
kotlin.test.assertFailsWith<RuntimeException> {
|
||||
service.process(intervalStart, intervalEnd, listOf(22L))
|
||||
}
|
||||
|
||||
verify(complexPlanPersistenceService, never()).replaceModesForInterval(
|
||||
eqValue(listOf(22L)),
|
||||
eqValue(intervalStart),
|
||||
eqValue(intervalEnd),
|
||||
anySurveyMap(),
|
||||
isNull()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process passes sun filter to coverage batch client`() {
|
||||
val coverageBatchClient = coverageBatchClient()
|
||||
val earthGridService = mock(EarthGridService::class.java)
|
||||
val satelliteService = mock(SatelliteService::class.java)
|
||||
val complexPlanQueryService = mock(ComplexPlanQueryService::class.java)
|
||||
val complexPlanPersistenceService = mock(ComplexPlanPersistenceService::class.java)
|
||||
val surveyPlacementService = SurveyPlacementService()
|
||||
|
||||
val service = ComplexPlanCalculationService(
|
||||
coverageBatchClient = coverageBatchClient,
|
||||
coverageCalculationClientResolver = coverageResolver(coverageBatchClient),
|
||||
earthGridService = earthGridService,
|
||||
satelliteService = satelliteService,
|
||||
complexPlanQueryService = complexPlanQueryService,
|
||||
complexPlanPersistenceService = complexPlanPersistenceService,
|
||||
complexPlanProperties = props(),
|
||||
surveyPlacementService = surveyPlacementService
|
||||
)
|
||||
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0)
|
||||
val intervalEnd = LocalDateTime.of(2026, 3, 27, 12, 0)
|
||||
val requestedSun = 12.5
|
||||
val cell = EarthCellWithRequestsDTO(
|
||||
id = 100,
|
||||
num = 42,
|
||||
contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||
requests = emptyList()
|
||||
)
|
||||
var capturedSun: Double? = null
|
||||
|
||||
`when`(satelliteService.satellites).thenReturn(
|
||||
listOf(
|
||||
SatelliteModel(
|
||||
satelliteId = 22L,
|
||||
name = "T",
|
||||
bls = BLS(durationMax = 900.0, dailyMaxDuration = 2_000.0, revolutionMaxDuration = 2_000.0)
|
||||
)
|
||||
)
|
||||
)
|
||||
`when`(complexPlanQueryService.loadConstraintModes(listOf(22L), intervalStart, intervalEnd)).thenReturn(emptyMap())
|
||||
`when`(earthGridService.cells(eqValue(2), eqValue(3))).thenReturn(listOf(cell))
|
||||
`when`(coverageBatchClient.plannedModes(listOf(22L), intervalStart, intervalEnd)).thenReturn(emptyMap())
|
||||
org.mockito.Mockito.doAnswer { invocation ->
|
||||
capturedSun = invocation.arguments[5] as Double?
|
||||
emptyMap<Long, List<SlotDTO>>()
|
||||
}.`when`(coverageBatchClient).coverageModes(
|
||||
anyListValue(),
|
||||
anyLocalDateTime(),
|
||||
anyLocalDateTime(),
|
||||
anyListValue(),
|
||||
anyOccupiedIntervals(),
|
||||
org.mockito.ArgumentMatchers.nullable(Double::class.java),
|
||||
isNull()
|
||||
)
|
||||
doReturn(SatelliteModeSaveResult()).`when`(complexPlanPersistenceService).replaceModesForInterval(
|
||||
eqValue(listOf(22L)),
|
||||
eqValue(intervalStart),
|
||||
eqValue(intervalEnd),
|
||||
anySurveyMap(),
|
||||
isNull()
|
||||
)
|
||||
|
||||
service.process(
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
satelliteIds = listOf(22L),
|
||||
sun = requestedSun,
|
||||
countLat = 2,
|
||||
countLong = 3
|
||||
)
|
||||
|
||||
verify(earthGridService).cells(2, 3)
|
||||
verify(coverageBatchClient, org.mockito.Mockito.atLeastOnce()).coverageModes(
|
||||
anyListValue(),
|
||||
anyLocalDateTime(),
|
||||
anyLocalDateTime(),
|
||||
anyListValue(),
|
||||
anyOccupiedIntervals(),
|
||||
org.mockito.ArgumentMatchers.nullable(Double::class.java),
|
||||
isNull()
|
||||
)
|
||||
assertEquals(requestedSun, capturedSun)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `process uses coverage scheme client when requested`() {
|
||||
val coverageBatchClient = coverageBatchClient()
|
||||
val coverageSchemeClient = mock(CoverageCalculationClient::class.java)
|
||||
val earthGridService = mock(EarthGridService::class.java)
|
||||
val satelliteService = mock(SatelliteService::class.java)
|
||||
val complexPlanQueryService = mock(ComplexPlanQueryService::class.java)
|
||||
val complexPlanPersistenceService = mock(ComplexPlanPersistenceService::class.java)
|
||||
val surveyPlacementService = SurveyPlacementService()
|
||||
|
||||
`when`(coverageSchemeClient.source).thenReturn(CoverageCalculationSource.COVERAGE_SCHEME)
|
||||
|
||||
val service = ComplexPlanCalculationService(
|
||||
coverageBatchClient = coverageBatchClient,
|
||||
coverageCalculationClientResolver = coverageResolver(coverageBatchClient, coverageSchemeClient),
|
||||
earthGridService = earthGridService,
|
||||
satelliteService = satelliteService,
|
||||
complexPlanQueryService = complexPlanQueryService,
|
||||
complexPlanPersistenceService = complexPlanPersistenceService,
|
||||
complexPlanProperties = props(),
|
||||
surveyPlacementService = surveyPlacementService
|
||||
)
|
||||
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0)
|
||||
val intervalEnd = LocalDateTime.of(2026, 3, 27, 12, 0)
|
||||
val cell = EarthCellWithRequestsDTO(
|
||||
id = 100,
|
||||
num = 42,
|
||||
contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||
requests = emptyList()
|
||||
)
|
||||
|
||||
`when`(satelliteService.satellites).thenReturn(
|
||||
listOf(
|
||||
SatelliteModel(
|
||||
satelliteId = 22L,
|
||||
name = "T",
|
||||
bls = BLS(durationMax = 900.0, dailyMaxDuration = 2_000.0, revolutionMaxDuration = 2_000.0)
|
||||
)
|
||||
)
|
||||
)
|
||||
`when`(complexPlanQueryService.loadConstraintModes(listOf(22L), intervalStart, intervalEnd)).thenReturn(emptyMap())
|
||||
`when`(earthGridService.cells(isNull(), isNull())).thenReturn(listOf(cell))
|
||||
`when`(coverageBatchClient.plannedModes(listOf(22L), intervalStart, intervalEnd)).thenReturn(emptyMap())
|
||||
doReturn(emptyMap<Long, List<SlotDTO>>()).`when`(coverageSchemeClient).coverageModes(
|
||||
anyListValue(),
|
||||
anyLocalDateTime(),
|
||||
anyLocalDateTime(),
|
||||
anyListValue(),
|
||||
anyOccupiedIntervals(),
|
||||
isNull(),
|
||||
isNull()
|
||||
)
|
||||
doReturn(SatelliteModeSaveResult()).`when`(complexPlanPersistenceService).replaceModesForInterval(
|
||||
eqValue(listOf(22L)),
|
||||
eqValue(intervalStart),
|
||||
eqValue(intervalEnd),
|
||||
anySurveyMap(),
|
||||
isNull()
|
||||
)
|
||||
|
||||
service.process(
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
satelliteIds = listOf(22L),
|
||||
coverageSource = CoverageCalculationSource.COVERAGE_SCHEME
|
||||
)
|
||||
|
||||
verify(coverageBatchClient).plannedModes(listOf(22L), intervalStart, intervalEnd)
|
||||
verify(coverageSchemeClient).coverageModes(
|
||||
anyListValue(),
|
||||
eqValue(intervalStart),
|
||||
eqValue(intervalEnd),
|
||||
eqValue(listOf(22L)),
|
||||
anyOccupiedIntervals(),
|
||||
isNull(),
|
||||
isNull()
|
||||
)
|
||||
verify(coverageBatchClient, never()).coverageModes(
|
||||
anyListValue(),
|
||||
anyLocalDateTime(),
|
||||
anyLocalDateTime(),
|
||||
anyListValue(),
|
||||
anyOccupiedIntervals(),
|
||||
isNull(),
|
||||
isNull()
|
||||
)
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
|
||||
@SpringBootTest
|
||||
class ComplexPlanExecutionLockServiceTest {
|
||||
|
||||
@Autowired
|
||||
private lateinit var complexPlanExecutionLockService: ComplexPlanExecutionLockService
|
||||
|
||||
@Test
|
||||
fun `same satellite cannot acquire execution lock twice before release`() {
|
||||
val firstLock = complexPlanExecutionLockService.tryAcquire(listOf(22L))
|
||||
assertNotNull(firstLock)
|
||||
|
||||
try {
|
||||
val secondLock = complexPlanExecutionLockService.tryAcquire(listOf(22L))
|
||||
assertNull(secondLock)
|
||||
} finally {
|
||||
firstLock.close()
|
||||
}
|
||||
|
||||
val thirdLock = complexPlanExecutionLockService.tryAcquire(listOf(22L))
|
||||
assertNotNull(thirdLock)
|
||||
thirdLock.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `different satellites can acquire startup locks independently`() {
|
||||
val firstLock = complexPlanExecutionLockService.tryAcquire(listOf(22L))
|
||||
val secondLock = complexPlanExecutionLockService.tryAcquire(listOf(23L))
|
||||
|
||||
assertNotNull(firstLock)
|
||||
assertNotNull(secondLock)
|
||||
|
||||
firstLock.close()
|
||||
secondLock.close()
|
||||
}
|
||||
}
|
||||
+362
@@ -0,0 +1,362 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito.doReturn
|
||||
import org.mockito.Mockito.inOrder
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.verify
|
||||
import space.nstart.pcp.pcp_satellites_service.configuration.CustomValidationException
|
||||
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunCreateDTO
|
||||
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunStatisticsDTO
|
||||
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunResponseDTO
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunEntity
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunStatus
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.CoverageCalculationSource
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.ComplexPlanProcessRequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageStrategy
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class ComplexPlanRunOrchestrationServiceTest {
|
||||
|
||||
@Test
|
||||
fun `orchestration marks run failed when lock is unavailable`() {
|
||||
val persistenceService = mock(ComplexPlanRunPersistenceService::class.java)
|
||||
val queryService = mock(ComplexPlanRunQueryService::class.java)
|
||||
val calculationService = mock(ComplexPlanCalculationService::class.java)
|
||||
val satelliteService = mock(SatelliteService::class.java)
|
||||
val lockService = mock(ComplexPlanExecutionLockService::class.java)
|
||||
|
||||
val orchestrationService = ComplexPlanRunOrchestrationService(
|
||||
complexPlanRunPersistenceService = persistenceService,
|
||||
complexPlanRunQueryService = queryService,
|
||||
complexPlanCalculationService = calculationService,
|
||||
satelliteService = satelliteService,
|
||||
complexPlanExecutionLockService = lockService
|
||||
)
|
||||
|
||||
val request = ComplexPlanProcessRequestDTO(
|
||||
intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0),
|
||||
intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0),
|
||||
satelliteId = 22L
|
||||
)
|
||||
val createdRun = ComplexPlanRunEntity(
|
||||
id = 10L,
|
||||
status = ComplexPlanRunStatus.CREATED,
|
||||
intervalStart = request.intervalStart,
|
||||
intervalEnd = request.intervalEnd
|
||||
)
|
||||
|
||||
doReturn(createdRun)
|
||||
.`when`(persistenceService)
|
||||
.createRun(
|
||||
ComplexPlanRunCreateDTO(
|
||||
intervalStart = request.intervalStart,
|
||||
intervalEnd = request.intervalEnd,
|
||||
satelliteIds = listOf(22L),
|
||||
requestedBy = "tester",
|
||||
requestPayload = "{\"intervalStart\":\"2026-03-26T10:00:00\",\"intervalEnd\":\"2026-03-26T12:00:00\",\"satelliteId\":22,\"satelliteIds\":[],\"sun\":null,\"coverageSource\":null,\"countLat\":null,\"countLong\":null,\"coverageStrategy\":\"GREEDY\"}"
|
||||
)
|
||||
)
|
||||
doReturn(null).`when`(lockService).tryAcquire(listOf(22L))
|
||||
|
||||
assertFailsWith<CustomValidationException> {
|
||||
orchestrationService.process(request, requestedBy = "tester")
|
||||
}
|
||||
|
||||
verify(persistenceService).markFailed(
|
||||
10L,
|
||||
"Complex plan recalculation is already running for at least one requested satellite"
|
||||
)
|
||||
verify(calculationService, never()).process(
|
||||
10L,
|
||||
request.intervalStart,
|
||||
request.intervalEnd,
|
||||
listOf(22L),
|
||||
null,
|
||||
CoverageCalculationSource.SLOTS,
|
||||
null,
|
||||
null,
|
||||
SlotCoverageStrategy.GREEDY
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `orchestration marks run failed when overlapping RUNNING run exists`() {
|
||||
val persistenceService = mock(ComplexPlanRunPersistenceService::class.java)
|
||||
val queryService = mock(ComplexPlanRunQueryService::class.java)
|
||||
val calculationService = mock(ComplexPlanCalculationService::class.java)
|
||||
val satelliteService = mock(SatelliteService::class.java)
|
||||
val lockService = mock(ComplexPlanExecutionLockService::class.java)
|
||||
val lockHandle = ComplexPlanExecutionLockService.ExecutionLockHandle {}
|
||||
|
||||
val orchestrationService = ComplexPlanRunOrchestrationService(
|
||||
complexPlanRunPersistenceService = persistenceService,
|
||||
complexPlanRunQueryService = queryService,
|
||||
complexPlanCalculationService = calculationService,
|
||||
satelliteService = satelliteService,
|
||||
complexPlanExecutionLockService = lockService
|
||||
)
|
||||
|
||||
val request = ComplexPlanProcessRequestDTO(
|
||||
intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0),
|
||||
intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0),
|
||||
satelliteId = 22L
|
||||
)
|
||||
val createdRun = ComplexPlanRunEntity(
|
||||
id = 10L,
|
||||
status = ComplexPlanRunStatus.CREATED,
|
||||
intervalStart = request.intervalStart,
|
||||
intervalEnd = request.intervalEnd
|
||||
)
|
||||
|
||||
doReturn(createdRun)
|
||||
.`when`(persistenceService)
|
||||
.createRun(
|
||||
ComplexPlanRunCreateDTO(
|
||||
intervalStart = request.intervalStart,
|
||||
intervalEnd = request.intervalEnd,
|
||||
satelliteIds = listOf(22L),
|
||||
requestedBy = "tester",
|
||||
requestPayload = "{\"intervalStart\":\"2026-03-26T10:00:00\",\"intervalEnd\":\"2026-03-26T12:00:00\",\"satelliteId\":22,\"satelliteIds\":[],\"sun\":null,\"coverageSource\":null,\"countLat\":null,\"countLong\":null,\"coverageStrategy\":\"GREEDY\"}"
|
||||
)
|
||||
)
|
||||
doReturn(lockHandle).`when`(lockService).tryAcquire(listOf(22L))
|
||||
doReturn(listOf(99L))
|
||||
.`when`(persistenceService)
|
||||
.findOverlappingRunningRunIds(listOf(22L), request.intervalStart, request.intervalEnd, 10L)
|
||||
|
||||
assertFailsWith<CustomValidationException> {
|
||||
orchestrationService.process(request, requestedBy = "tester")
|
||||
}
|
||||
|
||||
verify(persistenceService).markFailed(
|
||||
10L,
|
||||
"Complex plan recalculation is already running for overlapping interval, runIds=[99]"
|
||||
)
|
||||
verify(calculationService, never()).process(
|
||||
10L,
|
||||
request.intervalStart,
|
||||
request.intervalEnd,
|
||||
listOf(22L),
|
||||
null,
|
||||
CoverageCalculationSource.SLOTS,
|
||||
null,
|
||||
null,
|
||||
SlotCoverageStrategy.GREEDY
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `orchestration marks run running before releasing startup lock`() {
|
||||
val persistenceService = mock(ComplexPlanRunPersistenceService::class.java)
|
||||
val queryService = mock(ComplexPlanRunQueryService::class.java)
|
||||
val calculationService = mock(ComplexPlanCalculationService::class.java)
|
||||
val satelliteService = mock(SatelliteService::class.java)
|
||||
val lockService = mock(ComplexPlanExecutionLockService::class.java)
|
||||
val lockHandle = mock(ComplexPlanExecutionLockService.ExecutionLockHandle::class.java)
|
||||
|
||||
val orchestrationService = ComplexPlanRunOrchestrationService(
|
||||
complexPlanRunPersistenceService = persistenceService,
|
||||
complexPlanRunQueryService = queryService,
|
||||
complexPlanCalculationService = calculationService,
|
||||
satelliteService = satelliteService,
|
||||
complexPlanExecutionLockService = lockService
|
||||
)
|
||||
|
||||
val request = ComplexPlanProcessRequestDTO(
|
||||
intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0),
|
||||
intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0),
|
||||
satelliteId = 22L
|
||||
)
|
||||
val createdRun = ComplexPlanRunEntity(
|
||||
id = 10L,
|
||||
status = ComplexPlanRunStatus.CREATED,
|
||||
intervalStart = request.intervalStart,
|
||||
intervalEnd = request.intervalEnd
|
||||
)
|
||||
val runningRun = ComplexPlanRunEntity(
|
||||
id = 10L,
|
||||
status = ComplexPlanRunStatus.RUNNING,
|
||||
intervalStart = request.intervalStart,
|
||||
intervalEnd = request.intervalEnd
|
||||
)
|
||||
val response = ComplexPlanRunResponseDTO(
|
||||
runId = 10L,
|
||||
status = ComplexPlanRunStatus.COMPLETED,
|
||||
intervalStart = request.intervalStart,
|
||||
intervalEnd = request.intervalEnd,
|
||||
satelliteIds = listOf(22L),
|
||||
requestedBy = "tester",
|
||||
createdAt = request.intervalStart.minusMinutes(1).atOffset(ZoneOffset.UTC),
|
||||
startedAt = request.intervalStart.atOffset(ZoneOffset.UTC),
|
||||
finishedAt = request.intervalStart.plusMinutes(1).atOffset(ZoneOffset.UTC),
|
||||
durationMs = 60_000,
|
||||
errorMessage = null,
|
||||
calculatedModesCount = 1,
|
||||
bookedModesCount = 0,
|
||||
complanModesCount = 1,
|
||||
mixedModesCount = 0,
|
||||
affectedSatellitesCount = 1,
|
||||
processedCellsCount = 1,
|
||||
bookedSlotLinksCount = 0,
|
||||
snapshotIds = listOf(501L),
|
||||
requestPayload = "{}"
|
||||
)
|
||||
|
||||
doReturn(createdRun)
|
||||
.`when`(persistenceService)
|
||||
.createRun(
|
||||
ComplexPlanRunCreateDTO(
|
||||
intervalStart = request.intervalStart,
|
||||
intervalEnd = request.intervalEnd,
|
||||
satelliteIds = listOf(22L),
|
||||
requestedBy = "tester",
|
||||
requestPayload = "{\"intervalStart\":\"2026-03-26T10:00:00\",\"intervalEnd\":\"2026-03-26T12:00:00\",\"satelliteId\":22,\"satelliteIds\":[],\"sun\":null,\"coverageSource\":null,\"countLat\":null,\"countLong\":null,\"coverageStrategy\":\"GREEDY\"}"
|
||||
)
|
||||
)
|
||||
doReturn(lockHandle).`when`(lockService).tryAcquire(listOf(22L))
|
||||
doReturn(emptyList<Long>())
|
||||
.`when`(persistenceService)
|
||||
.findOverlappingRunningRunIds(listOf(22L), request.intervalStart, request.intervalEnd, 10L)
|
||||
doReturn(runningRun).`when`(persistenceService).markRunning(10L)
|
||||
doReturn(ComplexPlanCalculationResult(calculatedModesCount = 1))
|
||||
.`when`(calculationService)
|
||||
.process(
|
||||
10L,
|
||||
request.intervalStart,
|
||||
request.intervalEnd,
|
||||
listOf(22L),
|
||||
null,
|
||||
CoverageCalculationSource.SLOTS,
|
||||
null,
|
||||
null,
|
||||
SlotCoverageStrategy.GREEDY
|
||||
)
|
||||
doReturn(createdRun)
|
||||
.`when`(persistenceService)
|
||||
.markCompleted(10L, ComplexPlanRunStatisticsDTO(calculatedModesCount = 1))
|
||||
doReturn(response).`when`(queryService).getRun(10L)
|
||||
|
||||
val result = orchestrationService.process(request, requestedBy = "tester")
|
||||
|
||||
val ordered = inOrder(persistenceService, lockHandle, calculationService)
|
||||
ordered.verify(persistenceService)
|
||||
.findOverlappingRunningRunIds(listOf(22L), request.intervalStart, request.intervalEnd, 10L)
|
||||
ordered.verify(persistenceService).markRunning(10L)
|
||||
ordered.verify(lockHandle).close()
|
||||
ordered.verify(calculationService).process(
|
||||
10L,
|
||||
request.intervalStart,
|
||||
request.intervalEnd,
|
||||
listOf(22L),
|
||||
null,
|
||||
CoverageCalculationSource.SLOTS,
|
||||
null,
|
||||
null,
|
||||
SlotCoverageStrategy.GREEDY
|
||||
)
|
||||
assertEquals(10L, result.runId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `orchestration passes requested coverage source to calculation`() {
|
||||
val persistenceService = mock(ComplexPlanRunPersistenceService::class.java)
|
||||
val queryService = mock(ComplexPlanRunQueryService::class.java)
|
||||
val calculationService = mock(ComplexPlanCalculationService::class.java)
|
||||
val satelliteService = mock(SatelliteService::class.java)
|
||||
val lockService = mock(ComplexPlanExecutionLockService::class.java)
|
||||
val lockHandle = mock(ComplexPlanExecutionLockService.ExecutionLockHandle::class.java)
|
||||
|
||||
val orchestrationService = ComplexPlanRunOrchestrationService(
|
||||
complexPlanRunPersistenceService = persistenceService,
|
||||
complexPlanRunQueryService = queryService,
|
||||
complexPlanCalculationService = calculationService,
|
||||
satelliteService = satelliteService,
|
||||
complexPlanExecutionLockService = lockService
|
||||
)
|
||||
|
||||
val request = ComplexPlanProcessRequestDTO(
|
||||
intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0),
|
||||
intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0),
|
||||
satelliteId = 22L,
|
||||
coverageSource = CoverageCalculationSource.COVERAGE_SCHEME,
|
||||
countLat = 2,
|
||||
countLong = 3,
|
||||
coverageStrategy = SlotCoverageStrategy.CONTINUOUS
|
||||
)
|
||||
val createdRun = ComplexPlanRunEntity(
|
||||
id = 10L,
|
||||
status = ComplexPlanRunStatus.CREATED,
|
||||
intervalStart = request.intervalStart,
|
||||
intervalEnd = request.intervalEnd
|
||||
)
|
||||
val response = ComplexPlanRunResponseDTO(
|
||||
runId = 10L,
|
||||
status = ComplexPlanRunStatus.COMPLETED,
|
||||
intervalStart = request.intervalStart,
|
||||
intervalEnd = request.intervalEnd,
|
||||
satelliteIds = listOf(22L),
|
||||
requestedBy = "tester",
|
||||
createdAt = request.intervalStart.minusMinutes(1).atOffset(ZoneOffset.UTC),
|
||||
startedAt = request.intervalStart.atOffset(ZoneOffset.UTC),
|
||||
finishedAt = request.intervalStart.plusMinutes(1).atOffset(ZoneOffset.UTC),
|
||||
durationMs = 60_000,
|
||||
errorMessage = null,
|
||||
calculatedModesCount = 0,
|
||||
bookedModesCount = 0,
|
||||
complanModesCount = 0,
|
||||
mixedModesCount = 0,
|
||||
affectedSatellitesCount = 0,
|
||||
processedCellsCount = 0,
|
||||
bookedSlotLinksCount = 0,
|
||||
snapshotIds = emptyList(),
|
||||
requestPayload = "{}"
|
||||
)
|
||||
|
||||
doReturn(createdRun).`when`(persistenceService).createRun(
|
||||
ComplexPlanRunCreateDTO(
|
||||
intervalStart = request.intervalStart,
|
||||
intervalEnd = request.intervalEnd,
|
||||
satelliteIds = listOf(22L),
|
||||
requestedBy = "tester",
|
||||
requestPayload = "{\"intervalStart\":\"2026-03-26T10:00:00\",\"intervalEnd\":\"2026-03-26T12:00:00\",\"satelliteId\":22,\"satelliteIds\":[],\"sun\":null,\"coverageSource\":\"COVERAGE_SCHEME\",\"countLat\":2,\"countLong\":3,\"coverageStrategy\":\"CONTINUOUS\"}"
|
||||
)
|
||||
)
|
||||
doReturn(lockHandle).`when`(lockService).tryAcquire(listOf(22L))
|
||||
doReturn(emptyList<Long>())
|
||||
.`when`(persistenceService)
|
||||
.findOverlappingRunningRunIds(listOf(22L), request.intervalStart, request.intervalEnd, 10L)
|
||||
doReturn(createdRun).`when`(persistenceService).markRunning(10L)
|
||||
doReturn(ComplexPlanCalculationResult()).`when`(calculationService).process(
|
||||
10L,
|
||||
request.intervalStart,
|
||||
request.intervalEnd,
|
||||
listOf(22L),
|
||||
null,
|
||||
CoverageCalculationSource.COVERAGE_SCHEME,
|
||||
2,
|
||||
3,
|
||||
SlotCoverageStrategy.CONTINUOUS
|
||||
)
|
||||
doReturn(createdRun).`when`(persistenceService).markCompleted(10L, ComplexPlanRunStatisticsDTO())
|
||||
doReturn(response).`when`(queryService).getRun(10L)
|
||||
|
||||
orchestrationService.process(request, requestedBy = "tester")
|
||||
|
||||
verify(calculationService).process(
|
||||
10L,
|
||||
request.intervalStart,
|
||||
request.intervalEnd,
|
||||
listOf(22L),
|
||||
null,
|
||||
CoverageCalculationSource.COVERAGE_SCHEME,
|
||||
2,
|
||||
3,
|
||||
SlotCoverageStrategy.CONTINUOUS
|
||||
)
|
||||
}
|
||||
}
|
||||
+334
@@ -0,0 +1,334 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunCreateDTO
|
||||
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunStatisticsDTO
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunStatus
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeSource
|
||||
import space.nstart.pcp.pcp_satellites_service.model.mode.SurveyMode
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.ComplexPlanRunRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeBookedSlotRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeCellRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeSnapshotRepository
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@SpringBootTest
|
||||
class ComplexPlanRunPersistenceServiceTest {
|
||||
|
||||
@Autowired
|
||||
private lateinit var complexPlanRunPersistenceService: ComplexPlanRunPersistenceService
|
||||
|
||||
@Autowired
|
||||
private lateinit var complexPlanRunRepository: ComplexPlanRunRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var satelliteModeRepository: SatelliteModeRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var satelliteModeBookedSlotRepository: SatelliteModeBookedSlotRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var satelliteModeCellRepository: SatelliteModeCellRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var satelliteModeSnapshotRepository: SatelliteModeSnapshotRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var complexPlanPersistenceService: ComplexPlanPersistenceService
|
||||
|
||||
@Autowired
|
||||
private lateinit var complexPlanRunQueryService: ComplexPlanRunQueryService
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
satelliteModeBookedSlotRepository.deleteAll()
|
||||
satelliteModeCellRepository.deleteAll()
|
||||
satelliteModeRepository.deleteAll()
|
||||
satelliteModeSnapshotRepository.deleteAll()
|
||||
complexPlanRunRepository.deleteAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create run stores CREATED status and then moves to RUNNING`() {
|
||||
val run = complexPlanRunPersistenceService.createRun(
|
||||
ComplexPlanRunCreateDTO(
|
||||
intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0),
|
||||
intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0),
|
||||
satelliteIds = listOf(22L, 23L),
|
||||
requestedBy = "tester",
|
||||
requestPayload = """{"k":"v"}"""
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(ComplexPlanRunStatus.CREATED, run.status)
|
||||
assertEquals(listOf(22L, 23L), run.satellites.map { it.satelliteId }.sorted())
|
||||
|
||||
val running = complexPlanRunPersistenceService.markRunning(run.id!!)
|
||||
assertEquals(ComplexPlanRunStatus.RUNNING, running.status)
|
||||
assertNotNull(running.startedAt)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `completed run stores duration and statistics`() {
|
||||
val run = complexPlanRunPersistenceService.createRun(
|
||||
ComplexPlanRunCreateDTO(
|
||||
intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0),
|
||||
intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0),
|
||||
satelliteIds = listOf(22L),
|
||||
requestedBy = "tester",
|
||||
requestPayload = "{}"
|
||||
)
|
||||
)
|
||||
complexPlanRunPersistenceService.markRunning(run.id!!)
|
||||
Thread.sleep(5)
|
||||
|
||||
val completed = complexPlanRunPersistenceService.markCompleted(
|
||||
runId = run.id!!,
|
||||
statistics = ComplexPlanRunStatisticsDTO(
|
||||
calculatedModesCount = 3,
|
||||
bookedModesCount = 1,
|
||||
complanModesCount = 1,
|
||||
mixedModesCount = 1,
|
||||
affectedSatellitesCount = 1,
|
||||
processedCellsCount = 2,
|
||||
bookedSlotLinksCount = 4,
|
||||
totalRequestsArea = 10.0,
|
||||
coveredArea = 4.0,
|
||||
coveredAreaPercent = 40.0
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(ComplexPlanRunStatus.COMPLETED, completed.status)
|
||||
assertNotNull(completed.finishedAt)
|
||||
assertNotNull(completed.durationMs)
|
||||
assertTrue(completed.durationMs!! >= 0)
|
||||
assertEquals(3, completed.calculatedModesCount)
|
||||
assertEquals(1, completed.bookedModesCount)
|
||||
assertEquals(1, completed.complanModesCount)
|
||||
assertEquals(1, completed.mixedModesCount)
|
||||
assertEquals(2, completed.processedCellsCount)
|
||||
assertEquals(4, completed.bookedSlotLinksCount)
|
||||
assertEquals(10.0, completed.totalRequestsArea)
|
||||
assertEquals(4.0, completed.coveredArea)
|
||||
assertEquals(40.0, completed.coveredAreaPercent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getRun returns linked snapshot ids and active snapshot ids`() {
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0)
|
||||
val intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0)
|
||||
val run = complexPlanRunPersistenceService.createRun(
|
||||
ComplexPlanRunCreateDTO(
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
satelliteIds = listOf(22L),
|
||||
requestedBy = "tester",
|
||||
requestPayload = "{}"
|
||||
)
|
||||
)
|
||||
|
||||
complexPlanPersistenceService.saveModesForInterval(
|
||||
satelliteIds = listOf(22L),
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
surveysBySatelliteId = mapOf(
|
||||
22L to listOf(
|
||||
SurveyMode(
|
||||
revolution = 123,
|
||||
time = intervalStart.plusMinutes(5),
|
||||
timeStop = intervalStart.plusMinutes(15),
|
||||
roll = 3.0,
|
||||
contourWKT = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||
source = SatelliteModeSource.COMPLAN
|
||||
)
|
||||
)
|
||||
),
|
||||
complexPlanRunId = run.id
|
||||
)
|
||||
|
||||
val response = complexPlanRunQueryService.getRun(run.id!!)
|
||||
|
||||
assertEquals(1, response.snapshotIds.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failed run stores error and keeps audit trail`() {
|
||||
val run = complexPlanRunPersistenceService.createRun(
|
||||
ComplexPlanRunCreateDTO(
|
||||
intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0),
|
||||
intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0),
|
||||
satelliteIds = listOf(22L),
|
||||
requestedBy = null,
|
||||
requestPayload = "{}"
|
||||
)
|
||||
)
|
||||
complexPlanRunPersistenceService.markRunning(run.id!!)
|
||||
|
||||
val failed = complexPlanRunPersistenceService.markFailed(run.id!!, "boom")
|
||||
|
||||
assertEquals(ComplexPlanRunStatus.FAILED, failed.status)
|
||||
assertEquals("boom", failed.errorMessage)
|
||||
assertNotNull(failed.finishedAt)
|
||||
assertNotNull(failed.durationMs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shutdown marks CREATED and RUNNING runs as FAILED and leaves completed untouched`() {
|
||||
val createdRun = complexPlanRunPersistenceService.createRun(
|
||||
ComplexPlanRunCreateDTO(
|
||||
intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0),
|
||||
intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0),
|
||||
satelliteIds = listOf(22L),
|
||||
requestedBy = "tester",
|
||||
requestPayload = "{}"
|
||||
)
|
||||
)
|
||||
val runningRun = complexPlanRunPersistenceService.createRun(
|
||||
ComplexPlanRunCreateDTO(
|
||||
intervalStart = LocalDateTime.of(2026, 3, 26, 13, 0),
|
||||
intervalEnd = LocalDateTime.of(2026, 3, 26, 14, 0),
|
||||
satelliteIds = listOf(23L),
|
||||
requestedBy = "tester",
|
||||
requestPayload = "{}"
|
||||
)
|
||||
)
|
||||
complexPlanRunPersistenceService.markRunning(runningRun.id!!)
|
||||
val completedRun = complexPlanRunPersistenceService.createRun(
|
||||
ComplexPlanRunCreateDTO(
|
||||
intervalStart = LocalDateTime.of(2026, 3, 26, 15, 0),
|
||||
intervalEnd = LocalDateTime.of(2026, 3, 26, 16, 0),
|
||||
satelliteIds = listOf(24L),
|
||||
requestedBy = "tester",
|
||||
requestPayload = "{}"
|
||||
)
|
||||
)
|
||||
complexPlanRunPersistenceService.markRunning(completedRun.id!!)
|
||||
complexPlanRunPersistenceService.markCompleted(
|
||||
runId = completedRun.id!!,
|
||||
statistics = ComplexPlanRunStatisticsDTO(calculatedModesCount = 1)
|
||||
)
|
||||
|
||||
val affectedRuns = complexPlanRunPersistenceService.markActiveRunsFailedOnShutdown()
|
||||
|
||||
assertEquals(2, affectedRuns)
|
||||
|
||||
val reloadedCreated = complexPlanRunRepository.findById(createdRun.id!!).orElseThrow()
|
||||
val reloadedRunning = complexPlanRunRepository.findById(runningRun.id!!).orElseThrow()
|
||||
val reloadedCompleted = complexPlanRunRepository.findById(completedRun.id!!).orElseThrow()
|
||||
|
||||
assertEquals(ComplexPlanRunStatus.FAILED, reloadedCreated.status)
|
||||
assertEquals(ComplexPlanRunStatus.FAILED, reloadedRunning.status)
|
||||
assertEquals(ComplexPlanRunStatus.COMPLETED, reloadedCompleted.status)
|
||||
assertEquals(
|
||||
ComplexPlanRunPersistenceService.APPLICATION_SHUTDOWN_ERROR_MESSAGE,
|
||||
reloadedCreated.errorMessage
|
||||
)
|
||||
assertEquals(
|
||||
ComplexPlanRunPersistenceService.APPLICATION_SHUTDOWN_ERROR_MESSAGE,
|
||||
reloadedRunning.errorMessage
|
||||
)
|
||||
assertNotNull(reloadedCreated.finishedAt)
|
||||
assertNotNull(reloadedRunning.finishedAt)
|
||||
assertNotNull(reloadedRunning.durationMs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `findOverlappingRunningRunIds is interval aware`() {
|
||||
val runningRun = complexPlanRunPersistenceService.createRun(
|
||||
ComplexPlanRunCreateDTO(
|
||||
intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0),
|
||||
intervalEnd = LocalDateTime.of(2026, 3, 26, 11, 0),
|
||||
satelliteIds = listOf(10L),
|
||||
requestedBy = "tester",
|
||||
requestPayload = "{}"
|
||||
)
|
||||
)
|
||||
complexPlanRunPersistenceService.markRunning(runningRun.id!!)
|
||||
|
||||
val nonOverlapping = complexPlanRunPersistenceService.findOverlappingRunningRunIds(
|
||||
satelliteIds = listOf(10L),
|
||||
intervalStart = LocalDateTime.of(2026, 3, 26, 12, 0),
|
||||
intervalEnd = LocalDateTime.of(2026, 3, 26, 13, 0),
|
||||
excludedRunId = -1L
|
||||
)
|
||||
val overlapping = complexPlanRunPersistenceService.findOverlappingRunningRunIds(
|
||||
satelliteIds = listOf(10L),
|
||||
intervalStart = LocalDateTime.of(2026, 3, 26, 10, 30),
|
||||
intervalEnd = LocalDateTime.of(2026, 3, 26, 11, 30),
|
||||
excludedRunId = -1L
|
||||
)
|
||||
val otherSatellite = complexPlanRunPersistenceService.findOverlappingRunningRunIds(
|
||||
satelliteIds = listOf(11L),
|
||||
intervalStart = LocalDateTime.of(2026, 3, 26, 10, 30),
|
||||
intervalEnd = LocalDateTime.of(2026, 3, 26, 11, 30),
|
||||
excludedRunId = -1L
|
||||
)
|
||||
|
||||
assertTrue(nonOverlapping.isEmpty())
|
||||
assertEquals(listOf(runningRun.id!!), overlapping)
|
||||
assertTrue(otherSatellite.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `startup recovery marks stale RUNNING runs as FAILED`() {
|
||||
val run = complexPlanRunPersistenceService.createRun(
|
||||
ComplexPlanRunCreateDTO(
|
||||
intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0),
|
||||
intervalEnd = LocalDateTime.of(2026, 3, 26, 11, 0),
|
||||
satelliteIds = listOf(10L),
|
||||
requestedBy = "tester",
|
||||
requestPayload = "{}"
|
||||
)
|
||||
)
|
||||
complexPlanRunPersistenceService.markRunning(run.id!!)
|
||||
|
||||
val affectedRuns = complexPlanRunPersistenceService.markRunningRunsFailedOnStartup()
|
||||
|
||||
val reloaded = complexPlanRunRepository.findById(run.id!!).orElseThrow()
|
||||
assertEquals(1, affectedRuns)
|
||||
assertEquals(ComplexPlanRunStatus.FAILED, reloaded.status)
|
||||
assertEquals(ComplexPlanRunPersistenceService.STARTUP_RECOVERY_ERROR_MESSAGE, reloaded.errorMessage)
|
||||
assertNotNull(reloaded.finishedAt)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `startup recovery is no-op when there are no RUNNING runs`() {
|
||||
val affectedRuns = complexPlanRunPersistenceService.markRunningRunsFailedOnStartup()
|
||||
assertEquals(0, affectedRuns)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `run keeps link to created snapshots`() {
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 27, 10, 0)
|
||||
val intervalEnd = LocalDateTime.of(2026, 3, 27, 12, 0)
|
||||
val run = complexPlanRunPersistenceService.createRun(
|
||||
ComplexPlanRunCreateDTO(
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
satelliteIds = listOf(22L),
|
||||
requestedBy = "tester",
|
||||
requestPayload = "{}"
|
||||
)
|
||||
)
|
||||
|
||||
complexPlanPersistenceService.replaceModesForInterval(
|
||||
satelliteIds = listOf(22L),
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
surveysBySatelliteId = emptyMap(),
|
||||
complexPlanRunId = run.id
|
||||
)
|
||||
|
||||
val reloaded = complexPlanRunPersistenceService.getRun(run.id!!)
|
||||
assertEquals(1, reloaded.snapshots.size)
|
||||
assertEquals(22L, reloaded.snapshots.single().satelliteId)
|
||||
assertTrue(reloaded.snapshots.single().isActive)
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.verify
|
||||
|
||||
class ComplexPlanRunShutdownHandlerTest {
|
||||
|
||||
@Test
|
||||
fun `context close handler marks active runs as failed`() {
|
||||
val persistenceService = mock(ComplexPlanRunPersistenceService::class.java)
|
||||
val handler = ComplexPlanRunShutdownHandler(persistenceService)
|
||||
|
||||
handler.onContextClosed()
|
||||
|
||||
verify(persistenceService).markActiveRunsFailedOnShutdown()
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito.doReturn
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.verify
|
||||
|
||||
class ComplexPlanRunStartupRecoveryTest {
|
||||
|
||||
@Test
|
||||
fun `startup recovery invokes persistence reconciliation`() {
|
||||
val persistenceService = mock(ComplexPlanRunPersistenceService::class.java)
|
||||
doReturn(2).`when`(persistenceService).markRunningRunsFailedOnStartup()
|
||||
|
||||
val recovery = ComplexPlanRunStartupRecovery(persistenceService)
|
||||
recovery.onApplicationReady()
|
||||
|
||||
verify(persistenceService).markRunningRunsFailedOnStartup()
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import com.sun.net.httpserver.HttpServer
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.Mockito.mock
|
||||
import org.springframework.beans.factory.ObjectProvider
|
||||
import org.springframework.test.util.ReflectionTestUtils
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageBatchResponseDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageStrategy
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageTargetDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
|
||||
import tools.jackson.databind.ObjectMapper
|
||||
import java.net.InetSocketAddress
|
||||
import java.time.LocalDateTime
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class CoverageBatchClientTest {
|
||||
|
||||
private var server: HttpServer? = null
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
server?.stop(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `coverageModes sends sun and coverage strategy in batch request body`() {
|
||||
var requestBody = ""
|
||||
server = HttpServer.create(InetSocketAddress(0), 0).apply {
|
||||
createContext("/api/slots/poly-cover/batch") { exchange ->
|
||||
requestBody = exchange.requestBody.bufferedReader().readText()
|
||||
val responseBody = "[]"
|
||||
exchange.responseHeaders.add("Content-Type", "application/json")
|
||||
exchange.sendResponseHeaders(200, responseBody.toByteArray().size.toLong())
|
||||
exchange.responseBody.use { it.write(responseBody.toByteArray()) }
|
||||
}
|
||||
start()
|
||||
}
|
||||
|
||||
val builderProvider = mock(ObjectProvider::class.java) as ObjectProvider<WebClient.Builder>
|
||||
`when`(builderProvider.ifAvailable).thenReturn(WebClient.builder())
|
||||
|
||||
val client = CoverageBatchClient(builderProvider)
|
||||
ReflectionTestUtils.setField(client, "url", "http://localhost:${server!!.address.port}")
|
||||
|
||||
client.coverageModes(
|
||||
targets = listOf(SlotCoverageTargetDTO(targetId = 101L, wkt = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))")),
|
||||
intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0),
|
||||
intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0),
|
||||
satelliteIds = listOf(22L),
|
||||
occupiedIntervals = emptyList(),
|
||||
sun = 12.5,
|
||||
coverageStrategy = SlotCoverageStrategy.CONTINUOUS
|
||||
)
|
||||
|
||||
assertTrue(requestBody.contains("\"sun\":12.5"))
|
||||
assertTrue(requestBody.contains("\"coverageStrategy\":\"CONTINUOUS\""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `coverageModes falls back to GREEDY when CONTINUOUS batch request returns server error`() {
|
||||
val requestBodies = mutableListOf<String>()
|
||||
val requestCounter = AtomicInteger()
|
||||
server = HttpServer.create(InetSocketAddress(0), 0).apply {
|
||||
createContext("/api/slots/poly-cover/batch") { exchange ->
|
||||
val requestBody = exchange.requestBody.bufferedReader().readText()
|
||||
requestBodies += requestBody
|
||||
val attempt = requestCounter.incrementAndGet()
|
||||
if (attempt == 1) {
|
||||
val responseBody = """{"error":"solver failed"}"""
|
||||
exchange.responseHeaders.add("Content-Type", "application/json")
|
||||
exchange.sendResponseHeaders(500, responseBody.toByteArray().size.toLong())
|
||||
exchange.responseBody.use { it.write(responseBody.toByteArray()) }
|
||||
} else {
|
||||
val responseBody = ObjectMapper().writeValueAsString(
|
||||
listOf(
|
||||
SlotCoverageBatchResponseDTO(
|
||||
targetId = 101L,
|
||||
slots = listOf(
|
||||
SlotDTO(
|
||||
cycle = 0,
|
||||
satelliteId = 22,
|
||||
tn = LocalDateTime.of(2026, 3, 26, 10, 0),
|
||||
tk = LocalDateTime.of(2026, 3, 26, 10, 10),
|
||||
roll = 10.0,
|
||||
contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||
revolution = 501,
|
||||
latitude = 10.0,
|
||||
longitude = 20.0
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
exchange.responseHeaders.add("Content-Type", "application/json")
|
||||
exchange.sendResponseHeaders(200, responseBody.toByteArray().size.toLong())
|
||||
exchange.responseBody.use { it.write(responseBody.toByteArray()) }
|
||||
}
|
||||
}
|
||||
start()
|
||||
}
|
||||
|
||||
val builderProvider = mock(ObjectProvider::class.java) as ObjectProvider<WebClient.Builder>
|
||||
`when`(builderProvider.ifAvailable).thenReturn(WebClient.builder())
|
||||
|
||||
val client = CoverageBatchClient(builderProvider)
|
||||
ReflectionTestUtils.setField(client, "url", "http://localhost:${server!!.address.port}")
|
||||
|
||||
val response = client.coverageModes(
|
||||
targets = listOf(SlotCoverageTargetDTO(targetId = 101L, wkt = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))")),
|
||||
intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0),
|
||||
intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0),
|
||||
satelliteIds = listOf(22L),
|
||||
occupiedIntervals = emptyList(),
|
||||
sun = null,
|
||||
coverageStrategy = SlotCoverageStrategy.CONTINUOUS
|
||||
)
|
||||
|
||||
assertEquals(2, requestBodies.size)
|
||||
assertTrue(requestBodies[0].contains("\"coverageStrategy\":\"CONTINUOUS\""))
|
||||
assertTrue(requestBodies[1].contains("\"coverageStrategy\":\"GREEDY\""))
|
||||
assertEquals(1, response.size)
|
||||
assertEquals(1, response.getValue(101L).size)
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import com.sun.net.httpserver.HttpServer
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.Mockito.mock
|
||||
import org.springframework.beans.factory.ObjectProvider
|
||||
import org.springframework.test.util.ReflectionTestUtils
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.CoverageCalculationSource
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageTargetDTO
|
||||
import java.net.InetSocketAddress
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class CoverageSchemeCalculationClientTest {
|
||||
|
||||
private var server: HttpServer? = null
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
server?.stop(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `coverageModes calls coverage scheme service and maps response to slot model`() {
|
||||
var requestBody = ""
|
||||
server = HttpServer.create(InetSocketAddress(0), 0).apply {
|
||||
createContext("/api/coverage-schemes/calculate") { exchange ->
|
||||
requestBody = exchange.requestBody.bufferedReader().readText()
|
||||
val responseBody = """
|
||||
{
|
||||
"targetPolygonWkt": "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||
"observations": [],
|
||||
"coverageScheme": [
|
||||
{
|
||||
"satelliteId": 22,
|
||||
"tn": "2026-03-26T10:05:00",
|
||||
"tk": "2026-03-26T10:15:00",
|
||||
"roll": 8.5,
|
||||
"contour": "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||
"revolution": 501,
|
||||
"revolutionSign": "ASC",
|
||||
"slotIds": [],
|
||||
"latitude": 10.0,
|
||||
"longitude": 20.0
|
||||
}
|
||||
]
|
||||
}
|
||||
""".trimIndent()
|
||||
exchange.responseHeaders.add("Content-Type", "application/json")
|
||||
exchange.sendResponseHeaders(200, responseBody.toByteArray().size.toLong())
|
||||
exchange.responseBody.use { it.write(responseBody.toByteArray()) }
|
||||
}
|
||||
start()
|
||||
}
|
||||
|
||||
val builderProvider = mock(ObjectProvider::class.java) as ObjectProvider<WebClient.Builder>
|
||||
`when`(builderProvider.ifAvailable).thenReturn(WebClient.builder())
|
||||
|
||||
val client = CoverageSchemeCalculationClient(builderProvider)
|
||||
ReflectionTestUtils.setField(client, "url", "http://localhost:${server!!.address.port}")
|
||||
|
||||
val result = client.coverageModes(
|
||||
targets = listOf(SlotCoverageTargetDTO(targetId = 101L, wkt = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))")),
|
||||
intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0),
|
||||
intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0),
|
||||
satelliteIds = listOf(22L),
|
||||
occupiedIntervals = emptyList(),
|
||||
sun = null
|
||||
)
|
||||
|
||||
val slot = result.getValue(101L).single()
|
||||
assertEquals(CoverageCalculationSource.COVERAGE_SCHEME, client.source)
|
||||
assertTrue(requestBody.contains("\"polygonWkt\":\"POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))\""))
|
||||
assertTrue(requestBody.contains("\"satelliteIds\":[22]"))
|
||||
assertEquals(22L, slot.satelliteId)
|
||||
assertEquals(8.5, slot.roll)
|
||||
assertEquals(501L, slot.revolution)
|
||||
assertEquals(10.0, slot.latitude)
|
||||
assertEquals(20.0, slot.longitude)
|
||||
}
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import com.sun.net.httpserver.HttpServer
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.Mockito.mock
|
||||
import org.springframework.beans.factory.ObjectProvider
|
||||
import org.springframework.test.util.ReflectionTestUtils
|
||||
import org.springframework.util.unit.DataSize
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import space.nstart.pcp.pcp_satellites_service.configuration.WebClientConfig
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.URI
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
|
||||
class EarthGridServiceTest {
|
||||
|
||||
private var server: HttpServer? = null
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
server?.stop(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cells calls v1 cells with requests and maps wrapper items`() {
|
||||
val requestedUris = mutableListOf<URI>()
|
||||
server = serverWithCellsResponse(requestedUris)
|
||||
|
||||
val cells = earthGridService().cells().toList()
|
||||
val cell = cells.single()
|
||||
val request = cell.requests.single()
|
||||
|
||||
assertEquals("/v1/cells/with-requests", requestedUris.single().path)
|
||||
assertEquals("minImportance=0.001", requestedUris.single().query)
|
||||
assertFalse(requestedUris.any { uri -> uri.path == "/api/earth-grid/with-requests" })
|
||||
assertEquals(1L, cell.id)
|
||||
assertEquals(1L, cell.num)
|
||||
assertEquals(55.5, cell.latitude)
|
||||
assertEquals(37.5, cell.longitude)
|
||||
assertEquals(10.0, cell.importance)
|
||||
assertEquals("POLYGON ((30 50, 31 50, 31 51, 30 51, 30 50))", cell.contour)
|
||||
assertEquals("00000000-0000-0000-0000-000000000001", request.requestId)
|
||||
assertEquals(42.5, request.covPercent)
|
||||
assertEquals(9.0, request.importance)
|
||||
assertEquals("POLYGON ((30.1 50.1, 30.2 50.1, 30.2 50.2, 30.1 50.2, 30.1 50.1))", request.contour)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cells forwards countLat and countLong to v1 cells with requests`() {
|
||||
val requestedUris = mutableListOf<URI>()
|
||||
server = serverWithCellsResponse(requestedUris)
|
||||
|
||||
earthGridService().cells(countLat = 12, countLong = 24).toList()
|
||||
|
||||
assertEquals("/v1/cells/with-requests", requestedUris.single().path)
|
||||
assertEquals("minImportance=0.001&countLat=12&countLong=24", requestedUris.single().query)
|
||||
assertFalse(requestedUris.any { uri -> uri.path == "/api/earth-grid/with-requests" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cells supports response larger than default webclient buffer`() {
|
||||
val itemCount = 4_000
|
||||
val contour = "POLYGON ((${(1..80).joinToString(",") { index -> "$index $index" }}))"
|
||||
val responseBody = buildString {
|
||||
append("""{"items":[""")
|
||||
repeat(itemCount) { index ->
|
||||
if (index > 0) {
|
||||
append(",")
|
||||
}
|
||||
append(
|
||||
"""
|
||||
{
|
||||
"cellNum": $index,
|
||||
"latitude": 55.5,
|
||||
"longitude": 37.5,
|
||||
"importance": 10.0,
|
||||
"contour": "$contour",
|
||||
"requests": []
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
append("]}")
|
||||
}
|
||||
val requestedUris = mutableListOf<URI>()
|
||||
server = serverWithCellsResponse(requestedUris, responseBody)
|
||||
|
||||
val cells = earthGridService {
|
||||
WebClientConfig(DataSize.ofMegabytes(20)).webClientBuilder()
|
||||
}.cells().toList()
|
||||
|
||||
assertEquals(itemCount, cells.size)
|
||||
assertEquals("/v1/cells/with-requests", requestedUris.single().path)
|
||||
assertEquals("minImportance=0.001", requestedUris.single().query)
|
||||
}
|
||||
|
||||
private fun serverWithCellsResponse(requestedUris: MutableList<URI>): HttpServer =
|
||||
serverWithCellsResponse(
|
||||
requestedUris,
|
||||
"""
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"cellNum": 1,
|
||||
"latitude": 55.5,
|
||||
"longitude": 37.5,
|
||||
"importance": 10.0,
|
||||
"contour": "POLYGON ((30 50, 31 50, 31 51, 30 51, 30 50))",
|
||||
"requests": [
|
||||
{
|
||||
"requestId": "00000000-0000-0000-0000-000000000001",
|
||||
"contour": "POLYGON ((30.1 50.1, 30.2 50.1, 30.2 50.2, 30.1 50.2, 30.1 50.1))",
|
||||
"coveragePercent": 42.5,
|
||||
"importance": 9.0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
private fun serverWithCellsResponse(
|
||||
requestedUris: MutableList<URI>,
|
||||
responseBody: String
|
||||
): HttpServer =
|
||||
HttpServer.create(InetSocketAddress(0), 0).apply {
|
||||
createContext("/v1/cells/with-requests") { exchange ->
|
||||
requestedUris += exchange.requestURI
|
||||
exchange.responseHeaders.add("Content-Type", "application/json")
|
||||
exchange.sendResponseHeaders(200, responseBody.toByteArray().size.toLong())
|
||||
exchange.responseBody.use { it.write(responseBody.toByteArray()) }
|
||||
}
|
||||
start()
|
||||
}
|
||||
|
||||
private fun earthGridService(
|
||||
builderFactory: () -> WebClient.Builder = { WebClient.builder() }
|
||||
): EarthGridService {
|
||||
val builderProvider = mock(ObjectProvider::class.java) as ObjectProvider<WebClient.Builder>
|
||||
`when`(builderProvider.ifAvailable).thenReturn(builderFactory())
|
||||
|
||||
val service = EarthGridService(builderProvider)
|
||||
ReflectionTestUtils.setField(service, "url", "http://localhost:${server!!.address.port}")
|
||||
return service
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package space.nstart.pcp.pcp_satellites_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_satellites_service.repository.ComplexPlanRunSatelliteRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeSnapshotRepository
|
||||
|
||||
class SatelliteDeletedServiceTest {
|
||||
|
||||
@Test
|
||||
fun `deleteSatelliteData removes complex mission satellite data`() {
|
||||
val snapshotRepository = mock(SatelliteModeSnapshotRepository::class.java)
|
||||
val modeRepository = mock(SatelliteModeRepository::class.java)
|
||||
val runSatelliteRepository = mock(ComplexPlanRunSatelliteRepository::class.java)
|
||||
val service = SatelliteDeletedService(snapshotRepository, modeRepository, runSatelliteRepository)
|
||||
|
||||
`when`(modeRepository.countBySatelliteId(56756L)).thenReturn(12L)
|
||||
`when`(snapshotRepository.countBySatelliteId(56756L)).thenReturn(3L)
|
||||
`when`(runSatelliteRepository.countBySatelliteId(56756L)).thenReturn(2L)
|
||||
`when`(snapshotRepository.deleteAllBySatelliteId(56756L)).thenReturn(3L)
|
||||
`when`(runSatelliteRepository.deleteAllBySatelliteId(56756L)).thenReturn(2L)
|
||||
|
||||
val summary = service.deleteSatelliteData(56756L)
|
||||
|
||||
assertEquals(56756L, summary.satelliteId)
|
||||
assertEquals(3L, summary.snapshotsDeleted)
|
||||
assertEquals(12L, summary.modesDeletedByCascade)
|
||||
assertEquals(2L, summary.runLinksDeleted)
|
||||
verify(snapshotRepository).deleteAllBySatelliteId(56756L)
|
||||
verify(runSatelliteRepository).deleteAllBySatelliteId(56756L)
|
||||
}
|
||||
}
|
||||
+480
@@ -0,0 +1,480 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean
|
||||
import org.mockito.Mockito.doThrow
|
||||
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunCreateDTO
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeSource
|
||||
import space.nstart.pcp.pcp_satellites_service.model.mode.SurveyMode
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.ComplexPlanRunRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeBookedSlotRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeCellRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeSnapshotRepository
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@SpringBootTest
|
||||
class SatelliteModePersistenceServiceTest {
|
||||
|
||||
@Autowired
|
||||
private lateinit var satelliteModePersistenceService: SatelliteModePersistenceService
|
||||
|
||||
@Autowired
|
||||
private lateinit var satelliteModeQueryService: SatelliteModeQueryService
|
||||
|
||||
@Autowired
|
||||
private lateinit var satelliteModeRepository: SatelliteModeRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var satelliteModeBookedSlotRepository: SatelliteModeBookedSlotRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var satelliteModeCellRepository: SatelliteModeCellRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var satelliteModeSnapshotRepository: SatelliteModeSnapshotRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var complexPlanRunPersistenceService: ComplexPlanRunPersistenceService
|
||||
|
||||
@Autowired
|
||||
private lateinit var complexPlanPersistenceService: ComplexPlanPersistenceService
|
||||
|
||||
@Autowired
|
||||
private lateinit var complexPlanRunRepository: ComplexPlanRunRepository
|
||||
|
||||
@MockitoSpyBean
|
||||
private lateinit var satelliteModeRepositorySpy: SatelliteModeRepository
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
satelliteModeBookedSlotRepository.deleteAll()
|
||||
satelliteModeCellRepository.deleteAll()
|
||||
satelliteModeRepository.deleteAll()
|
||||
satelliteModeSnapshotRepository.deleteAll()
|
||||
complexPlanRunRepository.deleteAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deleteModesForInterval removes only intersecting satellite modes and relations`() {
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0)
|
||||
val intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0)
|
||||
|
||||
satelliteModePersistenceService.saveModesForInterval(
|
||||
satelliteIds = listOf(22L, 56756L),
|
||||
intervalStart = intervalStart.minusHours(2),
|
||||
intervalEnd = intervalEnd.plusHours(2),
|
||||
surveysBySatelliteId = mapOf(
|
||||
22L to listOf(
|
||||
SurveyMode(
|
||||
revolution = 100,
|
||||
time = intervalStart.plusMinutes(10),
|
||||
timeStop = intervalStart.plusMinutes(20),
|
||||
roll = 5.0,
|
||||
contourWKT = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||
source = SatelliteModeSource.SLOTS,
|
||||
bookedSlotIds = listOf(1001L),
|
||||
cellNums = listOf(42L)
|
||||
)
|
||||
),
|
||||
56756L to listOf(
|
||||
SurveyMode(
|
||||
revolution = 200,
|
||||
time = intervalEnd.plusHours(1),
|
||||
timeStop = intervalEnd.plusHours(2),
|
||||
roll = 7.0,
|
||||
contourWKT = "POLYGON ((2 0, 3 0, 3 1, 2 1, 2 0))",
|
||||
source = SatelliteModeSource.COMPLAN,
|
||||
bookedSlotIds = listOf(2001L),
|
||||
cellNums = listOf(99L)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
satelliteModePersistenceService.deleteModesForInterval(listOf(22L), intervalStart, intervalEnd)
|
||||
|
||||
val remaining = satelliteModeRepository.findAll()
|
||||
assertEquals(1, remaining.size)
|
||||
assertEquals(56756L, remaining.single().satelliteId)
|
||||
assertEquals(1, satelliteModeSnapshotRepository.count())
|
||||
assertEquals(1, satelliteModeBookedSlotRepository.findAll().size)
|
||||
assertEquals(1, satelliteModeCellRepository.findAll().size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `saveModesForInterval persists source booked slots and cell numbers`() {
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0)
|
||||
val intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0)
|
||||
|
||||
satelliteModePersistenceService.saveModesForInterval(
|
||||
satelliteIds = listOf(22L),
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
surveysBySatelliteId = mapOf(
|
||||
22L to listOf(
|
||||
SurveyMode(
|
||||
revolution = 300,
|
||||
time = intervalStart.plusMinutes(5),
|
||||
timeStop = intervalStart.plusMinutes(25),
|
||||
roll = 8.0,
|
||||
contourWKT = "POLYGON ((0 0, 2 0, 2 1, 0 1, 0 0))",
|
||||
source = SatelliteModeSource.SLOTS,
|
||||
bookedSlotIds = listOf(101L, 102L),
|
||||
cellNums = listOf(42L, 43L)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val modes = satelliteModeQueryService.findModesByInterval(listOf(22L), intervalStart, intervalEnd)
|
||||
assertEquals(1, modes.size)
|
||||
assertEquals("SLOTS", modes.single().source)
|
||||
assertEquals(listOf(101L, 102L), modes.single().bookedSlotIds)
|
||||
assertEquals(listOf(42L, 43L), modes.single().cellNums)
|
||||
assertEquals(-1L, modes.single().cellNum)
|
||||
assertTrue(satelliteModeCellRepository.findAll().size == 2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `findModesByInterval reads active snapshot by satellite and filters returned modes by interval`() {
|
||||
val snapshotStart = LocalDateTime.of(2026, 3, 26, 8, 0)
|
||||
val snapshotEnd = LocalDateTime.of(2026, 3, 26, 14, 0)
|
||||
val requestedStart = LocalDateTime.of(2026, 3, 26, 10, 0)
|
||||
val requestedEnd = LocalDateTime.of(2026, 3, 26, 11, 0)
|
||||
|
||||
satelliteModePersistenceService.saveModesForInterval(
|
||||
satelliteIds = listOf(22L),
|
||||
intervalStart = snapshotStart,
|
||||
intervalEnd = snapshotEnd,
|
||||
surveysBySatelliteId = mapOf(
|
||||
22L to listOf(
|
||||
SurveyMode(
|
||||
revolution = 301,
|
||||
time = snapshotStart.plusMinutes(5),
|
||||
timeStop = snapshotStart.plusMinutes(25),
|
||||
roll = 7.0,
|
||||
contourWKT = "POLYGON ((0 0, 2 0, 2 1, 0 1, 0 0))",
|
||||
source = SatelliteModeSource.SLOTS
|
||||
),
|
||||
SurveyMode(
|
||||
revolution = 302,
|
||||
time = requestedStart.plusMinutes(10),
|
||||
timeStop = requestedStart.plusMinutes(20),
|
||||
roll = 8.0,
|
||||
contourWKT = "POLYGON ((0 0, 2 0, 2 1, 0 1, 0 0))",
|
||||
source = SatelliteModeSource.COMPLAN
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val modes = satelliteModeQueryService.findModesByInterval(listOf(22L), requestedStart, requestedEnd)
|
||||
|
||||
assertEquals(1, modes.size)
|
||||
assertEquals(302L, modes.single().revolution)
|
||||
assertEquals("COMPLAN", modes.single().source)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `saveModesForInterval stores complex plan run relation`() {
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0)
|
||||
val intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0)
|
||||
val run = complexPlanRunPersistenceService.createRun(
|
||||
ComplexPlanRunCreateDTO(
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
satelliteIds = listOf(22L),
|
||||
requestedBy = "tester",
|
||||
requestPayload = "{}"
|
||||
)
|
||||
)
|
||||
|
||||
complexPlanPersistenceService.saveModesForInterval(
|
||||
satelliteIds = listOf(22L),
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
surveysBySatelliteId = mapOf(
|
||||
22L to listOf(
|
||||
SurveyMode(
|
||||
revolution = 400,
|
||||
time = intervalStart.plusMinutes(5),
|
||||
timeStop = intervalStart.plusMinutes(15),
|
||||
roll = 8.0,
|
||||
contourWKT = "POLYGON ((0 0, 2 0, 2 1, 0 1, 0 0))",
|
||||
source = SatelliteModeSource.COMPLAN,
|
||||
cellNums = listOf(42L)
|
||||
)
|
||||
)
|
||||
),
|
||||
complexPlanRunId = run.id
|
||||
)
|
||||
|
||||
val linkedModes = satelliteModeRepository.findAllByComplexPlanRun_IdOrderByStartTimeAsc(run.id!!)
|
||||
assertEquals(1, linkedModes.size)
|
||||
assertEquals(run.id, linkedModes.single().complexPlanRun?.id)
|
||||
assertEquals(run.id, linkedModes.single().snapshot?.complexPlanRun?.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `replaceModesForInterval creates new active snapshot and keeps old history`() {
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0)
|
||||
val intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0)
|
||||
|
||||
satelliteModePersistenceService.saveModesForInterval(
|
||||
satelliteIds = listOf(22L),
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
surveysBySatelliteId = mapOf(
|
||||
22L to listOf(
|
||||
SurveyMode(
|
||||
revolution = 100,
|
||||
time = intervalStart.plusMinutes(1),
|
||||
timeStop = intervalStart.plusMinutes(10),
|
||||
roll = 5.0,
|
||||
source = SatelliteModeSource.SLOTS,
|
||||
bookedSlotIds = listOf(101L),
|
||||
cellNums = listOf(42L)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
complexPlanPersistenceService.replaceModesForInterval(
|
||||
satelliteIds = listOf(22L),
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
surveysBySatelliteId = mapOf(
|
||||
22L to listOf(
|
||||
SurveyMode(
|
||||
revolution = 200,
|
||||
time = intervalStart.plusMinutes(20),
|
||||
timeStop = intervalStart.plusMinutes(30),
|
||||
roll = 6.0,
|
||||
source = SatelliteModeSource.COMPLAN,
|
||||
bookedSlotIds = listOf(202L),
|
||||
cellNums = listOf(43L, 44L)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val modes = satelliteModeQueryService.findModesByInterval(listOf(22L), intervalStart, intervalEnd)
|
||||
assertEquals(1, modes.size)
|
||||
assertEquals("COMPLAN", modes.single().source)
|
||||
assertEquals(listOf(202L), modes.single().bookedSlotIds)
|
||||
assertEquals(listOf(43L, 44L), modes.single().cellNums)
|
||||
val snapshots = satelliteModeSnapshotRepository.findAll().sortedBy { it.id }
|
||||
assertEquals(2, snapshots.size)
|
||||
assertTrue(snapshots.last().isActive)
|
||||
assertTrue(!snapshots.first().isActive)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `saveModesForInterval keeps only one active snapshot per satellite across different intervals`() {
|
||||
val firstStart = LocalDateTime.of(2026, 3, 26, 10, 0)
|
||||
val firstEnd = LocalDateTime.of(2026, 3, 26, 12, 0)
|
||||
val secondStart = LocalDateTime.of(2026, 3, 27, 10, 0)
|
||||
val secondEnd = LocalDateTime.of(2026, 3, 27, 12, 0)
|
||||
|
||||
satelliteModePersistenceService.saveModesForInterval(
|
||||
satelliteIds = listOf(22L),
|
||||
intervalStart = firstStart,
|
||||
intervalEnd = firstEnd,
|
||||
surveysBySatelliteId = mapOf(
|
||||
22L to listOf(
|
||||
SurveyMode(
|
||||
revolution = 401,
|
||||
time = firstStart.plusMinutes(5),
|
||||
timeStop = firstStart.plusMinutes(15),
|
||||
roll = 4.0,
|
||||
source = SatelliteModeSource.SLOTS
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
satelliteModePersistenceService.saveModesForInterval(
|
||||
satelliteIds = listOf(22L),
|
||||
intervalStart = secondStart,
|
||||
intervalEnd = secondEnd,
|
||||
surveysBySatelliteId = mapOf(
|
||||
22L to listOf(
|
||||
SurveyMode(
|
||||
revolution = 402,
|
||||
time = secondStart.plusMinutes(5),
|
||||
timeStop = secondStart.plusMinutes(15),
|
||||
roll = 5.0,
|
||||
source = SatelliteModeSource.COMPLAN
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val snapshots = satelliteModeSnapshotRepository.findAll().filter { it.satelliteId == 22L }.sortedBy { it.createdAt }
|
||||
assertEquals(2, snapshots.size)
|
||||
assertEquals(1, snapshots.count { it.isActive })
|
||||
assertTrue(!snapshots.first().isActive)
|
||||
assertTrue(snapshots.last().isActive)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `replaceModesForInterval rolls back and keeps old result when save fails`() {
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0)
|
||||
val intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0)
|
||||
|
||||
satelliteModePersistenceService.saveModesForInterval(
|
||||
satelliteIds = listOf(22L),
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
surveysBySatelliteId = mapOf(
|
||||
22L to listOf(
|
||||
SurveyMode(
|
||||
revolution = 300,
|
||||
time = intervalStart.plusMinutes(1),
|
||||
timeStop = intervalStart.plusMinutes(10),
|
||||
roll = 5.0,
|
||||
source = SatelliteModeSource.SLOTS,
|
||||
bookedSlotIds = listOf(301L),
|
||||
cellNums = listOf(42L)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
kotlin.test.assertFailsWith<Exception> {
|
||||
complexPlanPersistenceService.replaceModesForInterval(
|
||||
satelliteIds = listOf(22L),
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
surveysBySatelliteId = mapOf(
|
||||
22L to listOf(
|
||||
SurveyMode(
|
||||
revolution = 301,
|
||||
time = intervalStart.plusMinutes(20),
|
||||
timeStop = intervalStart.plusMinutes(30),
|
||||
roll = 6.0,
|
||||
source = SatelliteModeSource.COMPLAN,
|
||||
bookedSlotIds = listOf(302L),
|
||||
cellNums = listOf(43L)
|
||||
)
|
||||
)
|
||||
),
|
||||
complexPlanRunId = Long.MAX_VALUE
|
||||
)
|
||||
}
|
||||
|
||||
val modes = satelliteModeQueryService.findModesByInterval(listOf(22L), intervalStart, intervalEnd)
|
||||
assertEquals(1, modes.size)
|
||||
assertEquals("SLOTS", modes.single().source)
|
||||
assertEquals(listOf(301L), modes.single().bookedSlotIds)
|
||||
assertEquals(listOf(42L), modes.single().cellNums)
|
||||
assertEquals(1, satelliteModeSnapshotRepository.count())
|
||||
assertTrue(satelliteModeSnapshotRepository.findAll().single().isActive)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `saveModesForInterval creates stage4 snapshot and keeps it active`() {
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 28, 10, 0)
|
||||
val intervalEnd = LocalDateTime.of(2026, 3, 28, 12, 0)
|
||||
|
||||
val result = satelliteModePersistenceService.saveModesForInterval(
|
||||
satelliteIds = listOf(22L),
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
surveysBySatelliteId = mapOf(
|
||||
22L to listOf(
|
||||
SurveyMode(
|
||||
revolution = 601,
|
||||
time = intervalStart.plusMinutes(10),
|
||||
timeStop = intervalStart.plusMinutes(20),
|
||||
roll = 5.0,
|
||||
source = SatelliteModeSource.COMPLAN,
|
||||
contourWKT = "POLYGON ((0 0, 2 0, 2 1, 0 1, 0 0))"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(1, result.snapshotIds.size)
|
||||
assertEquals(result.snapshotIds, result.activeSnapshotIds)
|
||||
val snapshot = satelliteModeSnapshotRepository.findById(result.snapshotIds.single()).orElseThrow()
|
||||
assertEquals(22L, snapshot.satelliteId)
|
||||
assertTrue(snapshot.isActive)
|
||||
assertEquals(snapshot.id, satelliteModeRepository.findAll().single().snapshot?.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `saveModesForInterval persists MANUAL source`() {
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0)
|
||||
val intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0)
|
||||
|
||||
satelliteModePersistenceService.saveModesForInterval(
|
||||
satelliteIds = listOf(22L),
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
surveysBySatelliteId = mapOf(
|
||||
22L to listOf(
|
||||
SurveyMode(
|
||||
revolution = 450,
|
||||
time = intervalStart.plusMinutes(10),
|
||||
timeStop = intervalStart.plusMinutes(20),
|
||||
roll = 6.0,
|
||||
source = SatelliteModeSource.MANUAL,
|
||||
contourWKT = "POLYGON ((0 0, 2 0, 2 1, 0 1, 0 0))",
|
||||
cellNums = listOf(42L)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val savedMode = satelliteModeRepository.findAll().single()
|
||||
assertEquals(SatelliteModeSource.MANUAL, savedMode.source)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `saveModesForInterval derives coordinates from contour for COMPLAN and MIXED`() {
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0)
|
||||
val intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0)
|
||||
|
||||
satelliteModePersistenceService.saveModesForInterval(
|
||||
satelliteIds = listOf(22L),
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
surveysBySatelliteId = mapOf(
|
||||
22L to listOf(
|
||||
SurveyMode(
|
||||
revolution = 500,
|
||||
time = intervalStart.plusMinutes(5),
|
||||
timeStop = intervalStart.plusMinutes(15),
|
||||
roll = 4.0,
|
||||
source = SatelliteModeSource.COMPLAN,
|
||||
contourWKT = "POLYGON ((0 0, 2 0, 2 1, 0 1, 0 0))",
|
||||
cellNums = listOf(42L)
|
||||
),
|
||||
SurveyMode(
|
||||
revolution = 501,
|
||||
time = intervalStart.plusMinutes(20),
|
||||
timeStop = intervalStart.plusMinutes(30),
|
||||
roll = 4.0,
|
||||
source = SatelliteModeSource.MIXED,
|
||||
contourWKT = "POLYGON ((10 10, 12 10, 12 11, 10 11, 10 10))",
|
||||
cellNums = listOf(43L)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val savedModes = satelliteModeRepository.findAll().sortedBy { it.startTime }
|
||||
assertEquals(2, savedModes.size)
|
||||
assertEquals(0.5, savedModes[0].lat)
|
||||
assertEquals(1.0, savedModes[0].longitude)
|
||||
assertEquals(10.5, savedModes[1].lat)
|
||||
assertEquals(11.0, savedModes[1].longitude)
|
||||
}
|
||||
}
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
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_satellites_service.entity.SatelliteModeEntity
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeSource
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeType
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeBookedSlotRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeCellRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeSnapshotRepository
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class SatelliteModeQueryServiceTest {
|
||||
|
||||
@Test
|
||||
fun `loadDayConstraintModes uses full day boundaries for single day interval`() {
|
||||
val satelliteModeRepository = mock(SatelliteModeRepository::class.java)
|
||||
val satelliteModeSnapshotRepository = mock(SatelliteModeSnapshotRepository::class.java)
|
||||
val bookedSlotRepository = mock(SatelliteModeBookedSlotRepository::class.java)
|
||||
val cellRepository = mock(SatelliteModeCellRepository::class.java)
|
||||
val service = SatelliteModeQueryService(
|
||||
satelliteModeRepository = satelliteModeRepository,
|
||||
satelliteModeSnapshotRepository = satelliteModeSnapshotRepository,
|
||||
satelliteModeBookedSlotRepository = bookedSlotRepository,
|
||||
satelliteModeCellRepository = cellRepository,
|
||||
satelliteModeMapper = SatelliteModeMapper(),
|
||||
satelliteModeSnapshotMapper = SatelliteModeSnapshotMapper()
|
||||
)
|
||||
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 15)
|
||||
val intervalEnd = LocalDateTime.of(2026, 3, 26, 11, 45)
|
||||
val dayMode = modeEntity(
|
||||
id = 1L,
|
||||
revolution = 501L,
|
||||
startTime = LocalDateTime.of(2026, 3, 26, 12, 0),
|
||||
endTime = LocalDateTime.of(2026, 3, 26, 12, 10)
|
||||
)
|
||||
val sameRevolutionOutsideDayWindow = modeEntity(
|
||||
id = 2L,
|
||||
revolution = 501L,
|
||||
startTime = LocalDateTime.of(2026, 3, 25, 23, 40),
|
||||
endTime = LocalDateTime.of(2026, 3, 25, 23, 50)
|
||||
)
|
||||
|
||||
doReturn(listOf(dayMode)).`when`(satelliteModeRepository)
|
||||
.findAllBySnapshot_SatelliteIdInAndSnapshot_IsActiveTrueAndEndTimeGreaterThanAndStartTimeLessThanOrderByStartTimeAsc(
|
||||
listOf(22L),
|
||||
LocalDateTime.of(2026, 3, 26, 0, 0),
|
||||
LocalDateTime.of(2026, 3, 27, 0, 0)
|
||||
)
|
||||
doReturn(emptyList<space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeBookedSlotEntity>())
|
||||
.`when`(bookedSlotRepository).findAllByMode_IdIn(org.mockito.ArgumentMatchers.anyList())
|
||||
doReturn(emptyList<space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeCellEntity>())
|
||||
.`when`(cellRepository).findAllByMode_IdIn(org.mockito.ArgumentMatchers.anyList())
|
||||
|
||||
val loaded = service.loadDayConstraintModes(listOf(22L), intervalStart, intervalEnd)
|
||||
|
||||
assertEquals(1, loaded.getValue(22L).size)
|
||||
verify(satelliteModeRepository)
|
||||
.findAllBySnapshot_SatelliteIdInAndSnapshot_IsActiveTrueAndEndTimeGreaterThanAndStartTimeLessThanOrderByStartTimeAsc(
|
||||
listOf(22L),
|
||||
LocalDateTime.of(2026, 3, 26, 0, 0),
|
||||
LocalDateTime.of(2026, 3, 27, 0, 0)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadDayConstraintModes covers all touched days when interval crosses midnight`() {
|
||||
val satelliteModeRepository = mock(SatelliteModeRepository::class.java)
|
||||
val satelliteModeSnapshotRepository = mock(SatelliteModeSnapshotRepository::class.java)
|
||||
val bookedSlotRepository = mock(SatelliteModeBookedSlotRepository::class.java)
|
||||
val cellRepository = mock(SatelliteModeCellRepository::class.java)
|
||||
val service = SatelliteModeQueryService(
|
||||
satelliteModeRepository = satelliteModeRepository,
|
||||
satelliteModeSnapshotRepository = satelliteModeSnapshotRepository,
|
||||
satelliteModeBookedSlotRepository = bookedSlotRepository,
|
||||
satelliteModeCellRepository = cellRepository,
|
||||
satelliteModeMapper = SatelliteModeMapper(),
|
||||
satelliteModeSnapshotMapper = SatelliteModeSnapshotMapper()
|
||||
)
|
||||
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 26, 23, 0)
|
||||
val intervalEnd = LocalDateTime.of(2026, 3, 27, 1, 0)
|
||||
|
||||
doReturn(emptyList<SatelliteModeEntity>()).`when`(satelliteModeRepository)
|
||||
.findAllBySnapshot_SatelliteIdInAndSnapshot_IsActiveTrueAndEndTimeGreaterThanAndStartTimeLessThanOrderByStartTimeAsc(
|
||||
listOf(22L),
|
||||
LocalDateTime.of(2026, 3, 26, 0, 0),
|
||||
LocalDateTime.of(2026, 3, 28, 0, 0)
|
||||
)
|
||||
|
||||
service.loadDayConstraintModes(listOf(22L), intervalStart, intervalEnd)
|
||||
|
||||
verify(satelliteModeRepository)
|
||||
.findAllBySnapshot_SatelliteIdInAndSnapshot_IsActiveTrueAndEndTimeGreaterThanAndStartTimeLessThanOrderByStartTimeAsc(
|
||||
listOf(22L),
|
||||
LocalDateTime.of(2026, 3, 26, 0, 0),
|
||||
LocalDateTime.of(2026, 3, 28, 0, 0)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadRevolutionConstraintModes loads same satellite revolution context outside day window`() {
|
||||
val satelliteModeRepository = mock(SatelliteModeRepository::class.java)
|
||||
val satelliteModeSnapshotRepository = mock(SatelliteModeSnapshotRepository::class.java)
|
||||
val bookedSlotRepository = mock(SatelliteModeBookedSlotRepository::class.java)
|
||||
val cellRepository = mock(SatelliteModeCellRepository::class.java)
|
||||
val service = SatelliteModeQueryService(
|
||||
satelliteModeRepository = satelliteModeRepository,
|
||||
satelliteModeSnapshotRepository = satelliteModeSnapshotRepository,
|
||||
satelliteModeBookedSlotRepository = bookedSlotRepository,
|
||||
satelliteModeCellRepository = cellRepository,
|
||||
satelliteModeMapper = SatelliteModeMapper(),
|
||||
satelliteModeSnapshotMapper = SatelliteModeSnapshotMapper()
|
||||
)
|
||||
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 15)
|
||||
val intervalEnd = LocalDateTime.of(2026, 3, 26, 11, 45)
|
||||
val dayMode = modeEntity(
|
||||
id = 1L,
|
||||
revolution = 501L,
|
||||
startTime = LocalDateTime.of(2026, 3, 26, 12, 0),
|
||||
endTime = LocalDateTime.of(2026, 3, 26, 12, 10)
|
||||
)
|
||||
val sameRevolutionOutsideDayWindow = modeEntity(
|
||||
id = 2L,
|
||||
revolution = 501L,
|
||||
startTime = LocalDateTime.of(2026, 3, 25, 23, 40),
|
||||
endTime = LocalDateTime.of(2026, 3, 25, 23, 50)
|
||||
)
|
||||
|
||||
doReturn(listOf(dayMode)).`when`(satelliteModeRepository)
|
||||
.findAllBySnapshot_SatelliteIdInAndSnapshot_IsActiveTrueAndEndTimeGreaterThanAndStartTimeLessThanOrderByStartTimeAsc(
|
||||
listOf(22L),
|
||||
LocalDateTime.of(2026, 3, 26, 0, 0),
|
||||
LocalDateTime.of(2026, 3, 27, 0, 0)
|
||||
)
|
||||
doReturn(listOf(dayMode, sameRevolutionOutsideDayWindow)).`when`(satelliteModeRepository)
|
||||
.findAllBySnapshot_SatelliteIdAndSnapshot_IsActiveTrueAndRevolutionInOrderByStartTimeAsc(22L, setOf(501L))
|
||||
doReturn(emptyList<space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeBookedSlotEntity>())
|
||||
.`when`(bookedSlotRepository).findAllByMode_IdIn(org.mockito.ArgumentMatchers.anyList())
|
||||
doReturn(emptyList<space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeCellEntity>())
|
||||
.`when`(cellRepository).findAllByMode_IdIn(org.mockito.ArgumentMatchers.anyList())
|
||||
|
||||
val loaded = service.loadRevolutionConstraintModes(listOf(22L), intervalStart, intervalEnd)
|
||||
|
||||
assertEquals(2, loaded.getValue(22L).size)
|
||||
assertTrue(loaded.getValue(22L).any { it.time == sameRevolutionOutsideDayWindow.startTime })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadConstraintModes returns merged deduplicated context`() {
|
||||
val satelliteModeRepository = mock(SatelliteModeRepository::class.java)
|
||||
val satelliteModeSnapshotRepository = mock(SatelliteModeSnapshotRepository::class.java)
|
||||
val bookedSlotRepository = mock(SatelliteModeBookedSlotRepository::class.java)
|
||||
val cellRepository = mock(SatelliteModeCellRepository::class.java)
|
||||
val service = SatelliteModeQueryService(
|
||||
satelliteModeRepository = satelliteModeRepository,
|
||||
satelliteModeSnapshotRepository = satelliteModeSnapshotRepository,
|
||||
satelliteModeBookedSlotRepository = bookedSlotRepository,
|
||||
satelliteModeCellRepository = cellRepository,
|
||||
satelliteModeMapper = SatelliteModeMapper(),
|
||||
satelliteModeSnapshotMapper = SatelliteModeSnapshotMapper()
|
||||
)
|
||||
|
||||
val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 15)
|
||||
val intervalEnd = LocalDateTime.of(2026, 3, 26, 11, 45)
|
||||
val dayMode = modeEntity(
|
||||
id = 1L,
|
||||
revolution = 501L,
|
||||
startTime = LocalDateTime.of(2026, 3, 26, 12, 0),
|
||||
endTime = LocalDateTime.of(2026, 3, 26, 12, 10)
|
||||
)
|
||||
val sameRevolutionOutsideDayWindow = modeEntity(
|
||||
id = 2L,
|
||||
revolution = 501L,
|
||||
startTime = LocalDateTime.of(2026, 3, 25, 23, 40),
|
||||
endTime = LocalDateTime.of(2026, 3, 25, 23, 50)
|
||||
)
|
||||
|
||||
doReturn(listOf(dayMode)).`when`(satelliteModeRepository)
|
||||
.findAllBySnapshot_SatelliteIdInAndSnapshot_IsActiveTrueAndEndTimeGreaterThanAndStartTimeLessThanOrderByStartTimeAsc(
|
||||
listOf(22L),
|
||||
LocalDateTime.of(2026, 3, 26, 0, 0),
|
||||
LocalDateTime.of(2026, 3, 27, 0, 0)
|
||||
)
|
||||
doReturn(listOf(dayMode, sameRevolutionOutsideDayWindow)).`when`(satelliteModeRepository)
|
||||
.findAllBySnapshot_SatelliteIdAndSnapshot_IsActiveTrueAndRevolutionInOrderByStartTimeAsc(22L, setOf(501L))
|
||||
doReturn(emptyList<space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeBookedSlotEntity>())
|
||||
.`when`(bookedSlotRepository).findAllByMode_IdIn(org.mockito.ArgumentMatchers.anyList())
|
||||
doReturn(emptyList<space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeCellEntity>())
|
||||
.`when`(cellRepository).findAllByMode_IdIn(org.mockito.ArgumentMatchers.anyList())
|
||||
|
||||
val loaded = service.loadConstraintModes(listOf(22L), intervalStart, intervalEnd)
|
||||
|
||||
assertEquals(2, loaded.getValue(22L).size)
|
||||
assertEquals(
|
||||
listOf(sameRevolutionOutsideDayWindow.startTime, dayMode.startTime),
|
||||
loaded.getValue(22L).map { it.time }
|
||||
)
|
||||
}
|
||||
|
||||
private fun modeEntity(
|
||||
id: Long,
|
||||
revolution: Long,
|
||||
startTime: LocalDateTime,
|
||||
endTime: LocalDateTime
|
||||
) = SatelliteModeEntity(
|
||||
id = id,
|
||||
satelliteId = 22L,
|
||||
source = SatelliteModeSource.COMPLAN,
|
||||
type = SatelliteModeType.SURVEY,
|
||||
startTime = startTime,
|
||||
endTime = endTime,
|
||||
revolution = revolution,
|
||||
duration = java.time.Duration.between(startTime, endTime).seconds.toDouble(),
|
||||
roll = 3.0
|
||||
)
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import space.nstart.pcp.pcp_satellites_service.model.mode.SurveyMode
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.DailyDurationDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.RevolutionDurationDTO
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class SatelliteServiceTest {
|
||||
|
||||
@Test
|
||||
fun `mission statistics groups durations by revolution`() {
|
||||
val service = SatelliteService(testSatelliteCatalogClient())
|
||||
val satellite = service.satellites.first { it.satelliteId == 22L }
|
||||
satellite.longMission.surveys.clear()
|
||||
|
||||
satellite.longMission.surveys.add(
|
||||
SurveyMode(
|
||||
revolution = 101,
|
||||
time = LocalDateTime.of(2026, 3, 26, 10, 0),
|
||||
timeStop = LocalDateTime.of(2026, 3, 26, 10, 5),
|
||||
duration = 300.0
|
||||
)
|
||||
)
|
||||
satellite.longMission.surveys.add(
|
||||
SurveyMode(
|
||||
revolution = 101,
|
||||
time = LocalDateTime.of(2026, 3, 26, 10, 10),
|
||||
timeStop = LocalDateTime.of(2026, 3, 26, 10, 12),
|
||||
duration = 120.0
|
||||
)
|
||||
)
|
||||
satellite.longMission.surveys.add(
|
||||
SurveyMode(
|
||||
revolution = 102,
|
||||
time = LocalDateTime.of(2026, 3, 26, 11, 0),
|
||||
timeStop = LocalDateTime.of(2026, 3, 26, 11, 3),
|
||||
duration = 180.0,
|
||||
contourWKT = "POLYGON ((0 0, 2 0, 2 1, 0 1, 0 0))"
|
||||
)
|
||||
)
|
||||
satellite.longMission.surveys.add(
|
||||
SurveyMode(
|
||||
revolution = 103,
|
||||
time = LocalDateTime.of(2026, 3, 26, 23, 59),
|
||||
timeStop = LocalDateTime.of(2026, 3, 27, 0, 1),
|
||||
duration = 120.0,
|
||||
contourWKT = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"
|
||||
)
|
||||
)
|
||||
|
||||
val stats = service.missionStatistics(22L)
|
||||
|
||||
assertEquals(4, stats.modesCount)
|
||||
assertEquals(3.0, stats.totalContourArea)
|
||||
assertContentEquals(
|
||||
listOf(
|
||||
RevolutionDurationDTO(revolution = 101, totalDuration = 420.0),
|
||||
RevolutionDurationDTO(revolution = 102, totalDuration = 180.0),
|
||||
RevolutionDurationDTO(revolution = 103, totalDuration = 120.0)
|
||||
),
|
||||
stats.revolutionDurations
|
||||
)
|
||||
assertContentEquals(
|
||||
listOf(
|
||||
DailyDurationDTO(date = LocalDate.of(2026, 3, 26), totalDuration = 660.0),
|
||||
DailyDurationDTO(date = LocalDate.of(2026, 3, 27), totalDuration = 60.0)
|
||||
),
|
||||
stats.dailyDurations
|
||||
)
|
||||
}
|
||||
}
|
||||
+400
@@ -0,0 +1,400 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import ch.qos.logback.classic.Logger
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent
|
||||
import ch.qos.logback.core.read.ListAppender
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.slf4j.LoggerFactory
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeSource
|
||||
import space.nstart.pcp.pcp_satellites_service.model.BLS
|
||||
import space.nstart.pcp.pcp_satellites_service.model.SatelliteModel
|
||||
import space.nstart.pcp.pcp_satellites_service.model.mode.SurveyMode
|
||||
import java.time.Duration
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class SurveyPlacementServiceTest {
|
||||
|
||||
private val service = SurveyPlacementService()
|
||||
|
||||
@Test
|
||||
fun `candidate fits without trimming`() {
|
||||
val surveys = mutableListOf<SurveyMode>()
|
||||
val satellite = satellite()
|
||||
|
||||
val decision = service.tryPlaceSurvey(surveys, survey(start = 0, end = 40), satellite)
|
||||
|
||||
assertTrue(decision.accepted)
|
||||
assertFalse(decision.trimmed)
|
||||
assertEquals(1, surveys.size)
|
||||
assertEquals(40.0, surveys.single().duration)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `candidate exceeds daily max duration and is trimmed then accepted`() {
|
||||
val surveys = mutableListOf(survey(start = 600, end = 670, revolution = 2, roll = 20.0))
|
||||
val satellite = satellite(dailyMaxDuration = 100.0)
|
||||
|
||||
val decision = service.tryPlaceSurvey(surveys, survey(start = 0, end = 50), satellite)
|
||||
|
||||
assertTrue(decision.accepted)
|
||||
assertTrue(decision.trimmed)
|
||||
assertEquals(2, surveys.size)
|
||||
assertEquals(30.0, decision.adjustedCandidate?.duration)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `candidate exceeds daily max duration and is rejected below duration min`() {
|
||||
val surveys = mutableListOf(survey(start = 600, end = 695, revolution = 2, roll = 20.0))
|
||||
val satellite = satellite(dailyMaxDuration = 100.0)
|
||||
|
||||
val decision = service.tryPlaceSurvey(surveys, survey(start = 0, end = 20), satellite)
|
||||
|
||||
assertFalse(decision.accepted)
|
||||
assertEquals("daily_max_duration", decision.rejectionReason)
|
||||
assertEquals(1, surveys.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `candidate exceeds revolution max duration and is trimmed then accepted`() {
|
||||
val surveys = mutableListOf(survey(start = 600, end = 670, revolution = 5, roll = 20.0))
|
||||
val satellite = satellite(revolutionMaxDuration = 100.0)
|
||||
|
||||
val decision = service.tryPlaceSurvey(
|
||||
surveys,
|
||||
survey(start = 0, end = 50, revolution = 5),
|
||||
satellite
|
||||
)
|
||||
|
||||
assertTrue(decision.accepted)
|
||||
assertTrue(decision.trimmed)
|
||||
assertEquals(30.0, decision.adjustedCandidate?.duration)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `candidate exceeds merged chain duration max and is trimmed then accepted`() {
|
||||
val surveys = mutableListOf(survey(start = 0, end = 80, roll = 10.0))
|
||||
val satellite = satellite(durationMax = 100.0, mmi = 10.0)
|
||||
|
||||
val decision = service.tryPlaceSurvey(
|
||||
surveys,
|
||||
survey(start = 50, end = 140, roll = 10.0),
|
||||
satellite
|
||||
)
|
||||
|
||||
assertTrue(decision.accepted)
|
||||
assertTrue(decision.trimmed)
|
||||
assertEquals(1, surveys.size)
|
||||
assertEquals(0, secondsFromBase(surveys.single().time))
|
||||
assertEquals(100, secondsFromBase(surveys.single().timeStop))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `merge allowed when time overlaps and geometry intersects`() {
|
||||
val surveys = mutableListOf(
|
||||
survey(
|
||||
start = 0,
|
||||
end = 40,
|
||||
roll = 10.0,
|
||||
contour = "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))"
|
||||
)
|
||||
)
|
||||
val satellite = satellite()
|
||||
|
||||
val decision = service.tryPlaceSurvey(
|
||||
surveys,
|
||||
survey(
|
||||
start = 20,
|
||||
end = 70,
|
||||
roll = 10.0,
|
||||
contour = "POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1))"
|
||||
),
|
||||
satellite
|
||||
)
|
||||
|
||||
assertTrue(decision.accepted)
|
||||
assertEquals(1, surveys.size)
|
||||
assertTrue(surveys.single().contourWKT.startsWith("POLYGON"))
|
||||
assertFalse(surveys.single().contourWKT.startsWith("MULTIPOLYGON"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `different roll conflict resolved by trimming`() {
|
||||
val surveys = mutableListOf(survey(start = 51, end = 80, roll = 20.0))
|
||||
val satellite = satellite(mmi = 10.0)
|
||||
|
||||
val decision = service.tryPlaceSurvey(
|
||||
surveys,
|
||||
survey(start = 0, end = 70, roll = 10.0),
|
||||
satellite
|
||||
)
|
||||
|
||||
assertTrue(decision.accepted)
|
||||
assertTrue(decision.trimmed)
|
||||
assertEquals(40.0, decision.adjustedCandidate?.duration)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `different roll conflict that cannot be resolved is rejected`() {
|
||||
val surveys = mutableListOf(survey(start = 5, end = 40, roll = 20.0))
|
||||
val satellite = satellite(mmi = 10.0)
|
||||
|
||||
val decision = service.tryPlaceSurvey(
|
||||
surveys,
|
||||
survey(start = 0, end = 30, roll = 10.0),
|
||||
satellite
|
||||
)
|
||||
|
||||
assertFalse(decision.accepted)
|
||||
assertEquals("different_roll_conflict", decision.rejectionReason)
|
||||
assertEquals(1, surveys.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `epsilon based roll comparison treats nearly equal rolls as same chain`() {
|
||||
val surveys = mutableListOf(
|
||||
survey(
|
||||
start = 0,
|
||||
end = 40,
|
||||
roll = 10.0,
|
||||
contour = "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))"
|
||||
)
|
||||
)
|
||||
val satellite = satellite()
|
||||
|
||||
val decision = service.tryPlaceSurvey(
|
||||
surveys,
|
||||
survey(
|
||||
start = 20,
|
||||
end = 70,
|
||||
roll = 10.005,
|
||||
contour = "POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1))"
|
||||
),
|
||||
satellite
|
||||
)
|
||||
|
||||
assertTrue(decision.accepted)
|
||||
assertEquals(1, surveys.size)
|
||||
assertEquals(70, secondsFromBase(surveys.single().timeStop))
|
||||
assertNotNull(decision.appliedSurvey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `merge forbidden when time overlaps but geometry does not intersect`() {
|
||||
val surveys = mutableListOf(
|
||||
survey(
|
||||
start = 0,
|
||||
end = 40,
|
||||
contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"
|
||||
)
|
||||
)
|
||||
val satellite = satellite()
|
||||
|
||||
val logs = captureLogs {
|
||||
val decision = service.tryPlaceSurvey(
|
||||
surveys,
|
||||
survey(
|
||||
start = 20,
|
||||
end = 70,
|
||||
contour = "POLYGON ((10 10, 11 10, 11 11, 10 11, 10 10))"
|
||||
),
|
||||
satellite
|
||||
)
|
||||
|
||||
assertTrue(decision.accepted)
|
||||
}
|
||||
|
||||
assertEquals(2, surveys.size)
|
||||
assertTrue(logs.any { it.formattedMessage.contains("contours lack sufficient areal intersection despite temporal overlap") })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `merge forbidden when contours only touch by boundary`() {
|
||||
val surveys = mutableListOf(
|
||||
survey(
|
||||
start = 0,
|
||||
end = 40,
|
||||
contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"
|
||||
)
|
||||
)
|
||||
val satellite = satellite()
|
||||
|
||||
val logs = captureLogs {
|
||||
val decision = service.tryPlaceSurvey(
|
||||
surveys,
|
||||
survey(
|
||||
start = 20,
|
||||
end = 70,
|
||||
contour = "POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0))"
|
||||
),
|
||||
satellite
|
||||
)
|
||||
|
||||
assertTrue(decision.accepted)
|
||||
}
|
||||
|
||||
assertEquals(2, surveys.size)
|
||||
assertTrue(logs.any { it.formattedMessage.contains("contours lack sufficient areal intersection despite temporal overlap") })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `merge forbidden when intervals do not overlap but gap is within mmi`() {
|
||||
val surveys = mutableListOf(
|
||||
survey(
|
||||
start = 0,
|
||||
end = 40,
|
||||
contour = "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))"
|
||||
)
|
||||
)
|
||||
val satellite = satellite(mmi = 10.0)
|
||||
|
||||
val decision = service.tryPlaceSurvey(
|
||||
surveys,
|
||||
survey(
|
||||
start = 45,
|
||||
end = 70,
|
||||
contour = "POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1))"
|
||||
),
|
||||
satellite
|
||||
)
|
||||
|
||||
assertTrue(decision.accepted)
|
||||
assertEquals(2, surveys.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `merge allowed when intervals are adjacent and contours are continuous`() {
|
||||
val surveys = mutableListOf(
|
||||
survey(
|
||||
start = 0,
|
||||
end = 40,
|
||||
contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"
|
||||
)
|
||||
)
|
||||
val satellite = satellite()
|
||||
|
||||
val decision = service.tryPlaceSurvey(
|
||||
surveys,
|
||||
survey(
|
||||
start = 40,
|
||||
end = 70,
|
||||
contour = "POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0))"
|
||||
),
|
||||
satellite
|
||||
)
|
||||
|
||||
assertTrue(decision.accepted)
|
||||
assertEquals(1, surveys.size)
|
||||
assertEquals(0, secondsFromBase(surveys.single().time))
|
||||
assertEquals(70, secondsFromBase(surveys.single().timeStop))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `defensive fallback skips merge when union returns multipolygon`() {
|
||||
val logs = captureLogs {
|
||||
val method = service.javaClass.getDeclaredMethod(
|
||||
"unionContoursIfPolygon",
|
||||
String::class.java,
|
||||
String::class.java,
|
||||
Long::class.javaPrimitiveType,
|
||||
SurveyMode::class.java,
|
||||
SurveyMode::class.java
|
||||
)
|
||||
method.isAccessible = true
|
||||
|
||||
val result = method.invoke(
|
||||
service,
|
||||
"POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||
"POLYGON ((1 1, 2 1, 2 2, 1 2, 1 1))",
|
||||
22L,
|
||||
survey(start = 20, end = 70, contour = "POLYGON ((1 1, 2 1, 2 2, 1 2, 1 1))"),
|
||||
survey(start = 0, end = 40, contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))")
|
||||
)
|
||||
|
||||
assertEquals(null, result)
|
||||
}
|
||||
|
||||
assertTrue(logs.any { it.formattedMessage.contains("union produced non-polygon geometry") })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `no multipolygon persisted for merged route`() {
|
||||
val surveys = mutableListOf(
|
||||
survey(
|
||||
start = 0,
|
||||
end = 40,
|
||||
contour = "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))"
|
||||
)
|
||||
)
|
||||
val satellite = satellite()
|
||||
|
||||
val decision = service.tryPlaceSurvey(
|
||||
surveys,
|
||||
survey(
|
||||
start = 20,
|
||||
end = 70,
|
||||
contour = "POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1))"
|
||||
),
|
||||
satellite
|
||||
)
|
||||
|
||||
assertTrue(decision.accepted)
|
||||
assertEquals(1, surveys.size)
|
||||
assertFalse(surveys.single().contourWKT.startsWith("MULTIPOLYGON"))
|
||||
}
|
||||
|
||||
private fun satellite(
|
||||
durationMin: Double = 10.0,
|
||||
durationMax: Double = 300.0,
|
||||
mmi: Double = 10.0,
|
||||
dailyMaxDuration: Double = 1_000.0,
|
||||
revolutionMaxDuration: Double = 1_000.0
|
||||
) = SatelliteModel(
|
||||
satelliteId = 22,
|
||||
name = "T",
|
||||
bls = BLS(
|
||||
durationMin = durationMin,
|
||||
durationMax = durationMax,
|
||||
mmi = mmi,
|
||||
dailyMaxDuration = dailyMaxDuration,
|
||||
revolutionMaxDuration = revolutionMaxDuration
|
||||
)
|
||||
)
|
||||
|
||||
private fun survey(
|
||||
start: Long,
|
||||
end: Long,
|
||||
revolution: Long = 1,
|
||||
roll: Double = 10.0,
|
||||
source: SatelliteModeSource = SatelliteModeSource.COMPLAN,
|
||||
contour: String = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"
|
||||
) = SurveyMode(
|
||||
revolution = revolution,
|
||||
time = baseTime().plusSeconds(start),
|
||||
timeStop = baseTime().plusSeconds(end),
|
||||
roll = roll,
|
||||
duration = Duration.between(baseTime().plusSeconds(start), baseTime().plusSeconds(end)).seconds.toDouble(),
|
||||
contourWKT = contour,
|
||||
source = source
|
||||
)
|
||||
|
||||
private fun baseTime(): LocalDateTime = LocalDateTime.of(2026, 3, 26, 10, 0, 0)
|
||||
|
||||
private fun secondsFromBase(value: LocalDateTime): Long = Duration.between(baseTime(), value).seconds
|
||||
|
||||
private fun captureLogs(block: () -> Unit): List<ILoggingEvent> {
|
||||
val logger = LoggerFactory.getLogger(SurveyPlacementService::class.java) as Logger
|
||||
val appender = ListAppender<ILoggingEvent>()
|
||||
appender.start()
|
||||
logger.addAppender(appender)
|
||||
try {
|
||||
block()
|
||||
return appender.list.toList()
|
||||
} finally {
|
||||
logger.detachAppender(appender)
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.annotation.Primary
|
||||
import org.springframework.context.annotation.Profile
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteObservationProfileDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteVisualizationDTO
|
||||
|
||||
fun testSatelliteCatalogClient(): SatelliteCatalogClient = object : SatelliteCatalogClient {
|
||||
override fun allSatellites(): List<SatelliteDTO> = listOf(
|
||||
satelliteDto(10L, "Reflexio", SatelliteVisualizationDTO(125, 125, 78)),
|
||||
satelliteDto(11L, "Reflexio", SatelliteVisualizationDTO(125, 125, 78)),
|
||||
satelliteDto(22L, "Emissio", SatelliteVisualizationDTO(0, 125, 0)),
|
||||
satelliteDto(56756L, "KONDOR-FKA NO. 1", SatelliteVisualizationDTO(255, 0, 0), scanTle = true),
|
||||
satelliteDto(62138L, "KONDOR-FKA NO. 2", SatelliteVisualizationDTO(255, 125, 0), scanTle = true)
|
||||
)
|
||||
}
|
||||
|
||||
private fun satelliteDto(
|
||||
id: Long,
|
||||
name: String,
|
||||
visualization: SatelliteVisualizationDTO,
|
||||
scanTle: Boolean = false
|
||||
) = SatelliteDTO(
|
||||
id = id,
|
||||
noradId = if (scanTle) id else null,
|
||||
code = "TEST-$id",
|
||||
name = name,
|
||||
typeCode = name.uppercase().replace(' ', '_'),
|
||||
active = true,
|
||||
scanTle = scanTle,
|
||||
visualization = visualization,
|
||||
observationProfile = SatelliteObservationProfileDTO()
|
||||
)
|
||||
|
||||
@Configuration
|
||||
@Profile("test")
|
||||
class TestSatelliteCatalogClientConfiguration {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
fun satelliteCatalogClient(): SatelliteCatalogClient = testSatelliteCatalogClient()
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
spring:
|
||||
cloud:
|
||||
config:
|
||||
enabled: false
|
||||
datasource:
|
||||
url: jdbc:h2:mem:pcp_satellites;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
|
||||
|
||||
settings:
|
||||
calculation-interval: 3
|
||||
ballistics-service: http://localhost:7003
|
||||
earth-grid-service: http://localhost:7005
|
||||
complex-mission-service: http://localhost:7002
|
||||
stations-service: http://localhost:7009
|
||||
slots-service: http://localhost:7006
|
||||
coverage-scheme-service: http://localhost:7011
|
||||
Reference in New Issue
Block a user