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