Init
This commit is contained in:
+524
@@ -0,0 +1,524 @@
|
||||
package space.nstart.pcp.slots_service.controller
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.ArgumentMatchers.any
|
||||
import org.mockito.ArgumentMatchers.eq
|
||||
import org.mockito.Mockito.doReturn
|
||||
import org.mockito.Mockito.doThrow
|
||||
import org.mockito.Mockito.verify
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.boot.test.web.server.LocalServerPort
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import reactor.core.publisher.Mono
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.InitialConditionsDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.PositionDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.StationDTO
|
||||
import space.nstart.pcp.slots_service.dto.groupstate.GroupStateAvailabilityDTO
|
||||
import space.nstart.pcp.slots_service.dto.groupstate.GroupStateDetailsDTO
|
||||
import space.nstart.pcp.slots_service.dto.groupstate.GroupStateMatrixCellDTO
|
||||
import space.nstart.pcp.slots_service.dto.groupstate.GroupStateMatrixRowDTO
|
||||
import space.nstart.pcp.slots_service.dto.groupstate.GroupStateSatelliteDTO
|
||||
import space.nstart.pcp.slots_service.dto.pdcm.SatellitePdcmAvailabilityDTO
|
||||
import space.nstart.pcp.slots_service.dto.pdcm.SatellitePdcmDetailsDTO
|
||||
import space.nstart.pcp.slots_service.dto.pdcm.SatellitePdcmSummaryDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCalculationSummaryDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteCreateDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteGroupCreateDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteGroupDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSlotProfileDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteVisualizationDTO
|
||||
import space.nstart.pcp.slots_service.service.GroupStateService
|
||||
import space.nstart.pcp.slots_service.service.SatelliteCatalogService
|
||||
import space.nstart.pcp.slots_service.service.SatellitePdcmService
|
||||
import space.nstart.pcp.slots_service.service.SlotService
|
||||
import space.nstart.pcp.slots_service.service.StationService
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@SpringBootTest(
|
||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||
properties = [
|
||||
"spring.cloud.config.enabled=false",
|
||||
"spring.cloud.config.import-check.enabled=false",
|
||||
"spring.boot.admin.client.enabled=false"
|
||||
]
|
||||
)
|
||||
class CatalogControllerTest {
|
||||
|
||||
@LocalServerPort
|
||||
private var port: Int = 0
|
||||
|
||||
@MockitoBean
|
||||
private lateinit var satelliteCatalogService: SatelliteCatalogService
|
||||
|
||||
@MockitoBean
|
||||
private lateinit var stationService: StationService
|
||||
|
||||
@MockitoBean
|
||||
private lateinit var slotService: SlotService
|
||||
|
||||
@MockitoBean
|
||||
private lateinit var groupStateService: GroupStateService
|
||||
|
||||
@MockitoBean
|
||||
private lateinit var satellitePdcmService: SatellitePdcmService
|
||||
|
||||
@Test
|
||||
fun `groups page is rendered`() {
|
||||
val body = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/groups")
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java)
|
||||
.block()!!
|
||||
|
||||
assertTrue(body.contains("Группировки"))
|
||||
assertTrue(body.contains("Новая группировка"))
|
||||
assertTrue(body.contains("dropdown-toggle"))
|
||||
assertTrue(body.contains(">Управление<"))
|
||||
assertTrue(body.contains(">Состояние ОГ<"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `group state page is rendered`() {
|
||||
val body = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/group-state")
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java)
|
||||
.block()!!
|
||||
|
||||
assertTrue(body.contains("Состояние ОГ"))
|
||||
assertTrue(body.contains("Доступные группировки"))
|
||||
assertTrue(body.contains("Показать состояние"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `satellites page is rendered`() {
|
||||
val body = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/satellites")
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java)
|
||||
.block()!!
|
||||
|
||||
assertTrue(body.contains("Спутники"))
|
||||
assertTrue(body.contains("Observation profile"))
|
||||
assertTrue(body.contains("Slot profile"))
|
||||
assertTrue(body.contains("Расчет слотов"))
|
||||
assertTrue(body.contains("slot-calculation-summary"))
|
||||
assertTrue(body.contains("Начальные условия орбиты"))
|
||||
assertTrue(body.contains("Сохранить начальные условия"))
|
||||
assertTrue(body.contains("<th>Слоты</th>"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `satellite pdcm page is rendered`() {
|
||||
val body = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/satellites/pdcm")
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java)
|
||||
.block()!!
|
||||
|
||||
assertTrue(body.contains("Параметры движения центра масс"))
|
||||
assertTrue(body.contains("Asc-node"))
|
||||
assertTrue(body.contains("Спутники"))
|
||||
assertTrue(body.contains("Скачать CSV"))
|
||||
assertTrue(body.contains("/webjars/bootstrap/5.3.0/css/bootstrap.min.css"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `stations page is rendered`() {
|
||||
val body = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/stations")
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java)
|
||||
.block()!!
|
||||
|
||||
assertTrue(body.contains("Станции"))
|
||||
assertTrue(body.contains("Новая станция"))
|
||||
assertTrue(body.contains("Мин.угол"))
|
||||
assertTrue(body.contains("Макс.угол"))
|
||||
assertTrue(body.contains(">Управление<"))
|
||||
assertTrue(body.contains(">ЗРВ<"))
|
||||
assertFalse(body.contains("<th>UUID</th>"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `group create endpoint proxies request`() {
|
||||
val response = SatelliteGroupDTO(
|
||||
id = 71L,
|
||||
name = "KONDOR",
|
||||
satelliteIds = listOf(56756L, 62138L)
|
||||
)
|
||||
val request = SatelliteGroupCreateDTO(
|
||||
name = "KONDOR",
|
||||
satelliteIds = listOf(56756L, 62138L)
|
||||
)
|
||||
doReturn(response).`when`(satelliteCatalogService).createGroup(request)
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.post()
|
||||
.uri("/api/catalog/groups")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(request)
|
||||
.retrieve()
|
||||
.bodyToMono(SatelliteGroupDTO::class.java)
|
||||
.block()!!
|
||||
|
||||
assertEquals(71L, actual.id)
|
||||
assertEquals("KONDOR", actual.name)
|
||||
verify(satelliteCatalogService).createGroup(request)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `group state endpoint proxies request`() {
|
||||
val requestedTime = LocalDateTime.of(2026, 4, 24, 12, 15)
|
||||
val response = GroupStateDetailsDTO(
|
||||
groupId = 71L,
|
||||
groupName = "KONDOR",
|
||||
availableInterval = GroupStateAvailabilityDTO(
|
||||
timeStart = LocalDateTime.of(2026, 4, 24, 10, 0),
|
||||
timeStop = LocalDateTime.of(2026, 4, 24, 14, 0)
|
||||
),
|
||||
calculationTime = requestedTime,
|
||||
satellites = listOf(
|
||||
GroupStateSatelliteDTO(
|
||||
satelliteId = 501L,
|
||||
noradId = 56756L,
|
||||
code = "SAT-501",
|
||||
name = "Sat 501",
|
||||
omegabDeg = 15.0,
|
||||
uDeg = 45.0
|
||||
)
|
||||
),
|
||||
matrix = listOf(
|
||||
GroupStateMatrixRowDTO(
|
||||
satelliteId = 501L,
|
||||
cells = listOf(
|
||||
GroupStateMatrixCellDTO(
|
||||
rowSatelliteId = 501L,
|
||||
columnSatelliteId = 501L,
|
||||
deltaOmegabDeg = 0.0,
|
||||
deltaUDeg = 0.0
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
doReturn(response).`when`(groupStateService).groupState(71L, requestedTime)
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/api/catalog/group-states/71?time=2026-04-24T12:15:00")
|
||||
.retrieve()
|
||||
.bodyToMono(GroupStateDetailsDTO::class.java)
|
||||
.block()!!
|
||||
|
||||
assertEquals(71L, actual.groupId)
|
||||
assertEquals(requestedTime, actual.calculationTime)
|
||||
assertEquals(1, actual.satellites.size)
|
||||
verify(groupStateService).groupState(71L, requestedTime)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `satellite pdcm endpoint proxies request`() {
|
||||
val response = SatellitePdcmDetailsDTO(
|
||||
satelliteId = 501L,
|
||||
code = "SAT-501",
|
||||
name = "Sat 501",
|
||||
activeInterval = true,
|
||||
availableInterval = SatellitePdcmAvailabilityDTO(
|
||||
timeStart = LocalDateTime.of(2026, 4, 24, 10, 0),
|
||||
timeStop = LocalDateTime.of(2026, 4, 24, 12, 0)
|
||||
),
|
||||
ascNodes = emptyList()
|
||||
)
|
||||
doReturn(response).`when`(satellitePdcmService).satelliteDetails(501L)
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/api/catalog/satellites/pdcm/501")
|
||||
.retrieve()
|
||||
.bodyToMono(SatellitePdcmDetailsDTO::class.java)
|
||||
.block()!!
|
||||
|
||||
assertEquals(501L, actual.satelliteId)
|
||||
assertTrue(actual.activeInterval)
|
||||
verify(satellitePdcmService).satelliteDetails(501L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `satellite pdcm list endpoint proxies request`() {
|
||||
val response = listOf(
|
||||
SatellitePdcmSummaryDTO(
|
||||
satelliteId = 501L,
|
||||
code = "SAT-501",
|
||||
name = "Sat 501",
|
||||
activeInterval = true,
|
||||
availableInterval = SatellitePdcmAvailabilityDTO(
|
||||
timeStart = LocalDateTime.of(2026, 4, 24, 10, 0),
|
||||
timeStop = LocalDateTime.of(2026, 4, 24, 12, 0)
|
||||
)
|
||||
)
|
||||
)
|
||||
doReturn(response).`when`(satellitePdcmService).satelliteSummaries()
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/api/catalog/satellites/pdcm")
|
||||
.retrieve()
|
||||
.bodyToMono(Array<SatellitePdcmSummaryDTO>::class.java)
|
||||
.block()!!
|
||||
|
||||
assertEquals(1, actual.size)
|
||||
assertEquals(501L, actual[0].satelliteId)
|
||||
verify(satellitePdcmService).satelliteSummaries()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `satellite create endpoint proxies request`() {
|
||||
val request = SatelliteCreateDTO(
|
||||
id = 501L,
|
||||
noradId = 9501L,
|
||||
code = "SAT-501",
|
||||
name = "Sat 501",
|
||||
typeCode = "TEST",
|
||||
active = true,
|
||||
scanTle = true,
|
||||
visualization = SatelliteVisualizationDTO(red = 11, green = 22, blue = 33)
|
||||
)
|
||||
val response = SatelliteDTO(
|
||||
id = 501L,
|
||||
noradId = 9501L,
|
||||
code = "SAT-501",
|
||||
name = "Sat 501",
|
||||
typeCode = "TEST",
|
||||
active = true,
|
||||
scanTle = true,
|
||||
visualization = SatelliteVisualizationDTO(red = 11, green = 22, blue = 33)
|
||||
)
|
||||
doReturn(response).`when`(satelliteCatalogService).createSatellite(request)
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.post()
|
||||
.uri("/api/catalog/satellites")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(request)
|
||||
.retrieve()
|
||||
.bodyToMono(SatelliteDTO::class.java)
|
||||
.block()!!
|
||||
|
||||
assertEquals(501L, actual.id)
|
||||
assertEquals("SAT-501", actual.code)
|
||||
verify(satelliteCatalogService).createSatellite(request)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `station save endpoint proxies request`() {
|
||||
val stationId = UUID.fromString("2f1d1d8b-588d-4d4e-a456-80a5c93e09dd")
|
||||
val request = StationDTO(
|
||||
id = stationId,
|
||||
number = 7,
|
||||
name = "Station 7",
|
||||
position = PositionDTO(
|
||||
lat = 55.75,
|
||||
long = 37.62,
|
||||
height = 180.0
|
||||
),
|
||||
elevationMin = 5.0,
|
||||
elevationMax = 88.0
|
||||
)
|
||||
doReturn(request).`when`(stationService).saveStation(anyStation())
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.post()
|
||||
.uri("/api/stations")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(request)
|
||||
.retrieve()
|
||||
.bodyToMono(StationDTO::class.java)
|
||||
.block()!!
|
||||
|
||||
assertEquals(stationId, actual.id)
|
||||
assertEquals("Station 7", actual.name)
|
||||
verify(stationService).saveStation(anyStation())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `satellite initial conditions get endpoint returns no content when absent`() {
|
||||
doReturn(null).`when`(slotService).satelliteInitialConditions(501L)
|
||||
|
||||
val status = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/api/catalog/satellites/501/initial-conditions")
|
||||
.exchangeToMono { Mono.just(it.statusCode()) }
|
||||
.block()!!
|
||||
|
||||
assertEquals(HttpStatus.NO_CONTENT, status)
|
||||
verify(slotService).satelliteInitialConditions(501L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `satellite slot calculation summaries endpoint proxies request`() {
|
||||
val response = listOf(
|
||||
SlotCalculationSummaryDTO.calculated(
|
||||
satelliteId = 501L,
|
||||
slotCount = 10L,
|
||||
minTime = LocalDateTime.of(2026, 4, 20, 0, 0),
|
||||
maxTime = LocalDateTime.of(2026, 4, 21, 0, 0),
|
||||
minRevolution = 100L,
|
||||
maxRevolution = 112L
|
||||
)
|
||||
)
|
||||
doReturn(response).`when`(slotService).slotCalculationSummaries(listOf(501L, 502L))
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/api/catalog/satellites/slot-calculation-summaries?satelliteIds=501&satelliteIds=502")
|
||||
.retrieve()
|
||||
.bodyToMono(Array<SlotCalculationSummaryDTO>::class.java)
|
||||
.block()!!
|
||||
|
||||
assertEquals(1, actual.size)
|
||||
assertEquals(501L, actual[0].satelliteId)
|
||||
assertTrue(actual[0].calculated)
|
||||
assertEquals(12L, actual[0].revolutionInterval)
|
||||
verify(slotService).slotCalculationSummaries(listOf(501L, 502L))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `satellite slot calculation summaries endpoint returns empty summaries when slot service fails`() {
|
||||
doThrow(RuntimeException("slot service unavailable"))
|
||||
.`when`(slotService).slotCalculationSummaries(listOf(501L, 502L))
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/api/catalog/satellites/slot-calculation-summaries?satelliteIds=501&satelliteIds=502")
|
||||
.retrieve()
|
||||
.bodyToMono(Array<SlotCalculationSummaryDTO>::class.java)
|
||||
.block()!!
|
||||
|
||||
assertEquals(2, actual.size)
|
||||
assertEquals(501L, actual[0].satelliteId)
|
||||
assertFalse(actual[0].calculated)
|
||||
assertEquals(502L, actual[1].satelliteId)
|
||||
assertFalse(actual[1].calculated)
|
||||
verify(slotService).slotCalculationSummaries(listOf(501L, 502L))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `satellite slot calculation summary endpoint proxies request`() {
|
||||
val response = SlotCalculationSummaryDTO.calculated(
|
||||
satelliteId = 501L,
|
||||
slotCount = 10L,
|
||||
minTime = LocalDateTime.of(2026, 4, 20, 0, 0),
|
||||
maxTime = LocalDateTime.of(2026, 4, 21, 0, 0),
|
||||
minRevolution = 100L,
|
||||
maxRevolution = 112L
|
||||
)
|
||||
doReturn(response).`when`(slotService).slotCalculationSummary(501L)
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/api/catalog/satellites/501/slot-calculation-summary")
|
||||
.retrieve()
|
||||
.bodyToMono(SlotCalculationSummaryDTO::class.java)
|
||||
.block()!!
|
||||
|
||||
assertEquals(501L, actual.satelliteId)
|
||||
assertTrue(actual.calculated)
|
||||
assertEquals(12L, actual.revolutionInterval)
|
||||
verify(slotService).slotCalculationSummary(501L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `satellite slot calculation summary endpoint returns empty summary when slot service fails`() {
|
||||
doThrow(RuntimeException("slot service unavailable"))
|
||||
.`when`(slotService).slotCalculationSummary(501L)
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/api/catalog/satellites/501/slot-calculation-summary")
|
||||
.retrieve()
|
||||
.bodyToMono(SlotCalculationSummaryDTO::class.java)
|
||||
.block()!!
|
||||
|
||||
assertEquals(501L, actual.satelliteId)
|
||||
assertFalse(actual.calculated)
|
||||
assertEquals(0L, actual.slotCount)
|
||||
verify(slotService).slotCalculationSummary(501L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `satellite initial conditions save endpoint proxies request`() {
|
||||
val request = InitialConditionsDTO(
|
||||
orbPoint = OrbPointDTO(
|
||||
time = LocalDateTime.of(2026, 4, 20, 10, 15),
|
||||
revolution = 123L,
|
||||
vx = 1.1,
|
||||
vy = 2.2,
|
||||
vz = 3.3,
|
||||
x = 4.4,
|
||||
y = 5.5,
|
||||
z = 6.6
|
||||
),
|
||||
sBall = 0.07,
|
||||
f81 = 145.2
|
||||
)
|
||||
val response = SatelliteICDTO(
|
||||
satelliteId = 501L,
|
||||
ic = request
|
||||
)
|
||||
doReturn(response).`when`(slotService).saveSatelliteInitialConditions(eq(501L), anyInitialConditions(), eq(true))
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.post()
|
||||
.uri("/api/catalog/satellites/501/initial-conditions?create=true")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(request)
|
||||
.retrieve()
|
||||
.bodyToMono(SatelliteICDTO::class.java)
|
||||
.block()!!
|
||||
|
||||
assertEquals(501L, actual.satelliteId)
|
||||
assertEquals(123L, actual.ic.orbPoint.revolution)
|
||||
verify(slotService).saveSatelliteInitialConditions(eq(501L), anyInitialConditions(), eq(true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `satellite slot calculation endpoint proxies request`() {
|
||||
val request = SatelliteSlotProfileDTO(
|
||||
cycleRevs = 243L,
|
||||
slotDuration = 10L,
|
||||
durationCalcDays = 16L,
|
||||
maxChainLength = 2
|
||||
)
|
||||
|
||||
val status = WebClient.create("http://localhost:$port")
|
||||
.post()
|
||||
.uri("/api/catalog/satellites/501/slot-calculation")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(request)
|
||||
.exchangeToMono { Mono.just(it.statusCode()) }
|
||||
.block()!!
|
||||
|
||||
assertEquals(HttpStatus.NO_CONTENT, status)
|
||||
verify(slotService).startSlotCalculation(eq(501L), anySlotProfile())
|
||||
}
|
||||
|
||||
private fun anyStation(): StationDTO = any(StationDTO::class.java) ?: StationDTO()
|
||||
private fun anyInitialConditions(): InitialConditionsDTO = any(InitialConditionsDTO::class.java) ?: InitialConditionsDTO()
|
||||
private fun anySlotProfile(): SatelliteSlotProfileDTO = any(SatelliteSlotProfileDTO::class.java) ?: SatelliteSlotProfileDTO()
|
||||
}
|
||||
+597
@@ -0,0 +1,597 @@
|
||||
package space.nstart.pcp.slots_service.controller
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.ArgumentMatchers.any
|
||||
import org.mockito.Mockito.doAnswer
|
||||
import org.mockito.Mockito.doReturn
|
||||
import org.mockito.Mockito.verify
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
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 reactor.core.publisher.Flux
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.StationDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.ComplexPlanProcessRequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.RequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageStrategy
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteInfoDTO
|
||||
import space.nstart.pcp.slots_service.service.EarthService
|
||||
import space.nstart.pcp.slots_service.service.ComplexMissionService
|
||||
import space.nstart.pcp.slots_service.service.DynamicPlanService
|
||||
import space.nstart.pcp.slots_service.service.SlotService
|
||||
import space.nstart.pcp.slots_service.service.StationService
|
||||
import space.nstart.pcp.slots_service.dto.dynamicplan.DynamicPlanCalculationRequestDTO
|
||||
import space.nstart.pcp.slots_service.dto.dynamicplan.DynamicPlanCalculationModeDTO
|
||||
import space.nstart.pcp.slots_service.dto.dynamicplan.DynamicPlanRevolutionModeDTO
|
||||
import tools.jackson.databind.ObjectMapper
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@SpringBootTest(
|
||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||
properties = [
|
||||
"spring.cloud.config.enabled=false",
|
||||
"spring.cloud.config.import-check.enabled=false",
|
||||
"spring.boot.admin.client.enabled=false"
|
||||
]
|
||||
)
|
||||
class MapControllerComplexPlanTest {
|
||||
|
||||
@LocalServerPort
|
||||
private var port: Int = 0
|
||||
|
||||
@MockitoBean
|
||||
private lateinit var earthService: EarthService
|
||||
|
||||
@MockitoBean
|
||||
private lateinit var complexMissionService: ComplexMissionService
|
||||
|
||||
@MockitoBean
|
||||
private lateinit var dynamicPlanService: DynamicPlanService
|
||||
|
||||
@MockitoBean
|
||||
private lateinit var stationService: StationService
|
||||
|
||||
@MockitoBean
|
||||
private lateinit var slotService: SlotService
|
||||
|
||||
private val objectMapper = ObjectMapper()
|
||||
|
||||
@Test
|
||||
fun `local post api requests delegates to earth service facade`() {
|
||||
val responseId = UUID.fromString("11111111-1111-4111-8111-111111111111")
|
||||
var capturedRequest: RequestDTO? = null
|
||||
doAnswer { invocation ->
|
||||
capturedRequest = invocation.getArgument<RequestDTO>(0)
|
||||
RequestDTO(
|
||||
requestId = responseId,
|
||||
name = "created request",
|
||||
importance = 7.5,
|
||||
contour = "POLYGON ((0 0, 0 1, 1 1, 0 0))",
|
||||
)
|
||||
}.`when`(earthService).addRequest(anyRequest())
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.post()
|
||||
.uri("/api/requests")
|
||||
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
|
||||
.bodyValue(
|
||||
mapOf(
|
||||
"name" to "map request",
|
||||
"importance" to 7.5,
|
||||
"geoType" to "POLYGON",
|
||||
"contour" to "POLYGON ((0 0, 0 1, 1 1, 0 0))",
|
||||
)
|
||||
)
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java)
|
||||
.map(objectMapper::readTree)
|
||||
.block()!!
|
||||
|
||||
assertEquals(responseId.toString(), actual["requestId"].asText())
|
||||
assertEquals("created request", actual["name"].asText())
|
||||
assertEquals("map request", capturedRequest?.name)
|
||||
assertEquals("POLYGON ((0 0, 0 1, 1 1, 0 0))", capturedRequest?.contour)
|
||||
verify(earthService).addRequest(anyRequest())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `satellite slots endpoint proxies interval request`() {
|
||||
val satelliteId = 101L
|
||||
val timeStart = LocalDateTime.of(2026, 4, 8, 10, 0)
|
||||
val timeStop = LocalDateTime.of(2026, 4, 8, 12, 0)
|
||||
doReturn(
|
||||
listOf(
|
||||
SlotDTO(
|
||||
cycle = 2,
|
||||
satelliteId = satelliteId,
|
||||
tn = timeStart,
|
||||
tk = timeStart.plusMinutes(6),
|
||||
roll = 10.5,
|
||||
contour = "POLYGON ((0 0, 0 1, 1 1, 0 0))",
|
||||
revolution = 55L,
|
||||
revolutionSign = RevolutionSign.ASC,
|
||||
slotNumber = 9L
|
||||
)
|
||||
)
|
||||
).`when`(slotService).allSlotsByInterval(satelliteId, timeStart, timeStop)
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/api/satellites/$satelliteId/slots?timeStart=$timeStart&timeStop=$timeStop")
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java)
|
||||
.map(objectMapper::readTree)
|
||||
.block()!!
|
||||
|
||||
assertEquals(1, actual.size())
|
||||
assertEquals(satelliteId, actual[0]["satelliteId"].asLong())
|
||||
assertEquals(9L, actual[0]["slotNumber"].asLong())
|
||||
verify(slotService).allSlotsByInterval(satelliteId, timeStart, timeStop)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `complex plan endpoint proxies process request`() {
|
||||
val request = ComplexPlanProcessRequestDTO(
|
||||
intervalStart = LocalDateTime.of(2026, 4, 8, 10, 0),
|
||||
intervalEnd = LocalDateTime.of(2026, 4, 8, 12, 0),
|
||||
satelliteIds = listOf(101L, 202L),
|
||||
countLat = 2,
|
||||
countLong = 3,
|
||||
coverageStrategy = SlotCoverageStrategy.CONTINUOUS
|
||||
)
|
||||
val response = objectMapper.readTree(
|
||||
"""
|
||||
{
|
||||
"runId": 17,
|
||||
"status": "COMPLETED",
|
||||
"durationMs": 60000,
|
||||
"calculatedModesCount": 4,
|
||||
"bookedSlotLinksCount": 9
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
doReturn(response).`when`(complexMissionService).processComplexPlan(request)
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.post()
|
||||
.uri("/api/com-plan/process")
|
||||
.bodyValue(
|
||||
mapOf(
|
||||
"intervalStart" to "2026-04-08T10:00:00",
|
||||
"intervalEnd" to "2026-04-08T12:00:00",
|
||||
"satelliteIds" to listOf(101, 202),
|
||||
"countLat" to 2,
|
||||
"countLong" to 3,
|
||||
"coverageStrategy" to "CONTINUOUS"
|
||||
)
|
||||
)
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java)
|
||||
.map(objectMapper::readTree)
|
||||
.block()!!
|
||||
|
||||
assertEquals(17, actual["runId"].asInt())
|
||||
assertEquals("COMPLETED", actual["status"].asText())
|
||||
assertEquals(60000, actual["durationMs"].asLong())
|
||||
assertEquals(4, actual["calculatedModesCount"].asInt())
|
||||
verify(complexMissionService).processComplexPlan(request)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `complex plan page is rendered and menu contains link after rva`() {
|
||||
val body = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/com-plan")
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java)
|
||||
.block()!!
|
||||
|
||||
assertTrue(body.contains("Ком.План"))
|
||||
assertTrue(body.contains("complex-plan-runs-body"))
|
||||
assertTrue(body.contains("complex-plan-pagination"))
|
||||
assertTrue(body.contains("complex-plan-process-form"))
|
||||
assertTrue(body.contains("complex-plan-satellite-ids"))
|
||||
assertTrue(body.contains("complex-plan-interval-start"))
|
||||
assertTrue(body.contains("complex-plan-coverage-strategy"))
|
||||
assertTrue(body.contains("complex-plan-count-lat"))
|
||||
assertTrue(body.contains("complex-plan-count-long"))
|
||||
assertTrue(body.contains("complex-plan-report"))
|
||||
assertTrue(body.contains("/complex_plan_scripts.js"))
|
||||
assertTrue(body.indexOf("/rva") < body.indexOf("/com-plan"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `complex plan script renders added survey count by satellite`() {
|
||||
val body = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/complex_plan_scripts.js")
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java)
|
||||
.block()!!
|
||||
|
||||
assertTrue(body.contains("Добавлено съемок по спутникам"))
|
||||
assertTrue(body.contains("addedSurveyRowsBySatellite"))
|
||||
assertTrue(body.contains("source === 'COMPLAN' || source === 'MIXED'"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dynamic plan page is rendered and menu contains separate link`() {
|
||||
val body = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/dynamic-plan")
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java)
|
||||
.block()!!
|
||||
|
||||
assertTrue(body.contains("Dynamic Plan"))
|
||||
assertTrue(body.contains("dynamic-plan-form"))
|
||||
assertTrue(body.contains("dynamic-plan-request-id"))
|
||||
assertTrue(body.contains("dynamic-plan-satellite-ids"))
|
||||
assertTrue(body.contains("dynamic-plan-interval-start"))
|
||||
assertTrue(body.contains("dynamic-plan-interval-end"))
|
||||
assertTrue(body.contains("dynamic-plan-runs-body"))
|
||||
assertTrue(body.contains("dynamic-plan-runs-refresh"))
|
||||
assertTrue(body.contains("/dynamic_plan_map_layers.js"))
|
||||
assertTrue(body.contains("/dynamic_plan_scripts.js"))
|
||||
assertTrue(body.contains("/com-plan"))
|
||||
assertTrue(body.contains("/dynamic-plan"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `map page loads dynamic plan map layer script`() {
|
||||
doReturn(Flux.empty<RequestDTO>()).`when`(earthService).reqs()
|
||||
doReturn(Flux.empty<SatelliteInfoDTO>()).`when`(complexMissionService).satellites()
|
||||
doReturn(Flux.empty<StationDTO>()).`when`(stationService).stations()
|
||||
|
||||
val body = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/map")
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java)
|
||||
.block()!!
|
||||
|
||||
assertTrue(body.contains("cesiumContainer"))
|
||||
assertTrue(body.contains("dynamic_plan_map_layers.js"))
|
||||
assertTrue(body.contains("PcpDynamicPlanMapLayers"))
|
||||
verify(earthService).reqs()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dynamic plan map layer keeps only one visible run`() {
|
||||
val body = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/dynamic_plan_map_layers.js")
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java)
|
||||
.block()!!
|
||||
|
||||
assertTrue(body.contains("parsed.slice(0, 1)"))
|
||||
assertTrue(body.contains("writeRuns([normalizedRun])"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dynamic plan endpoint proxies calculation request`() {
|
||||
val requestId = UUID.fromString("11111111-1111-4111-8111-111111111111")
|
||||
val runId = UUID.fromString("22222222-2222-4222-8222-222222222222")
|
||||
val request = DynamicPlanCalculationRequestDTO(
|
||||
requestId = requestId,
|
||||
satelliteIds = listOf(101L, 202L),
|
||||
calculationStart = LocalDateTime.of(2026, 4, 8, 10, 0),
|
||||
calculationEnd = LocalDateTime.of(2026, 4, 8, 12, 0),
|
||||
calculationMode = DynamicPlanCalculationModeDTO.GREEDY,
|
||||
revolutionMode = DynamicPlanRevolutionModeDTO.DESC,
|
||||
filterCoveredRoutes = true
|
||||
)
|
||||
val response = objectMapper.readTree(
|
||||
"""
|
||||
{
|
||||
"runId": "$runId",
|
||||
"status": "PENDING",
|
||||
"requestId": "$requestId",
|
||||
"satelliteIds": [101, 202],
|
||||
"calculationStart": "2026-04-08T10:00:00",
|
||||
"calculationEnd": "2026-04-08T12:00:00",
|
||||
"calculationMode": "GREEDY",
|
||||
"revolutionMode": "DESC",
|
||||
"filterCoveredRoutes": true,
|
||||
"createdAt": "2026-04-08T09:59:59"
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
doReturn(response).`when`(dynamicPlanService).calculate(request)
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.post()
|
||||
.uri("/api/dynamic-plan/calc")
|
||||
.bodyValue(
|
||||
mapOf(
|
||||
"requestId" to requestId.toString(),
|
||||
"satelliteIds" to listOf(101, 202),
|
||||
"calculationStart" to "2026-04-08T10:00:00",
|
||||
"calculationEnd" to "2026-04-08T12:00:00",
|
||||
"calculationMode" to "GREEDY",
|
||||
"revolutionMode" to "DESC",
|
||||
"filterCoveredRoutes" to true
|
||||
)
|
||||
)
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java)
|
||||
.map(objectMapper::readTree)
|
||||
.block()!!
|
||||
|
||||
assertEquals(runId.toString(), actual["runId"].asText())
|
||||
assertEquals("PENDING", actual["status"].asText())
|
||||
assertEquals("GREEDY", actual["calculationMode"].asText())
|
||||
assertEquals("DESC", actual["revolutionMode"].asText())
|
||||
assertEquals(true, actual["filterCoveredRoutes"].asBoolean())
|
||||
verify(dynamicPlanService).calculate(request)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dynamic plan runs endpoint proxies history request`() {
|
||||
val runId = UUID.fromString("22222222-2222-4222-8222-222222222222")
|
||||
val response = objectMapper.readTree(
|
||||
"""
|
||||
[
|
||||
{
|
||||
"runId": "$runId",
|
||||
"status": "COMPLETED",
|
||||
"routesCount": 4
|
||||
}
|
||||
]
|
||||
""".trimIndent()
|
||||
)
|
||||
doReturn(response).`when`(dynamicPlanService).getRuns(null, 20, 0)
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/api/dynamic-plan/runs?limit=20&offset=0")
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java)
|
||||
.map(objectMapper::readTree)
|
||||
.block()!!
|
||||
|
||||
assertEquals(1, actual.size())
|
||||
assertEquals(runId.toString(), actual[0]["runId"].asText())
|
||||
assertEquals("COMPLETED", actual[0]["status"].asText())
|
||||
verify(dynamicPlanService).getRuns(null, 20, 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dynamic plan run endpoint proxies status request`() {
|
||||
val runId = UUID.fromString("22222222-2222-4222-8222-222222222222")
|
||||
val response = objectMapper.readTree(
|
||||
"""
|
||||
{
|
||||
"runId": "$runId",
|
||||
"status": "RUNNING",
|
||||
"routesCount": null
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
doReturn(response).`when`(dynamicPlanService).getRun(runId)
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/api/dynamic-plan/runs/$runId")
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java)
|
||||
.map(objectMapper::readTree)
|
||||
.block()!!
|
||||
|
||||
assertEquals(runId.toString(), actual["runId"].asText())
|
||||
assertEquals("RUNNING", actual["status"].asText())
|
||||
verify(dynamicPlanService).getRun(runId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dynamic plan result endpoint proxies result request`() {
|
||||
val runId = UUID.fromString("22222222-2222-4222-8222-222222222222")
|
||||
val requestId = UUID.fromString("11111111-1111-4111-8111-111111111111")
|
||||
val response = objectMapper.readTree(
|
||||
"""
|
||||
{
|
||||
"status": "COMPLETED",
|
||||
"requestId": "$requestId",
|
||||
"satelliteIds": [101, 202],
|
||||
"missingSatelliteIds": [],
|
||||
"routesCount": 4,
|
||||
"durationMs": 5000
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
doReturn(response).`when`(dynamicPlanService).getResult(runId)
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/api/dynamic-plan/runs/$runId/result")
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java)
|
||||
.map(objectMapper::readTree)
|
||||
.block()!!
|
||||
|
||||
assertEquals("COMPLETED", actual["status"].asText())
|
||||
assertEquals(4, actual["routesCount"].asInt())
|
||||
verify(dynamicPlanService).getResult(runId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dynamic plan routes endpoint proxies routes request`() {
|
||||
val runId = UUID.fromString("22222222-2222-4222-8222-222222222222")
|
||||
val response = objectMapper.readTree(
|
||||
"""
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"runId": "$runId",
|
||||
"requestId": "11111111-1111-4111-8111-111111111111",
|
||||
"satelliteId": 101,
|
||||
"startTime": "2026-04-08T10:10:00",
|
||||
"endTime": "2026-04-08T10:10:30",
|
||||
"duration": 30.0,
|
||||
"revolution": 55,
|
||||
"roll": 11.5,
|
||||
"contourWkt": "POLYGON ((0 0, 0 1, 1 1, 0 0))"
|
||||
}
|
||||
]
|
||||
""".trimIndent()
|
||||
)
|
||||
doReturn(response).`when`(dynamicPlanService).getRoutes(runId, 1000, 0)
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/api/dynamic-plan/runs/$runId/routes")
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java)
|
||||
.map(objectMapper::readTree)
|
||||
.block()!!
|
||||
|
||||
assertEquals(1, actual.size())
|
||||
assertEquals(101, actual[0]["satelliteId"].asInt())
|
||||
verify(dynamicPlanService).getRoutes(runId, 1000, 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dynamic plan intervals endpoint proxies intervals request`() {
|
||||
val runId = UUID.fromString("22222222-2222-4222-8222-222222222222")
|
||||
val response = objectMapper.readTree(
|
||||
"""
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"runId": "$runId",
|
||||
"requestId": "11111111-1111-4111-8111-111111111111",
|
||||
"intervalId": "33333333-3333-4333-8333-333333333333",
|
||||
"regionId": "44444444-4444-4444-8444-444444444444",
|
||||
"revSign": "ASC",
|
||||
"contourWkt": "POLYGON ((0 0, 0 1, 1 1, 0 0))",
|
||||
"observationParametersCount": 12
|
||||
}
|
||||
]
|
||||
""".trimIndent()
|
||||
)
|
||||
doReturn(response).`when`(dynamicPlanService).getIntervals(runId, 1000, 0)
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/api/dynamic-plan/runs/$runId/intervals")
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java)
|
||||
.map(objectMapper::readTree)
|
||||
.block()!!
|
||||
|
||||
assertEquals(1, actual.size())
|
||||
assertEquals("ASC", actual[0]["revSign"].asText())
|
||||
assertEquals(12, actual[0]["observationParametersCount"].asInt())
|
||||
verify(dynamicPlanService).getIntervals(runId, 1000, 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `complex plan runs endpoint proxies list request`() {
|
||||
val response = objectMapper.readTree(
|
||||
"""
|
||||
[
|
||||
{
|
||||
"runId": 17,
|
||||
"status": "COMPLETED",
|
||||
"intervalStart": "2026-04-08T10:00:00",
|
||||
"intervalEnd": "2026-04-08T12:00:00",
|
||||
"satelliteIds": [101, 202]
|
||||
}
|
||||
]
|
||||
""".trimIndent()
|
||||
)
|
||||
doReturn(response).`when`(complexMissionService).complexPlanRuns()
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/api/com-plan/runs")
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java)
|
||||
.map(objectMapper::readTree)
|
||||
.block()!!
|
||||
|
||||
assertEquals(1, actual.size())
|
||||
assertEquals(17, actual[0]["runId"].asInt())
|
||||
assertEquals("COMPLETED", actual[0]["status"].asText())
|
||||
verify(complexMissionService).complexPlanRuns()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `complex plan run endpoint proxies detail request`() {
|
||||
val response = objectMapper.readTree(
|
||||
"""
|
||||
{
|
||||
"runId": 17,
|
||||
"status": "COMPLETED",
|
||||
"durationMs": 60000,
|
||||
"requestPayload": "{}"
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
doReturn(response).`when`(complexMissionService).complexPlanRun(17L)
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/api/com-plan/runs/17")
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java)
|
||||
.map(objectMapper::readTree)
|
||||
.block()!!
|
||||
|
||||
assertEquals(17, actual["runId"].asInt())
|
||||
assertEquals(60000, actual["durationMs"].asLong())
|
||||
verify(complexMissionService).complexPlanRun(17L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `complex plan run modes endpoint proxies modes request`() {
|
||||
val response = objectMapper.readTree(
|
||||
"""
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"satelliteId": 101,
|
||||
"startTime": "2026-04-08T10:00:00",
|
||||
"endTime": "2026-04-08T10:05:00"
|
||||
}
|
||||
]
|
||||
""".trimIndent()
|
||||
)
|
||||
doReturn(response).`when`(complexMissionService).complexPlanRunModes(17L)
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/api/com-plan/runs/17/modes")
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java)
|
||||
.map(objectMapper::readTree)
|
||||
.block()!!
|
||||
|
||||
assertEquals(1, actual.size())
|
||||
assertEquals(101, actual[0]["satelliteId"].asInt())
|
||||
verify(complexMissionService).complexPlanRunModes(17L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `complex plan run delete endpoint proxies delete request`() {
|
||||
WebClient.create("http://localhost:$port")
|
||||
.delete()
|
||||
.uri("/api/com-plan/runs/17")
|
||||
.retrieve()
|
||||
.toBodilessEntity()
|
||||
.block()
|
||||
|
||||
verify(complexMissionService).deleteComplexPlanRun(17L)
|
||||
}
|
||||
|
||||
private fun anyRequest(): RequestDTO {
|
||||
any(RequestDTO::class.java)
|
||||
return RequestDTO()
|
||||
}
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
package space.nstart.pcp.slots_service.controller
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.ArgumentMatchers.anyList
|
||||
import org.mockito.ArgumentMatchers.eq
|
||||
import org.mockito.Mockito.doReturn
|
||||
import org.mockito.Mockito.verify
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.boot.test.web.server.LocalServerPort
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import reactor.core.publisher.Flux
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.DurationOnRevDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.IntervalDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.PositionDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RadioVisibilityAreaDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.StationDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TargetPositionDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteInfoDTO
|
||||
import space.nstart.pcp.slots_service.service.ComplexMissionService
|
||||
import space.nstart.pcp.slots_service.service.SlotService
|
||||
import space.nstart.pcp.slots_service.service.StationService
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@SpringBootTest(
|
||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||
properties = [
|
||||
"spring.cloud.config.enabled=false",
|
||||
"spring.cloud.config.import-check.enabled=false",
|
||||
"spring.boot.admin.client.enabled=false"
|
||||
]
|
||||
)
|
||||
class RvaControllerTest {
|
||||
|
||||
@LocalServerPort
|
||||
private var port: Int = 0
|
||||
|
||||
@MockitoBean
|
||||
private lateinit var complexMissionService: ComplexMissionService
|
||||
|
||||
@MockitoBean
|
||||
private lateinit var stationService: StationService
|
||||
|
||||
@MockitoBean
|
||||
private lateinit var slotService: SlotService
|
||||
|
||||
@Test
|
||||
fun `rva page is rendered`() {
|
||||
doReturn(Flux.just(SatelliteInfoDTO(noradId = 56756L, name = "Kondor"))).`when`(complexMissionService).satellites()
|
||||
doReturn(Flux.just(sampleStation())).`when`(stationService).stations()
|
||||
|
||||
val body = WebClient.create("http://localhost:$port")
|
||||
.get()
|
||||
.uri("/rva")
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java)
|
||||
.block()!!
|
||||
|
||||
assertTrue(body.contains("ЗРВ"))
|
||||
assertTrue(body.contains("Рассчитать"))
|
||||
assertTrue(body.contains("Скачать CSV"))
|
||||
assertTrue(body.contains("Суммарная длительность на витке"))
|
||||
assertTrue(body.contains("rva-station-label"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rva common endpoint proxies request`() {
|
||||
val station = sampleStation()
|
||||
val response = listOf(
|
||||
RadioVisibilityAreaDTO(
|
||||
stationId = 1L,
|
||||
revolution = 42L,
|
||||
onStart = TargetPositionDTO(time = LocalDateTime.of(2026, 4, 17, 12, 0)),
|
||||
onMaximum = TargetPositionDTO(time = LocalDateTime.of(2026, 4, 17, 12, 3)),
|
||||
onStop = TargetPositionDTO(time = LocalDateTime.of(2026, 4, 17, 12, 5)),
|
||||
duration = 300.0
|
||||
)
|
||||
)
|
||||
doReturn(response).`when`(slotService).rvaCommon(eq(56756L), eq(2L), anyStationList())
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.post()
|
||||
.uri("/api/rva/common/56756/2")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(listOf(station))
|
||||
.retrieve()
|
||||
.bodyToFlux(RadioVisibilityAreaDTO::class.java)
|
||||
.collectList()
|
||||
.block()!!
|
||||
|
||||
assertEquals(1, actual.size)
|
||||
assertEquals(42L, actual.first().revolution)
|
||||
verify(slotService).rvaCommon(eq(56756L), eq(2L), anyStationList())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rva merge endpoint proxies request`() {
|
||||
val station = sampleStation()
|
||||
val response = listOf(
|
||||
IntervalDTO(
|
||||
begin = LocalDateTime.of(2026, 4, 17, 12, 0),
|
||||
end = LocalDateTime.of(2026, 4, 17, 12, 10),
|
||||
durationSec = 600.0
|
||||
)
|
||||
)
|
||||
doReturn(response).`when`(slotService).rvaMerge(eq(56756L), eq(2L), anyStationList())
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.post()
|
||||
.uri("/api/rva/merge/56756/2")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(listOf(station))
|
||||
.retrieve()
|
||||
.bodyToFlux(IntervalDTO::class.java)
|
||||
.collectList()
|
||||
.block()!!
|
||||
|
||||
assertEquals(1, actual.size)
|
||||
assertEquals(600.0, actual.first().durationSec)
|
||||
verify(slotService).rvaMerge(eq(56756L), eq(2L), anyStationList())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rva merge on rev endpoint proxies request`() {
|
||||
val station = sampleStation()
|
||||
val response = listOf(
|
||||
DurationOnRevDTO(
|
||||
revolution = 42L,
|
||||
durationSec = 780.0
|
||||
)
|
||||
)
|
||||
doReturn(response).`when`(slotService).rvaMergeOnRev(eq(56756L), eq(2L), anyStationList())
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.post()
|
||||
.uri("/api/rva/merge-on-rev/56756/2")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(listOf(station))
|
||||
.retrieve()
|
||||
.bodyToFlux(DurationOnRevDTO::class.java)
|
||||
.collectList()
|
||||
.block()!!
|
||||
|
||||
assertEquals(1, actual.size)
|
||||
assertEquals(42L, actual.first().revolution)
|
||||
assertEquals(780.0, actual.first().durationSec)
|
||||
verify(slotService).rvaMergeOnRev(eq(56756L), eq(2L), anyStationList())
|
||||
}
|
||||
|
||||
private fun sampleStation() = StationDTO(
|
||||
number = 1,
|
||||
name = "Station 1",
|
||||
position = PositionDTO(
|
||||
lat = 55.75,
|
||||
long = 37.62,
|
||||
height = 200.0
|
||||
),
|
||||
elevationMin = 5.0,
|
||||
elevationMax = 90.0
|
||||
)
|
||||
|
||||
private fun anyStationList(): List<StationDTO> = anyList<StationDTO>() ?: emptyList()
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
package space.nstart.pcp.slots_service.rest
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito.doReturn
|
||||
import org.mockito.Mockito.verify
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
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_types_lib.dto.ballistics.RevolutionSign
|
||||
import space.nstart.pcp.slots_service.czml.CZMLColor
|
||||
import space.nstart.pcp.slots_service.czml.CZMLEntity
|
||||
import space.nstart.pcp.slots_service.czml.CZMLMaterial
|
||||
import space.nstart.pcp.slots_service.czml.CZMLPolygon
|
||||
import space.nstart.pcp.slots_service.czml.CZMLPosition
|
||||
import space.nstart.pcp.slots_service.service.CoverageSchemeModeRowDTO
|
||||
import space.nstart.pcp.slots_service.service.CoverageSchemeService
|
||||
import space.nstart.pcp.slots_service.service.CoverageSchemeUiResponseDTO
|
||||
import space.nstart.pcp.slots_service.service.EarthService
|
||||
import space.nstart.pcp.slots_service.service.ComplexMissionService
|
||||
import space.nstart.pcp.slots_service.service.SlotService
|
||||
import space.nstart.pcp.slots_service.service.StationService
|
||||
import tools.jackson.databind.ObjectMapper
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
@SpringBootTest(
|
||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||
properties = [
|
||||
"spring.cloud.config.enabled=false",
|
||||
"spring.cloud.config.import-check.enabled=false",
|
||||
"spring.boot.admin.client.enabled=false"
|
||||
]
|
||||
)
|
||||
class RequestRestControllerCoverageSchemeTest {
|
||||
|
||||
@LocalServerPort
|
||||
private var port: Int = 0
|
||||
|
||||
@MockitoBean
|
||||
private lateinit var earthService: EarthService
|
||||
|
||||
@MockitoBean
|
||||
private lateinit var complexMissionService: ComplexMissionService
|
||||
|
||||
@MockitoBean
|
||||
private lateinit var stationService: StationService
|
||||
|
||||
@MockitoBean
|
||||
private lateinit var slotService: SlotService
|
||||
|
||||
@MockitoBean
|
||||
private lateinit var coverageSchemeService: CoverageSchemeService
|
||||
|
||||
private val objectMapper = ObjectMapper()
|
||||
|
||||
@Test
|
||||
fun `request coverage scheme endpoint returns map and table payload`() {
|
||||
val requestId = "req-scheme"
|
||||
val timeStart = LocalDateTime.of(2026, 4, 14, 10, 0)
|
||||
val timeStop = LocalDateTime.of(2026, 4, 14, 12, 0)
|
||||
val satellites = listOf(11L, 22L)
|
||||
val rollStepDegrees = 2.5
|
||||
val minimumTechnologyOverlap = 0.1
|
||||
val strictContinuousCoverage = false
|
||||
val allowPartialCoverage = true
|
||||
val orientationToleranceDegrees = 7.5
|
||||
val includeDebugInfo = false
|
||||
|
||||
doReturn(
|
||||
CoverageSchemeUiResponseDTO(
|
||||
cov = listOf(
|
||||
CZMLEntity("document", "req_cov_scheme_$requestId", "1.1"),
|
||||
CZMLEntity(
|
||||
id = "coverage_mode_${requestId}_11_1",
|
||||
name = "Режим схемы покрытия",
|
||||
polygon = CZMLPolygon(
|
||||
positions = CZMLPosition(cartographicDegrees = listOf(30.0, 10.0, 0.0, 40.0, 40.0, 0.0, 20.0, 40.0, 0.0, 30.0, 10.0, 0.0)),
|
||||
outlineColor = CZMLColor(12, 34, 56, 255),
|
||||
material = CZMLMaterial(12, 34, 56, 150)
|
||||
)
|
||||
)
|
||||
),
|
||||
slots = listOf(
|
||||
CoverageSchemeModeRowDTO(
|
||||
entityId = "coverage_mode_${requestId}_11_1",
|
||||
sequence = 1,
|
||||
satelliteId = 11L,
|
||||
tn = timeStart,
|
||||
tk = timeStart.plusMinutes(5),
|
||||
roll = 12.5,
|
||||
revolutionSign = RevolutionSign.ASC,
|
||||
contour = "POLYGON((30 10, 40 40, 20 40, 30 10))",
|
||||
selectionReason = "seed from WEST"
|
||||
)
|
||||
)
|
||||
)
|
||||
).`when`(coverageSchemeService).requestCoverageScheme(
|
||||
requestId,
|
||||
timeStart,
|
||||
timeStop,
|
||||
satellites,
|
||||
rollStepDegrees,
|
||||
minimumTechnologyOverlap,
|
||||
strictContinuousCoverage,
|
||||
allowPartialCoverage,
|
||||
orientationToleranceDegrees,
|
||||
includeDebugInfo
|
||||
)
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.post()
|
||||
.uri(
|
||||
"/requests/req-with-coverage-scheme/$requestId" +
|
||||
"?tn=2026-04-14T10:00:00" +
|
||||
"&tk=2026-04-14T12:00:00" +
|
||||
"&rollStepDegrees=2.5" +
|
||||
"&minimumTechnologyOverlap=0.1" +
|
||||
"&strictContinuousCoverage=false" +
|
||||
"&allowPartialCoverage=true" +
|
||||
"&orientationToleranceDegrees=7.5" +
|
||||
"&includeDebugInfo=false" +
|
||||
"&satellites=11" +
|
||||
"&satellites=22"
|
||||
)
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java)
|
||||
.map(objectMapper::readTree)
|
||||
.block()!!
|
||||
|
||||
assertEquals(2, actual["cov"].size())
|
||||
assertEquals(1, actual["slots"].size())
|
||||
assertEquals(1, actual["slots"][0]["sequence"].asInt())
|
||||
assertEquals(11L, actual["slots"][0]["satelliteId"].asLong())
|
||||
assertNotNull(actual["slots"][0]["entityId"])
|
||||
verify(coverageSchemeService).requestCoverageScheme(
|
||||
requestId,
|
||||
timeStart,
|
||||
timeStop,
|
||||
satellites,
|
||||
rollStepDegrees,
|
||||
minimumTechnologyOverlap,
|
||||
strictContinuousCoverage,
|
||||
allowPartialCoverage,
|
||||
orientationToleranceDegrees,
|
||||
includeDebugInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
package space.nstart.pcp.slots_service.rest
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito.doReturn
|
||||
import org.mockito.Mockito.verify
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
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 reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotStatus
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteInfoDTO
|
||||
import space.nstart.pcp.slots_service.service.EarthService
|
||||
import space.nstart.pcp.slots_service.service.ComplexMissionService
|
||||
import space.nstart.pcp.slots_service.service.SlotService
|
||||
import space.nstart.pcp.slots_service.service.StationService
|
||||
import tools.jackson.databind.ObjectMapper
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
@SpringBootTest(
|
||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||
properties = [
|
||||
"spring.cloud.config.enabled=false",
|
||||
"spring.cloud.config.import-check.enabled=false",
|
||||
"spring.boot.admin.client.enabled=false"
|
||||
]
|
||||
)
|
||||
class RequestRestControllerCoverageSunTest {
|
||||
|
||||
@LocalServerPort
|
||||
private var port: Int = 0
|
||||
|
||||
@MockitoBean
|
||||
private lateinit var earthService: EarthService
|
||||
|
||||
@MockitoBean
|
||||
private lateinit var complexMissionService: ComplexMissionService
|
||||
|
||||
@MockitoBean
|
||||
private lateinit var stationService: StationService
|
||||
|
||||
@MockitoBean
|
||||
private lateinit var slotService: SlotService
|
||||
|
||||
private val objectMapper = ObjectMapper()
|
||||
|
||||
@Test
|
||||
fun `request coverage forwards sun parameter to slot service`() {
|
||||
val requestId = "req-1"
|
||||
val timeStart = LocalDateTime.of(2026, 4, 9, 10, 0)
|
||||
val timeStop = LocalDateTime.of(2026, 4, 9, 12, 0)
|
||||
val satellites = listOf(11L, 22L)
|
||||
val sun = -45.5
|
||||
|
||||
doReturn(Flux.empty<SlotDTO>())
|
||||
.`when`(slotService)
|
||||
.slots(requestId, timeStart, timeStop, RevolutionSign.ASC, true, satellites, sun)
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.post()
|
||||
.uri(
|
||||
"/requests/req-with-covs/$requestId" +
|
||||
"?tn=2026-04-09T10:00:00" +
|
||||
"&tk=2026-04-09T12:00:00" +
|
||||
"&sign=ASC" +
|
||||
"&cov=true" +
|
||||
"&sun=-45.5" +
|
||||
"&satellites=11" +
|
||||
"&satellites=22"
|
||||
)
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java)
|
||||
.map(objectMapper::readTree)
|
||||
.block()!!
|
||||
|
||||
assertEquals(0, actual["slots"].size())
|
||||
verify(slotService).slots(requestId, timeStart, timeStop, RevolutionSign.ASC, true, satellites, sun)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `request coverage uses satellite color for slot polygons`() {
|
||||
val requestId = "req-color"
|
||||
val timeStart = LocalDateTime.of(2026, 4, 9, 10, 0)
|
||||
val timeStop = LocalDateTime.of(2026, 4, 9, 12, 0)
|
||||
val slot = SlotDTO(
|
||||
cycle = 7,
|
||||
satelliteId = 11L,
|
||||
tn = timeStart,
|
||||
tk = timeStart.plusMinutes(10),
|
||||
roll = 12.5,
|
||||
contour = "POLYGON((30 10, 40 40, 20 40, 10 20, 30 10))",
|
||||
revolution = 3,
|
||||
revolutionSign = RevolutionSign.ASC,
|
||||
slotNumber = 4,
|
||||
state = SlotStatus.BOOKED
|
||||
)
|
||||
|
||||
doReturn(Flux.just(slot))
|
||||
.`when`(slotService)
|
||||
.slots(requestId, timeStart, timeStop, null, true, null, null)
|
||||
|
||||
doReturn(
|
||||
Mono.just(
|
||||
SatelliteInfoDTO(
|
||||
noradId = 11L,
|
||||
name = "SAT-11",
|
||||
red = 12,
|
||||
green = 34,
|
||||
blue = 56,
|
||||
scanTLE = false
|
||||
)
|
||||
)
|
||||
)
|
||||
.`when`(complexMissionService)
|
||||
.satellite(11L)
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.post()
|
||||
.uri(
|
||||
"/requests/req-with-covs/$requestId" +
|
||||
"?tn=2026-04-09T10:00:00" +
|
||||
"&tk=2026-04-09T12:00:00" +
|
||||
"&cov=true"
|
||||
)
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java)
|
||||
.map(objectMapper::readTree)
|
||||
.block()!!
|
||||
|
||||
val slotEntity = actual["cov"]?.get(1)
|
||||
assertNotNull(slotEntity)
|
||||
assertEquals(listOf(12, 34, 56, 255), slotEntity["polygon"]["outlineColor"]["rgba"].map { it.asInt() })
|
||||
assertEquals(
|
||||
listOf(12, 34, 56, 150),
|
||||
slotEntity["polygon"]["material"]["solidColor"]["color"]["rgba"].map { it.asInt() }
|
||||
)
|
||||
verify(complexMissionService).satellite(11L)
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package space.nstart.pcp.slots_service.rest
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito.doReturn
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.verifyNoInteractions
|
||||
import org.springframework.test.util.ReflectionTestUtils
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteInfoDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SatelliteModeResponseDTO
|
||||
import space.nstart.pcp.slots_service.czml.CZMLEntity
|
||||
import space.nstart.pcp.slots_service.service.CZMLService
|
||||
import space.nstart.pcp.slots_service.service.ComplexMissionService
|
||||
import space.nstart.pcp.slots_service.service.SlotService
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class RequestRestControllerSatFldTest {
|
||||
|
||||
@Test
|
||||
fun `plan endpoint forwards runId to satellite plan`() {
|
||||
val czmlService = mock(CZMLService::class.java)
|
||||
val controller = RequestRestController()
|
||||
val timeStart = LocalDateTime.of(2026, 4, 22, 10, 0)
|
||||
val timeStop = LocalDateTime.of(2026, 4, 22, 12, 0)
|
||||
|
||||
doReturn(emptyList<CZMLEntity>()).`when`(czmlService)
|
||||
.showSatPlan(101L, timeStart, timeStop, 17L)
|
||||
ReflectionTestUtils.setField(controller, "czmlService", czmlService)
|
||||
|
||||
val result = controller.plan(
|
||||
id = 101L,
|
||||
tn = timeStart,
|
||||
tk = timeStop,
|
||||
runId = 17L
|
||||
)
|
||||
|
||||
assertTrue(result.none())
|
||||
verify(czmlService).showSatPlan(101L, timeStart, timeStop, 17L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sat endpoint forwards time interval to satFLD`() {
|
||||
val czmlService = mock(CZMLService::class.java)
|
||||
val controller = RequestRestController()
|
||||
val timeStart = LocalDateTime.of(2026, 4, 21, 10, 0)
|
||||
val timeStop = LocalDateTime.of(2026, 4, 21, 12, 0)
|
||||
|
||||
doReturn(emptyList<CZMLEntity>()).`when`(czmlService)
|
||||
.satFLD(101L, false, true, true, true, timeStart, timeStop)
|
||||
ReflectionTestUtils.setField(controller, "czmlService", czmlService)
|
||||
|
||||
val result = controller.sat(
|
||||
id = 101L,
|
||||
view = false,
|
||||
orbit = true,
|
||||
sw = true,
|
||||
fl = true,
|
||||
tn = timeStart,
|
||||
tk = timeStop
|
||||
)
|
||||
|
||||
assertTrue(result.none())
|
||||
verify(czmlService).satFLD(101L, false, true, true, true, timeStart, timeStop)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `satellite plan czml does not depend on slots service`() {
|
||||
val service = CZMLService()
|
||||
val complexMissionService = mock(ComplexMissionService::class.java)
|
||||
val slotService = mock(SlotService::class.java)
|
||||
val timeStart = LocalDateTime.of(2026, 5, 18, 15, 20)
|
||||
val timeStop = LocalDateTime.of(2026, 6, 1, 15, 20)
|
||||
|
||||
doReturn(Mono.just(SatelliteInfoDTO(noradId = 31L, name = "SAT-31")))
|
||||
.`when`(complexMissionService)
|
||||
.satellite(31L)
|
||||
doReturn(Flux.empty<SatelliteModeResponseDTO>())
|
||||
.`when`(complexMissionService)
|
||||
.plan(31L, timeStart, timeStop, null)
|
||||
|
||||
ReflectionTestUtils.setField(service, "complexMissionService", complexMissionService)
|
||||
ReflectionTestUtils.setField(service, "slotService", slotService)
|
||||
|
||||
val result = service.showSatPlan(31L, timeStart, timeStop).toList()
|
||||
|
||||
assertEquals("document", result.first().id)
|
||||
verifyNoInteractions(slotService)
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package space.nstart.pcp.slots_service.service
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange
|
||||
import com.sun.net.httpserver.HttpServer
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
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.ballistics.OrbPointDTO
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.URI
|
||||
import java.time.LocalDateTime
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
class BallisticsServiceTest {
|
||||
|
||||
private var server: HttpServer? = null
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
server?.stop(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sat data methods forward time interval to ballistics service`() {
|
||||
val requests = CopyOnWriteArrayList<URI>()
|
||||
val serverUrl = startServer(requests)
|
||||
val service = createService(serverUrl)
|
||||
val timeStart = LocalDateTime.of(2026, 4, 21, 10, 0)
|
||||
val timeStop = LocalDateTime.of(2026, 4, 21, 12, 0)
|
||||
|
||||
service.ascNodes(101L, timeStart, timeStop).collectList().block()
|
||||
service.flightLine(101L, timeStart, timeStop).collectList().block()
|
||||
service.points(101L, timeStart, timeStop).collectList().block()
|
||||
val availability = service.orbitAvailability()
|
||||
val exactPoint = service.exactTimePoint(101L, timeStart)
|
||||
|
||||
assertEquals(
|
||||
listOf(
|
||||
"/api/satellites/101/asc-node",
|
||||
"/api/satellites/101/flight-line",
|
||||
"/api/satellites/101/orbit",
|
||||
"/api/satellites/orbit/availability",
|
||||
"/api/satellites/101/extract-time"
|
||||
),
|
||||
requests.map { it.path }
|
||||
)
|
||||
assertEquals(
|
||||
listOf(
|
||||
"time_start=2026-04-21T10:00&time_stop=2026-04-21T12:00",
|
||||
"time_start=2026-04-21T10:00&time_stop=2026-04-21T12:00",
|
||||
"time_start=2026-04-21T10:00&time_stop=2026-04-21T12:00",
|
||||
null,
|
||||
null
|
||||
),
|
||||
requests.map { it.query }
|
||||
)
|
||||
assertEquals(1, availability.size)
|
||||
assertEquals(101L, availability.single().satelliteId)
|
||||
assertNotNull(exactPoint)
|
||||
assertEquals(timeStart, exactPoint.time)
|
||||
}
|
||||
|
||||
private fun createService(serverUrl: String): BallisticsService {
|
||||
val provider = object : ObjectProvider<WebClient.Builder> {
|
||||
override fun getObject(vararg args: Any?): WebClient.Builder = WebClient.builder()
|
||||
override fun getObject(): WebClient.Builder = WebClient.builder()
|
||||
override fun getIfAvailable(): WebClient.Builder = WebClient.builder()
|
||||
override fun getIfUnique(): WebClient.Builder = WebClient.builder()
|
||||
override fun iterator(): MutableIterator<WebClient.Builder> = mutableListOf<WebClient.Builder>().iterator()
|
||||
}
|
||||
val service = BallisticsService(provider)
|
||||
ReflectionTestUtils.setField(service, "url", serverUrl)
|
||||
return service
|
||||
}
|
||||
|
||||
private fun startServer(requests: MutableList<URI>): String {
|
||||
val startedServer = HttpServer.create(InetSocketAddress(0), 0)
|
||||
listOf(
|
||||
"/api/satellites/101/asc-node",
|
||||
"/api/satellites/101/flight-line",
|
||||
"/api/satellites/101/orbit"
|
||||
).forEach { path ->
|
||||
startedServer.createContext(path) { exchange ->
|
||||
requests.add(exchange.requestURI)
|
||||
respond(exchange, "[]")
|
||||
}
|
||||
}
|
||||
startedServer.createContext("/api/satellites/orbit/availability") { exchange ->
|
||||
requests.add(exchange.requestURI)
|
||||
respond(
|
||||
exchange,
|
||||
"""[{"satelliteId":101,"timeStart":"2026-04-21T10:00:00","timeStop":"2026-04-21T12:00:00"}]"""
|
||||
)
|
||||
}
|
||||
startedServer.createContext("/api/satellites/101/extract-time") { exchange ->
|
||||
requests.add(exchange.requestURI)
|
||||
respond(
|
||||
exchange,
|
||||
"""[{"time":"2026-04-21T10:00:00","revolution":1,"vx":1.0,"vy":2.0,"vz":3.0,"x":4.0,"y":5.0,"z":6.0}]"""
|
||||
)
|
||||
}
|
||||
startedServer.start()
|
||||
server = startedServer
|
||||
return "http://localhost:${startedServer.address.port}"
|
||||
}
|
||||
|
||||
private fun respond(exchange: HttpExchange, body: String) {
|
||||
exchange.responseHeaders.add("Content-Type", "application/json")
|
||||
val bytes = body.toByteArray()
|
||||
exchange.sendResponseHeaders(200, bytes.size.toLong())
|
||||
exchange.responseBody.use { output -> output.write(bytes) }
|
||||
}
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
package space.nstart.pcp.slots_service.service
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange
|
||||
import com.sun.net.httpserver.HttpServer
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.ObjectProvider
|
||||
import org.springframework.test.util.ReflectionTestUtils
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import space.nstart.pcp.slots_service.configuration.WebClientConfig
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.URI
|
||||
import java.time.LocalDateTime
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class ComplexMissionServiceTest {
|
||||
|
||||
private var server: HttpServer? = null
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
server?.stop(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `complex plan run methods call complex mission endpoints`() {
|
||||
val requests = CopyOnWriteArrayList<URI>()
|
||||
val methods = CopyOnWriteArrayList<String>()
|
||||
val serverUrl = startServer(requests, methods)
|
||||
val service = createService(serverUrl)
|
||||
|
||||
val runs = service.complexPlanRuns()
|
||||
val run = service.complexPlanRun(17L)
|
||||
val modes = service.complexPlanRunModes(17L)
|
||||
service.deleteComplexPlanRun(17L)
|
||||
|
||||
assertEquals(1, runs.size())
|
||||
assertEquals(17, runs[0]["runId"].asInt())
|
||||
assertEquals(17, run["runId"].asInt())
|
||||
assertEquals(0, modes.size())
|
||||
assertEquals(
|
||||
listOf(
|
||||
"/api/com-plan/runs",
|
||||
"/api/com-plan/runs/17",
|
||||
"/api/com-plan/runs/17/modes",
|
||||
"/api/com-plan/runs/17"
|
||||
),
|
||||
requests.map { it.path }
|
||||
)
|
||||
assertEquals(listOf("GET", "GET", "GET", "DELETE"), methods)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `complex plan run modes supports report payloads larger than previous webclient buffer`() {
|
||||
val itemPayload = "x".repeat(1000)
|
||||
val itemCount = 22_000
|
||||
val modesBody = buildString {
|
||||
append("[")
|
||||
repeat(itemCount) { index ->
|
||||
if (index > 0) {
|
||||
append(",")
|
||||
}
|
||||
append("""{"slotNumber":$index,"payload":"$itemPayload"}""")
|
||||
}
|
||||
append("]")
|
||||
}
|
||||
val serverUrl = startServer(
|
||||
requests = CopyOnWriteArrayList(),
|
||||
modesBody = modesBody
|
||||
)
|
||||
val service = createService(serverUrl) {
|
||||
WebClientConfig(32 * 1024 * 1024).webClientBuilder()
|
||||
}
|
||||
|
||||
val modes = service.complexPlanRunModes(17L)
|
||||
|
||||
assertEquals(itemCount, modes.size())
|
||||
assertEquals(itemPayload.length, modes[0]["payload"].asText().length)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `plan calls satellite scoped modes endpoint with runId`() {
|
||||
val requests = CopyOnWriteArrayList<URI>()
|
||||
val serverUrl = startServer(requests)
|
||||
val service = createService(serverUrl)
|
||||
val intervalStart = LocalDateTime.of(2026, 4, 22, 10, 0)
|
||||
val intervalEnd = LocalDateTime.of(2026, 4, 22, 12, 0)
|
||||
|
||||
val response = service.plan(101L, intervalStart, intervalEnd, 17L)
|
||||
.collectList()
|
||||
.block()
|
||||
.orEmpty()
|
||||
|
||||
assertEquals(emptyList(), response)
|
||||
assertEquals("/api/satellites/101/modes", requests.single().path)
|
||||
assertEquals(
|
||||
"intervalStart=2026-04-22T10:00&intervalEnd=2026-04-22T12:00&runId=17",
|
||||
requests.single().rawQuery
|
||||
)
|
||||
}
|
||||
|
||||
private fun createService(
|
||||
serverUrl: String,
|
||||
builderFactory: () -> WebClient.Builder = { WebClient.builder() }
|
||||
): ComplexMissionService {
|
||||
val provider = object : ObjectProvider<WebClient.Builder> {
|
||||
override fun getObject(vararg args: Any?): WebClient.Builder = builderFactory()
|
||||
override fun getObject(): WebClient.Builder = builderFactory()
|
||||
override fun getIfAvailable(): WebClient.Builder = builderFactory()
|
||||
override fun getIfUnique(): WebClient.Builder = builderFactory()
|
||||
override fun iterator(): MutableIterator<WebClient.Builder> = mutableListOf<WebClient.Builder>().iterator()
|
||||
}
|
||||
val service = ComplexMissionService(provider)
|
||||
ReflectionTestUtils.setField(service, "complexMissionUrl", serverUrl)
|
||||
ReflectionTestUtils.setField(service, "satelliteCatalogUrl", serverUrl)
|
||||
return service
|
||||
}
|
||||
|
||||
private fun startServer(
|
||||
requests: MutableList<URI>,
|
||||
methods: MutableList<String> = mutableListOf(),
|
||||
modesBody: String = "[]"
|
||||
): String {
|
||||
val startedServer = HttpServer.create(InetSocketAddress(0), 0)
|
||||
startedServer.createContext("/api/com-plan/runs") { exchange ->
|
||||
requests.add(exchange.requestURI)
|
||||
methods.add(exchange.requestMethod)
|
||||
if (exchange.requestMethod == "DELETE") {
|
||||
respondNoContent(exchange)
|
||||
} else {
|
||||
respond(
|
||||
exchange,
|
||||
if (exchange.requestURI.path.endsWith("/modes")) {
|
||||
modesBody
|
||||
} else if (exchange.requestURI.path.endsWith("/17")) {
|
||||
"""{"runId":17,"status":"COMPLETED"}"""
|
||||
} else {
|
||||
"""[{"runId":17,"status":"COMPLETED"}]"""
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
startedServer.createContext("/api/satellites/101/modes") { exchange ->
|
||||
requests.add(exchange.requestURI)
|
||||
methods.add(exchange.requestMethod)
|
||||
respond(exchange, "[]")
|
||||
}
|
||||
startedServer.start()
|
||||
server = startedServer
|
||||
return "http://localhost:${startedServer.address.port}"
|
||||
}
|
||||
|
||||
private fun respond(exchange: HttpExchange, body: String) {
|
||||
exchange.responseHeaders.add("Content-Type", "application/json")
|
||||
val bytes = body.toByteArray()
|
||||
exchange.sendResponseHeaders(200, bytes.size.toLong())
|
||||
exchange.responseBody.use { output -> output.write(bytes) }
|
||||
}
|
||||
|
||||
private fun respondNoContent(exchange: HttpExchange) {
|
||||
exchange.sendResponseHeaders(204, -1)
|
||||
exchange.close()
|
||||
}
|
||||
}
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
package space.nstart.pcp.slots_service.service
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange
|
||||
import com.sun.net.httpserver.HttpHandler
|
||||
import com.sun.net.httpserver.HttpServer
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
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.ballistics.RevolutionSign
|
||||
import space.nstart.pcp.pcp_types_lib.dto.coverage.CoverageSchemeResponseDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SurveySlotDTO
|
||||
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
|
||||
import space.nstart.pcp.slots_service.configuration.CustomErrorException
|
||||
import tools.jackson.databind.ObjectMapper
|
||||
import java.net.InetSocketAddress
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
class CoverageSchemeServiceTest {
|
||||
|
||||
private val objectMapper = ObjectMapper()
|
||||
private var server: HttpServer? = null
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
server?.stop(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `requestCoverageScheme maps coverageScheme slots to rows and czml`() {
|
||||
val timeStart = LocalDateTime.of(2026, 4, 14, 10, 0)
|
||||
val timeStop = LocalDateTime.of(2026, 4, 14, 12, 0)
|
||||
val serverUrl = startServer(
|
||||
mapOf(
|
||||
"/v1/requests/req-1/with-cells" to stringHandler(200, requestWithCellsJson()),
|
||||
"/api/coverage-schemes/calculate" to jsonHandler(
|
||||
CoverageSchemeResponseDTO(
|
||||
targetPolygonWkt = "POLYGON ((30 10, 40 40, 20 40, 30 10))",
|
||||
coverageScheme = listOf(
|
||||
SurveySlotDTO(
|
||||
satelliteId = 11L,
|
||||
tn = timeStart,
|
||||
tk = timeStart.plusMinutes(5),
|
||||
roll = 0.0,
|
||||
contour = "POLYGON((30 10, 40 40, 20 40, 30 10))",
|
||||
revolutionSign = RevolutionSign.ASC
|
||||
),
|
||||
SurveySlotDTO(
|
||||
satelliteId = 11L,
|
||||
tn = timeStart,
|
||||
tk = timeStart.plusMinutes(5),
|
||||
roll = 0.0,
|
||||
contour = "POLYGON((31 11, 32 12, 30 12, 31 11))",
|
||||
revolutionSign = RevolutionSign.ASC
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
"/api/satellites/11" to jsonHandler(
|
||||
SatelliteDTO(
|
||||
id = 11L,
|
||||
code = "SAT-11",
|
||||
name = "Sat-11",
|
||||
typeCode = "TEST",
|
||||
visualization = SatelliteVisualizationDTO(
|
||||
red = 12,
|
||||
green = 34,
|
||||
blue = 56
|
||||
),
|
||||
observationProfile = SatelliteObservationProfileDTO()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val service = createService(serverUrl)
|
||||
|
||||
val response = service.requestCoverageScheme(
|
||||
requestId = "req-1",
|
||||
timeStart = timeStart,
|
||||
timeStop = timeStop,
|
||||
satelliteIds = listOf(11L, 22L),
|
||||
rollStepDegrees = 2.5,
|
||||
minimumTechnologyOverlap = 0.1,
|
||||
strictContinuousCoverage = false,
|
||||
allowPartialCoverage = true,
|
||||
orientationToleranceDegrees = 7.5,
|
||||
includeDebugInfo = false
|
||||
)
|
||||
|
||||
assertEquals(2, response.slots.size)
|
||||
assertEquals(3, response.cov.size)
|
||||
assertEquals("document", response.cov.first().id)
|
||||
assertEquals(1, response.slots.first().sequence)
|
||||
assertEquals(2, response.slots.last().sequence)
|
||||
assertEquals(11L, response.slots.first().satelliteId)
|
||||
assertEquals(0.0, response.slots.first().roll)
|
||||
assertEquals(0.0, response.slots.last().roll)
|
||||
assertEquals(RevolutionSign.ASC, response.slots.first().revolutionSign)
|
||||
assertNotNull(response.cov[1].polygon)
|
||||
assertEquals(listOf(12, 34, 56, 255), response.cov[1].polygon?.outlineColor?.rgba?.toList())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `requestCoverageScheme converts backend error to CustomErrorException`() {
|
||||
val serverUrl = startServer(
|
||||
mapOf(
|
||||
"/v1/requests/req-1/with-cells" to stringHandler(200, requestWithCellsJson()),
|
||||
"/api/coverage-schemes/calculate" to stringHandler(400, """{"message":"coverage failed"}""")
|
||||
)
|
||||
)
|
||||
|
||||
val service = createService(serverUrl)
|
||||
|
||||
val error = assertFailsWith<CustomErrorException> {
|
||||
service.requestCoverageScheme(
|
||||
requestId = "req-1",
|
||||
timeStart = LocalDateTime.of(2026, 4, 14, 10, 0),
|
||||
timeStop = LocalDateTime.of(2026, 4, 14, 12, 0),
|
||||
satelliteIds = listOf(11L),
|
||||
rollStepDegrees = null,
|
||||
minimumTechnologyOverlap = null,
|
||||
strictContinuousCoverage = null,
|
||||
allowPartialCoverage = null,
|
||||
orientationToleranceDegrees = null,
|
||||
includeDebugInfo = null
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals("coverage failed", error.message)
|
||||
}
|
||||
|
||||
private fun createService(serverUrl: String): CoverageSchemeService {
|
||||
val provider = object : ObjectProvider<WebClient.Builder> {
|
||||
override fun getObject(vararg args: Any?): WebClient.Builder = WebClient.builder()
|
||||
override fun getObject(): WebClient.Builder = WebClient.builder()
|
||||
override fun getIfAvailable(): WebClient.Builder = WebClient.builder()
|
||||
override fun getIfUnique(): WebClient.Builder = WebClient.builder()
|
||||
override fun iterator(): MutableIterator<WebClient.Builder> = mutableListOf<WebClient.Builder>().iterator()
|
||||
}
|
||||
val service = CoverageSchemeService(provider)
|
||||
val earthService = EarthService(provider)
|
||||
val complexMissionService = ComplexMissionService(provider)
|
||||
|
||||
ReflectionTestUtils.setField(service, "earthService", earthService)
|
||||
ReflectionTestUtils.setField(service, "complexMissionService", complexMissionService)
|
||||
ReflectionTestUtils.setField(service, "url", serverUrl)
|
||||
ReflectionTestUtils.setField(earthService, "url", serverUrl)
|
||||
ReflectionTestUtils.setField(complexMissionService, "complexMissionUrl", serverUrl)
|
||||
ReflectionTestUtils.setField(complexMissionService, "satelliteCatalogUrl", serverUrl)
|
||||
return service
|
||||
}
|
||||
|
||||
private fun startServer(handlers: Map<String, HttpHandler>): String {
|
||||
val startedServer = HttpServer.create(InetSocketAddress(0), 0)
|
||||
handlers.forEach { (path, handler) ->
|
||||
startedServer.createContext(path, handler)
|
||||
}
|
||||
startedServer.start()
|
||||
server = startedServer
|
||||
return "http://localhost:${startedServer.address.port}"
|
||||
}
|
||||
|
||||
private fun jsonHandler(body: Any) = HttpHandler { exchange ->
|
||||
respond(exchange, 200, objectMapper.writeValueAsString(body))
|
||||
}
|
||||
|
||||
private fun stringHandler(status: Int, body: String) = HttpHandler { exchange ->
|
||||
respond(exchange, status, body)
|
||||
}
|
||||
|
||||
private fun requestWithCellsJson(): String =
|
||||
"""
|
||||
{
|
||||
"request": {
|
||||
"id": "11111111-1111-4111-8111-111111111111",
|
||||
"name": "coverage scheme request",
|
||||
"status": "ACCEPTED",
|
||||
"surveyType": "OPTICS",
|
||||
"geometry": "POLYGON ((30 10, 40 40, 20 40, 30 10))",
|
||||
"importance": 10.0,
|
||||
"beginDateTime": "2026-01-01T00:00:00Z",
|
||||
"endDateTime": "2026-01-02T00:00:00Z",
|
||||
"kpp": [],
|
||||
"highPriorityTransmit": false,
|
||||
"optics": null,
|
||||
"rsa": null,
|
||||
"coverage": {
|
||||
"requiredPercent": 100.0,
|
||||
"currentPercent": 0.0
|
||||
},
|
||||
"createdAt": "2026-01-01T00:00:00Z",
|
||||
"updatedAt": "2026-01-01T00:00:00Z",
|
||||
"deletedAt": null
|
||||
},
|
||||
"cells": []
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
private fun respond(exchange: HttpExchange, status: Int, body: String) {
|
||||
exchange.responseHeaders.add("Content-Type", "application/json")
|
||||
val bytes = body.toByteArray()
|
||||
exchange.sendResponseHeaders(status, bytes.size.toLong())
|
||||
exchange.responseBody.use { output -> output.write(bytes) }
|
||||
}
|
||||
}
|
||||
+543
@@ -0,0 +1,543 @@
|
||||
package space.nstart.pcp.slots_service.service
|
||||
|
||||
import com.sun.net.httpserver.HttpServer
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.ObjectProvider
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.web.reactive.function.client.ClientResponse
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import org.springframework.web.reactive.function.client.WebClientResponseException
|
||||
import reactor.core.publisher.Mono
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.RequestDTO
|
||||
import tools.jackson.databind.ObjectMapper
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.URI
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class EarthServiceTest {
|
||||
private val objectMapper = ObjectMapper()
|
||||
|
||||
@Test
|
||||
fun `cells calls v1 cells with min importance`() {
|
||||
val requestedUris = mutableListOf<URI>()
|
||||
val service = earthService(
|
||||
requestedUris = requestedUris,
|
||||
responses = mutableListOf(cellsResponse())
|
||||
)
|
||||
|
||||
service.cells(3.5).toList()
|
||||
|
||||
assertEquals("/v1/cells", requestedUris.single().path)
|
||||
assertEquals("minImportance=3.5&page=0&size=500", requestedUris.single().query)
|
||||
assertFalse(requestedUris.single().path.startsWith("/api/earth-grid"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cells maps response wrapper items to earth cell dto`() {
|
||||
val service = earthService(
|
||||
responses = mutableListOf(
|
||||
cellsResponse(
|
||||
"""
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"cellNum": 42,
|
||||
"latitude": 55.0,
|
||||
"longitude": 37.0,
|
||||
"importance": 10.0,
|
||||
"contour": "POLYGON ((36 55, 37 55, 37 56, 36 56, 36 55))"
|
||||
}
|
||||
],
|
||||
"page": 0,
|
||||
"size": 500,
|
||||
"totalItems": 1,
|
||||
"totalPages": 1
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val cell = service.cells(0.0).single()
|
||||
|
||||
assertEquals(42L, cell.id)
|
||||
assertEquals(42L, cell.num)
|
||||
assertEquals(55.0, cell.latitude)
|
||||
assertEquals(37.0, cell.longitude)
|
||||
assertEquals(10.0, cell.importance)
|
||||
assertEquals("POLYGON ((36 55, 37 55, 37 56, 36 56, 36 55))", cell.contour)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cells does not pass count lat and count long to v1 cells`() {
|
||||
val requestedUris = mutableListOf<URI>()
|
||||
val service = earthService(
|
||||
requestedUris = requestedUris,
|
||||
responses = mutableListOf(cellsResponse())
|
||||
)
|
||||
|
||||
service.cells(1.0, countLat = 2, countLong = 3).toList()
|
||||
|
||||
val query = requestedUris.single().query
|
||||
assertFalse(query.contains("countLat"))
|
||||
assertFalse(query.contains("countLong"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cells fetches all pages from v1 cells`() {
|
||||
val requestedUris = mutableListOf<URI>()
|
||||
val service = earthService(
|
||||
requestedUris = requestedUris,
|
||||
responses = mutableListOf(
|
||||
cellsResponse(
|
||||
"""
|
||||
{
|
||||
"items": [
|
||||
{"cellNum": 1, "latitude": 1.0, "longitude": 2.0, "importance": 3.0, "contour": "POLYGON EMPTY"}
|
||||
],
|
||||
"page": 0,
|
||||
"size": 500,
|
||||
"totalItems": 2,
|
||||
"totalPages": 2
|
||||
}
|
||||
""".trimIndent()
|
||||
),
|
||||
cellsResponse(
|
||||
"""
|
||||
{
|
||||
"items": [
|
||||
{"cellNum": 2, "latitude": 4.0, "longitude": 5.0, "importance": 6.0, "contour": "POLYGON EMPTY"}
|
||||
],
|
||||
"page": 1,
|
||||
"size": 500,
|
||||
"totalItems": 2,
|
||||
"totalPages": 2
|
||||
}
|
||||
""".trimIndent()
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
val cells = service.cells(0.0).toList()
|
||||
|
||||
assertEquals(listOf(1L, 2L), cells.map { cell -> cell.num })
|
||||
assertEquals(listOf("page=0", "page=1"), requestedUris.map { uri -> uri.query.substringAfter("minImportance=0.0&").substringBefore("&size") })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reqs calls v1 requests with page and size instead of legacy api requests`() {
|
||||
val requestedUris = mutableListOf<URI>()
|
||||
val service = earthService(
|
||||
requestedUris = requestedUris,
|
||||
responses = mutableListOf(requestsResponse())
|
||||
)
|
||||
|
||||
service.reqs().collectList().block()
|
||||
|
||||
assertEquals("/v1/requests", requestedUris.single().path)
|
||||
assertEquals("page=0&size=500", requestedUris.single().query)
|
||||
assertFalse(requestedUris.any { uri -> uri.path == "/api/requests" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reqs unwraps request list items and maps full request geometry to request dto`() {
|
||||
val requestId = UUID.fromString("11111111-1111-4111-8111-111111111111")
|
||||
val service = earthService(
|
||||
responses = mutableListOf(
|
||||
requestsResponse(
|
||||
"""
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": "$requestId",
|
||||
"name": "summary request name",
|
||||
"status": "ACCEPTED",
|
||||
"surveyType": "OPTICS",
|
||||
"importance": 7.0,
|
||||
"beginDateTime": "2026-01-01T00:00:00Z",
|
||||
"endDateTime": "2026-01-02T00:00:00Z",
|
||||
"kpp": [1, 2],
|
||||
"highPriorityTransmit": false,
|
||||
"coveragePercent": 0.0
|
||||
}
|
||||
],
|
||||
"page": 0,
|
||||
"size": 500,
|
||||
"totalItems": 1,
|
||||
"totalPages": 1
|
||||
}
|
||||
""".trimIndent()
|
||||
),
|
||||
requestDetailsResponse(
|
||||
"""
|
||||
{
|
||||
"id": "$requestId",
|
||||
"name": "real request name",
|
||||
"geometry": "POLYGON ((30 10, 40 40, 20 40, 30 10))",
|
||||
"importance": 7.0,
|
||||
"status": "ACCEPTED",
|
||||
"surveyType": "OPTICS"
|
||||
}
|
||||
""".trimIndent()
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
val request = service.reqs().single().block()!!
|
||||
|
||||
assertEquals(requestId, request.requestId)
|
||||
assertEquals("real request name", request.name)
|
||||
assertFalse(request.name == requestId.toString())
|
||||
assertEquals(7.0, request.importance)
|
||||
assertEquals("POLYGON ((30 10, 40 40, 20 40, 30 10))", request.contour)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `addRequest posts new v1 create request and returns legacy request dto`() {
|
||||
val responseId = UUID.fromString("33333333-3333-4333-8333-333333333333")
|
||||
val responseName = "created request"
|
||||
val responseImportance = 8.25
|
||||
val requestBody = AtomicReference<String>()
|
||||
val requestMethod = AtomicReference<String>()
|
||||
val requestPath = AtomicReference<String>()
|
||||
val server = HttpServer.create(InetSocketAddress(0), 0)
|
||||
server.createContext("/") { exchange ->
|
||||
requestMethod.set(exchange.requestMethod)
|
||||
requestPath.set(exchange.requestURI.path)
|
||||
requestBody.set(exchange.requestBody.readBytes().toString(StandardCharsets.UTF_8))
|
||||
|
||||
val response = """
|
||||
{
|
||||
"id": "$responseId",
|
||||
"name": "$responseName",
|
||||
"status": "ACCEPTED",
|
||||
"surveyType": "OPTICS",
|
||||
"importance": $responseImportance,
|
||||
"message": "Заявка успешно создана"
|
||||
}
|
||||
""".trimIndent().toByteArray(StandardCharsets.UTF_8)
|
||||
|
||||
exchange.responseHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
|
||||
exchange.sendResponseHeaders(HttpStatus.CREATED.value(), response.size.toLong())
|
||||
exchange.responseBody.use { body -> body.write(response) }
|
||||
}
|
||||
server.start()
|
||||
|
||||
try {
|
||||
val sourceRequest = RequestDTO(
|
||||
name = "map request",
|
||||
importance = 7.5,
|
||||
contour = "POLYGON ((30 10, 40 40, 20 40, 30 10))",
|
||||
)
|
||||
val service = EarthService(provider(WebClient.builder()), url = "http://localhost:${server.address.port}")
|
||||
|
||||
val result = service.addRequest(sourceRequest)!!
|
||||
|
||||
assertEquals(responseId, result.requestId)
|
||||
assertEquals(responseImportance, result.importance)
|
||||
assertEquals(sourceRequest.contour, result.contour)
|
||||
assertEquals(responseName, result.name)
|
||||
assertEquals("POST", requestMethod.get())
|
||||
assertEquals("/v1/requests", requestPath.get())
|
||||
assertFalse(requestPath.get() == "/api/requests")
|
||||
|
||||
val bodyText = assertNotNull(requestBody.get())
|
||||
val body = objectMapper.readTree(bodyText)
|
||||
assertNotNull(UUID.fromString(body["id"].asText()))
|
||||
assertEquals(sourceRequest.name, body["name"].asText())
|
||||
assertEquals(sourceRequest.contour, body["geometry"].asText())
|
||||
assertEquals(sourceRequest.importance, body["importance"].asDouble())
|
||||
assertEquals("2026-01-01T00:00:00Z", body["beginDateTime"].asText())
|
||||
assertEquals("2026-12-31T23:59:59Z", body["endDateTime"].asText())
|
||||
assertEquals(0, body["kpp"].size())
|
||||
assertFalse(body["highPriorityTransmit"].asBoolean())
|
||||
assertEquals("PANCHROMATIC", body["optics"]["resultType"].asText())
|
||||
assertEquals(5.0, body["optics"]["resolution"].asDouble())
|
||||
assertEquals(10.0, body["optics"]["sunAngleMin"].asDouble())
|
||||
assertEquals(90.0, body["optics"]["sunAngleMax"].asDouble())
|
||||
assertEquals(100.0, body["optics"]["clouds"].asDouble())
|
||||
assertTrue(!bodyText.contains("app_type"))
|
||||
assertTrue(!bodyText.contains("appType"))
|
||||
assertTrue(!bodyText.contains("AppType"))
|
||||
assertTrue(!bodyText.contains("requestType"))
|
||||
} finally {
|
||||
server.stop(0)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reqs fetches all pages and loads full request details for every summary`() {
|
||||
val firstId = UUID.fromString("11111111-1111-4111-8111-111111111111")
|
||||
val secondId = UUID.fromString("22222222-2222-4222-8222-222222222222")
|
||||
val requestedUris = mutableListOf<URI>()
|
||||
val service = earthService(
|
||||
requestedUris = requestedUris,
|
||||
responses = mutableListOf(
|
||||
requestsResponse(
|
||||
"""
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": "$firstId",
|
||||
"name": "first request",
|
||||
"status": "ACCEPTED",
|
||||
"surveyType": "OPTICS",
|
||||
"importance": 1.0,
|
||||
"beginDateTime": "2026-01-01T00:00:00Z",
|
||||
"endDateTime": "2026-01-02T00:00:00Z",
|
||||
"kpp": [],
|
||||
"highPriorityTransmit": false
|
||||
}
|
||||
],
|
||||
"page": 0,
|
||||
"size": 500,
|
||||
"totalItems": 2,
|
||||
"totalPages": 2
|
||||
}
|
||||
""".trimIndent()
|
||||
),
|
||||
requestsResponse(
|
||||
"""
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": "$secondId",
|
||||
"name": "second request",
|
||||
"status": "ACCEPTED",
|
||||
"surveyType": "RSA",
|
||||
"importance": 2.0,
|
||||
"beginDateTime": "2026-01-03T00:00:00Z",
|
||||
"endDateTime": "2026-01-04T00:00:00Z",
|
||||
"kpp": [3],
|
||||
"highPriorityTransmit": true
|
||||
}
|
||||
],
|
||||
"page": 1,
|
||||
"size": 500,
|
||||
"totalItems": 2,
|
||||
"totalPages": 2
|
||||
}
|
||||
""".trimIndent()
|
||||
),
|
||||
requestDetailsResponse(
|
||||
"""
|
||||
{
|
||||
"id": "$firstId",
|
||||
"name": "first request",
|
||||
"geometry": "POLYGON EMPTY",
|
||||
"importance": 1.0
|
||||
}
|
||||
""".trimIndent()
|
||||
),
|
||||
requestDetailsResponse(
|
||||
"""
|
||||
{
|
||||
"id": "$secondId",
|
||||
"name": "second request",
|
||||
"geometry": "POLYGON ((0 0, 1 0, 1 1, 0 0))",
|
||||
"importance": 2.0
|
||||
}
|
||||
""".trimIndent()
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
val requests = service.reqs().collectList().block()!!
|
||||
|
||||
assertEquals(listOf(firstId, secondId), requests.map { request -> request.requestId })
|
||||
assertEquals(
|
||||
listOf(
|
||||
"/v1/requests?page=0&size=500",
|
||||
"/v1/requests?page=1&size=500",
|
||||
"/v1/requests/$firstId",
|
||||
"/v1/requests/$secondId",
|
||||
),
|
||||
requestedUris.map { uri -> "${uri.path}${uri.query?.let { "?$it" } ?: ""}" }
|
||||
)
|
||||
assertFalse(requestedUris.any { uri -> uri.path == "/api/requests" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reqcells calls v1 request with cells instead of legacy by number endpoint`() {
|
||||
val requestId = UUID.fromString("11111111-1111-4111-8111-111111111111")
|
||||
val requestedUris = mutableListOf<URI>()
|
||||
val service = earthService(
|
||||
requestedUris = requestedUris,
|
||||
responses = mutableListOf(requestWithCellsResponse(requestId))
|
||||
)
|
||||
|
||||
service.reqcells(requestId.toString())
|
||||
|
||||
assertEquals("/v1/requests/$requestId/with-cells", requestedUris.single().path)
|
||||
assertFalse(requestedUris.any { uri -> uri.path.startsWith("/api/") })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reqcells maps new wrapper to current request with cells dto`() {
|
||||
val requestId = UUID.fromString("11111111-1111-4111-8111-111111111111")
|
||||
val service = earthService(
|
||||
responses = mutableListOf(
|
||||
requestWithCellsResponse(
|
||||
requestId = requestId,
|
||||
requestName = "request name from service",
|
||||
requestGeometry = "POLYGON ((30 10, 40 40, 20 40, 30 10))",
|
||||
requestImportance = 10.0,
|
||||
cellNum = 123L,
|
||||
cellImportance = 8.5,
|
||||
cellContour = "POLYGON ((36 55, 37 55, 37 56, 36 56, 36 55))"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val result = service.reqcells(requestId.toString())!!
|
||||
|
||||
assertEquals(requestId, result.request.requestId)
|
||||
assertEquals("request name from service", result.request.name)
|
||||
assertFalse(result.request.name == requestId.toString())
|
||||
assertEquals(10.0, result.request.importance)
|
||||
assertEquals("POLYGON ((30 10, 40 40, 20 40, 30 10))", result.request.contour)
|
||||
assertEquals(123L, result.cells.single().id)
|
||||
assertEquals(123L, result.cells.single().num)
|
||||
assertEquals(8.5, result.cells.single().importance)
|
||||
assertEquals("POLYGON ((36 55, 37 55, 37 56, 36 56, 36 55))", result.cells.single().contour)
|
||||
assertEquals(0.0, result.cells.single().latitude)
|
||||
assertEquals(0.0, result.cells.single().longitude)
|
||||
assertFalse(result.cells.single().javaClass.declaredFields.any { field -> field.name == "name" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reqcells propagates not found response`() {
|
||||
val requestId = UUID.fromString("11111111-1111-4111-8111-111111111111")
|
||||
val service = earthService(
|
||||
responses = mutableListOf(
|
||||
ClientResponse.create(HttpStatus.NOT_FOUND)
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
|
||||
.body("""{"code":"REQUEST_NOT_FOUND","message":"Заявка не найдена"}""")
|
||||
.build()
|
||||
)
|
||||
)
|
||||
|
||||
val error = assertFailsWith<WebClientResponseException> {
|
||||
service.reqcells(requestId.toString())
|
||||
}
|
||||
|
||||
assertEquals(HttpStatus.NOT_FOUND, error.statusCode)
|
||||
}
|
||||
|
||||
private fun earthService(
|
||||
requestedUris: MutableList<URI> = mutableListOf(),
|
||||
responses: MutableList<ClientResponse>,
|
||||
): EarthService {
|
||||
val builder = WebClient.builder()
|
||||
.exchangeFunction { request ->
|
||||
requestedUris += request.url()
|
||||
Mono.just(responses.removeAt(0))
|
||||
}
|
||||
return EarthService(provider(builder), url = "http://pcp-request-service")
|
||||
}
|
||||
|
||||
private fun cellsResponse(
|
||||
body: String = """
|
||||
{
|
||||
"items": [],
|
||||
"page": 0,
|
||||
"size": 500,
|
||||
"totalItems": 0,
|
||||
"totalPages": 1
|
||||
}
|
||||
""".trimIndent(),
|
||||
): ClientResponse =
|
||||
ClientResponse.create(HttpStatus.OK)
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
|
||||
.body(body)
|
||||
.build()
|
||||
|
||||
private fun requestsResponse(
|
||||
body: String = """
|
||||
{
|
||||
"items": [],
|
||||
"page": 0,
|
||||
"size": 500,
|
||||
"totalItems": 0,
|
||||
"totalPages": 1
|
||||
}
|
||||
""".trimIndent(),
|
||||
): ClientResponse =
|
||||
ClientResponse.create(HttpStatus.OK)
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
|
||||
.body(body)
|
||||
.build()
|
||||
|
||||
private fun requestDetailsResponse(body: String): ClientResponse =
|
||||
ClientResponse.create(HttpStatus.OK)
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
|
||||
.body(body)
|
||||
.build()
|
||||
|
||||
private fun requestWithCellsResponse(
|
||||
requestId: UUID,
|
||||
requestName: String = "request name",
|
||||
requestGeometry: String = "POLYGON EMPTY",
|
||||
requestImportance: Double = 1.0,
|
||||
cellNum: Long = 1L,
|
||||
cellImportance: Double = 1.0,
|
||||
cellContour: String = "POLYGON EMPTY",
|
||||
): ClientResponse =
|
||||
ClientResponse.create(HttpStatus.OK)
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
|
||||
.body(
|
||||
"""
|
||||
{
|
||||
"request": {
|
||||
"id": "$requestId",
|
||||
"name": "$requestName",
|
||||
"status": "ACCEPTED",
|
||||
"surveyType": "OPTICS",
|
||||
"geometry": "$requestGeometry",
|
||||
"importance": $requestImportance,
|
||||
"beginDateTime": "2026-01-01T00:00:00Z",
|
||||
"endDateTime": "2026-01-02T00:00:00Z",
|
||||
"kpp": [],
|
||||
"highPriorityTransmit": false,
|
||||
"optics": null,
|
||||
"rsa": null,
|
||||
"coverage": {
|
||||
"requiredPercent": 100.0,
|
||||
"currentPercent": 0.0
|
||||
},
|
||||
"createdAt": "2026-01-01T00:00:00Z",
|
||||
"updatedAt": "2026-01-01T00:00:00Z",
|
||||
"deletedAt": null
|
||||
},
|
||||
"cells": [
|
||||
{
|
||||
"cellNum": $cellNum,
|
||||
"name": "cell name must be ignored",
|
||||
"coveragePercent": 42.5,
|
||||
"importance": $cellImportance,
|
||||
"contour": "$cellContour"
|
||||
}
|
||||
]
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
.build()
|
||||
|
||||
private fun provider(builder: WebClient.Builder): ObjectProvider<WebClient.Builder> =
|
||||
object : ObjectProvider<WebClient.Builder> {
|
||||
override fun getObject(vararg args: Any?): WebClient.Builder = builder
|
||||
override fun getObject(): WebClient.Builder = builder
|
||||
override fun getIfAvailable(): WebClient.Builder = builder
|
||||
override fun getIfUnique(): WebClient.Builder = builder
|
||||
override fun iterator(): MutableIterator<WebClient.Builder> = mutableListOf(builder).iterator()
|
||||
}
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
package space.nstart.pcp.slots_service.service
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import org.mockito.Mockito.doReturn
|
||||
import org.mockito.Mockito.mock
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteOrbitAvailabilityDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteGroupDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSummaryDTO
|
||||
import space.nstart.pcp.slots_service.configuration.CustomErrorException
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.math.abs
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class GroupStateServiceTest {
|
||||
|
||||
private val satelliteCatalogService: SatelliteCatalogService = mock()
|
||||
private val ballisticsService: BallisticsService = mock()
|
||||
private val service = GroupStateService(satelliteCatalogService, ballisticsService)
|
||||
|
||||
@Test
|
||||
fun `group summaries calculate availability intersection`() {
|
||||
doReturn(listOf(primaryGroup(), groupWithoutCommonInterval())).`when`(satelliteCatalogService).allGroups()
|
||||
doReturn(satelliteSummaries()).`when`(satelliteCatalogService).allSatellites()
|
||||
doReturn(orbitAvailability()).`when`(ballisticsService).orbitAvailability()
|
||||
|
||||
val summaries = service.groupSummaries()
|
||||
|
||||
assertEquals(2, summaries.size)
|
||||
assertNotNull(summaries[0].availableInterval)
|
||||
assertEquals(LocalDateTime.of(2026, 4, 24, 10, 30), summaries[0].availableInterval?.timeStart)
|
||||
assertEquals(LocalDateTime.of(2026, 4, 24, 11, 30), summaries[0].availableInterval?.timeStop)
|
||||
assertNull(summaries[1].availableInterval)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `group state rejects time outside available interval`() {
|
||||
doReturn(primaryGroup()).`when`(satelliteCatalogService).group(71L)
|
||||
doReturn(satelliteSummaries()).`when`(satelliteCatalogService).allSatellites()
|
||||
doReturn(orbitAvailability()).`when`(ballisticsService).orbitAvailability()
|
||||
|
||||
val exception = assertThrows<CustomErrorException> {
|
||||
service.groupState(71L, LocalDateTime.of(2026, 4, 24, 10, 15))
|
||||
}
|
||||
|
||||
assertTrue(exception.message!!.contains("не входит в доступный интервал"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `group state builds pairwise matrix from orbital points`() {
|
||||
val calculationTime = LocalDateTime.of(2026, 4, 24, 10, 45)
|
||||
doReturn(primaryGroup()).`when`(satelliteCatalogService).group(71L)
|
||||
doReturn(satelliteSummaries()).`when`(satelliteCatalogService).allSatellites()
|
||||
doReturn(orbitAvailability()).`when`(ballisticsService).orbitAvailability()
|
||||
doReturn(firstOrbitalPoint(calculationTime)).`when`(ballisticsService).exactTimePoint(501L, calculationTime)
|
||||
doReturn(secondOrbitalPoint(calculationTime)).`when`(ballisticsService).exactTimePoint(502L, calculationTime)
|
||||
|
||||
val state = service.groupState(71L, calculationTime)
|
||||
|
||||
assertEquals(71L, state.groupId)
|
||||
assertEquals(calculationTime, state.calculationTime)
|
||||
assertEquals(2, state.satellites.size)
|
||||
assertEquals(2, state.matrix.size)
|
||||
state.satellites.forEach { satellite ->
|
||||
assertTrue(!satellite.omegabDeg.isNaN())
|
||||
assertTrue(!satellite.uDeg.isNaN())
|
||||
assertTrue(satellite.omegabDeg in 0.0..360.0)
|
||||
assertTrue(satellite.uDeg in 0.0..360.0)
|
||||
}
|
||||
|
||||
val firstRow = state.matrix[0]
|
||||
val secondRow = state.matrix[1]
|
||||
assertEquals(0.0, firstRow.cells[0].deltaOmegabDeg)
|
||||
assertEquals(0.0, secondRow.cells[1].deltaUDeg)
|
||||
assertTrue(abs(firstRow.cells[1].deltaOmegabDeg + secondRow.cells[0].deltaOmegabDeg) < 0.0001)
|
||||
assertTrue(abs(firstRow.cells[1].deltaUDeg + secondRow.cells[0].deltaUDeg) < 0.0001)
|
||||
}
|
||||
|
||||
private fun primaryGroup() = SatelliteGroupDTO(
|
||||
id = 71L,
|
||||
name = "KONDOR",
|
||||
satelliteIds = listOf(501L, 502L)
|
||||
)
|
||||
|
||||
private fun groupWithoutCommonInterval() = SatelliteGroupDTO(
|
||||
id = 72L,
|
||||
name = "NO-INTERSECTION",
|
||||
satelliteIds = listOf(501L, 503L)
|
||||
)
|
||||
|
||||
private fun satelliteSummaries() = listOf(
|
||||
SatelliteSummaryDTO(id = 501L, noradId = 56756L, code = "SAT-501", name = "Sat 501"),
|
||||
SatelliteSummaryDTO(id = 502L, noradId = 62138L, code = "SAT-502", name = "Sat 502"),
|
||||
SatelliteSummaryDTO(id = 503L, noradId = null, code = "SAT-503", name = "Sat 503")
|
||||
)
|
||||
|
||||
private fun orbitAvailability() = listOf(
|
||||
SatelliteOrbitAvailabilityDTO(
|
||||
satelliteId = 501L,
|
||||
timeStart = LocalDateTime.of(2026, 4, 24, 10, 0),
|
||||
timeStop = LocalDateTime.of(2026, 4, 24, 12, 0)
|
||||
),
|
||||
SatelliteOrbitAvailabilityDTO(
|
||||
satelliteId = 502L,
|
||||
timeStart = LocalDateTime.of(2026, 4, 24, 10, 30),
|
||||
timeStop = LocalDateTime.of(2026, 4, 24, 11, 30)
|
||||
)
|
||||
)
|
||||
|
||||
private fun firstOrbitalPoint(time: LocalDateTime) = OrbPointDTO(
|
||||
time = time,
|
||||
revolution = 1L,
|
||||
vx = 941.8995373,
|
||||
vy = 1150.6919822,
|
||||
vz = 7544.9920840,
|
||||
x = -5357933.7872,
|
||||
y = 4328646.1787,
|
||||
z = 0.0
|
||||
)
|
||||
|
||||
private fun secondOrbitalPoint(time: LocalDateTime) = OrbPointDTO(
|
||||
time = time,
|
||||
revolution = 1499L,
|
||||
vx = -272.89561674108205,
|
||||
vy = -1460.1016031205972,
|
||||
vz = 7539.209413654664,
|
||||
x = 6782429.138044466,
|
||||
y = -1205586.994919729,
|
||||
z = 0.0
|
||||
)
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
package space.nstart.pcp.slots_service.service
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito.doReturn
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.verify
|
||||
import reactor.core.publisher.Flux
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.AscNodeDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.KeplersDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteOrbitAvailabilityDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSummaryDTO
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class SatellitePdcmServiceTest {
|
||||
|
||||
private val satelliteCatalogService: SatelliteCatalogService = mock()
|
||||
private val ballisticsService: BallisticsService = mock()
|
||||
private val service = SatellitePdcmService(satelliteCatalogService, ballisticsService)
|
||||
|
||||
@Test
|
||||
fun `satellite summaries mark active interval by current time`() {
|
||||
val referenceTime = LocalDateTime.of(2026, 4, 24, 11, 0)
|
||||
doReturn(satelliteSummaries()).`when`(satelliteCatalogService).allSatellites()
|
||||
doReturn(orbitAvailability()).`when`(ballisticsService).orbitAvailability()
|
||||
|
||||
val summaries = service.satelliteSummaries(referenceTime)
|
||||
|
||||
assertEquals(2, summaries.size)
|
||||
assertEquals(501L, summaries.first().satelliteId)
|
||||
assertTrue(summaries.first().activeInterval)
|
||||
assertFalse(summaries.last().activeInterval)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `satellite details request asc-node by satellite id and not norad id`() {
|
||||
val referenceTime = LocalDateTime.of(2026, 4, 24, 11, 0)
|
||||
val availability = orbitAvailability().first()
|
||||
doReturn(satelliteSummaries()).`when`(satelliteCatalogService).allSatellites()
|
||||
doReturn(orbitAvailability()).`when`(ballisticsService).orbitAvailability()
|
||||
doReturn(
|
||||
Flux.just(
|
||||
AscNodeDTO(
|
||||
time = LocalDateTime.of(2026, 4, 24, 10, 30),
|
||||
revolution = 120,
|
||||
long = 37.62,
|
||||
height = 512.5,
|
||||
x = 1.0,
|
||||
y = 2.0,
|
||||
z = 3.0,
|
||||
vx = 4.0,
|
||||
vy = 5.0,
|
||||
vz = 6.0,
|
||||
keps = KeplersDTO(
|
||||
ael = 7000.0,
|
||||
e = 0.001,
|
||||
inkl = 97.6,
|
||||
omega = 123.4,
|
||||
w = 45.6
|
||||
),
|
||||
localMeanTime = LocalDateTime.of(2026, 4, 24, 13, 0)
|
||||
)
|
||||
)
|
||||
).`when`(ballisticsService).ascNodes(501L, availability.timeStart, availability.timeStop)
|
||||
|
||||
val details = service.satelliteDetails(501L, referenceTime)
|
||||
|
||||
assertEquals(501L, details.satelliteId)
|
||||
assertTrue(details.activeInterval)
|
||||
assertEquals(1, details.ascNodes.size)
|
||||
assertEquals(7000.0, details.ascNodes.first().keps?.ael)
|
||||
assertEquals(LocalDateTime.of(2026, 4, 24, 13, 0), details.ascNodes.first().localMeanTime)
|
||||
verify(ballisticsService).ascNodes(501L, availability.timeStart, availability.timeStop)
|
||||
}
|
||||
|
||||
private fun satelliteSummaries() = listOf(
|
||||
SatelliteSummaryDTO(id = 501L, noradId = 95678L, code = "SAT-501", name = "Sat 501"),
|
||||
SatelliteSummaryDTO(id = 502L, noradId = 95679L, code = "SAT-502", name = "Sat 502")
|
||||
)
|
||||
|
||||
private fun orbitAvailability() = listOf(
|
||||
SatelliteOrbitAvailabilityDTO(
|
||||
satelliteId = 501L,
|
||||
timeStart = LocalDateTime.of(2026, 4, 24, 10, 0),
|
||||
timeStop = LocalDateTime.of(2026, 4, 24, 12, 0)
|
||||
),
|
||||
SatelliteOrbitAvailabilityDTO(
|
||||
satelliteId = 502L,
|
||||
timeStart = LocalDateTime.of(2026, 4, 24, 12, 30),
|
||||
timeStop = LocalDateTime.of(2026, 4, 24, 14, 0)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
spring:
|
||||
application:
|
||||
name: pcp-ui-service-test
|
||||
cloud:
|
||||
config:
|
||||
enabled: false
|
||||
import-check:
|
||||
enabled: false
|
||||
boot:
|
||||
admin:
|
||||
client:
|
||||
enabled: false
|
||||
|
||||
settings:
|
||||
dynamic-plan-service: http://localhost:65535
|
||||
Reference in New Issue
Block a user