pcp-tgu интерфейс

This commit is contained in:
Дмитрий Соловьев
2026-05-29 15:15:02 +03:00
parent e7c2060e2e
commit 4556592e63
12 changed files with 1424 additions and 1 deletions
@@ -26,6 +26,7 @@ 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.slots_service.dto.tgu.TguPlanResponse
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
@@ -38,6 +39,7 @@ 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 space.nstart.pcp.slots_service.service.TguPlanningService
import java.time.LocalDateTime
import java.util.UUID
import kotlin.test.assertEquals
@@ -72,6 +74,9 @@ class CatalogControllerTest {
@MockitoBean
private lateinit var satellitePdcmService: SatellitePdcmService
@MockitoBean
private lateinit var tguPlanningService: TguPlanningService
@Test
fun `groups page is rendered`() {
val body = WebClient.create("http://localhost:$port")
@@ -155,6 +160,48 @@ class CatalogControllerTest {
assertFalse(body.contains("<th>UUID</th>"))
}
@Test
fun `tgu planning page is rendered from menu`() {
val body = WebClient.create("http://localhost:$port")
.get()
.uri("/tgu-planning")
.retrieve()
.bodyToMono(String::class.java)
.block()!!
assertTrue(body.contains("Планирование ТГУ"))
assertTrue(body.contains("/tgu_planning_scripts.js"))
assertTrue(body.contains("href=\"/tgu-planning\""))
}
@Test
fun `tgu spacecraft plans endpoint proxies norad spacecraft id`() {
val planId = UUID.fromString("6d0941e4-61d6-4f96-8fa8-90ba11db121e")
val response = listOf(
TguPlanResponse(
planId = planId,
spacecraftId = "56756",
startTime = LocalDateTime.of(2026, 5, 29, 10, 0),
endTime = LocalDateTime.of(2026, 5, 29, 10, 15),
kppId = "KPP-1",
status = "WAITING_DECISION"
)
)
doReturn(response).`when`(tguPlanningService).spacecraftPlans("56756", 14L)
val actual = WebClient.create("http://localhost:$port")
.get()
.uri("/api/tgu-planning/spacecraft/56756/plans?historyDays=14")
.retrieve()
.bodyToFlux(TguPlanResponse::class.java)
.collectList()
.block()!!
assertEquals(planId, actual.single().planId)
assertEquals("56756", actual.single().spacecraftId)
verify(tguPlanningService).spacecraftPlans("56756", 14L)
}
@Test
fun `group create endpoint proxies request`() {
val response = SatelliteGroupDTO(
@@ -0,0 +1,140 @@
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.junit.jupiter.api.assertThrows
import org.springframework.beans.factory.ObjectProvider
import org.springframework.http.HttpStatus
import org.springframework.test.util.ReflectionTestUtils
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.server.ResponseStatusException
import space.nstart.pcp.slots_service.dto.tgu.TguPlanDecision
import java.net.InetSocketAddress
import java.net.URI
import java.util.concurrent.CopyOnWriteArrayList
import kotlin.test.assertEquals
class TguPlanningServiceTest {
private var server: HttpServer? = null
@AfterEach
fun tearDown() {
server?.stop(0)
}
@Test
fun `read methods call existing tgu endpoints`() {
val requests = CopyOnWriteArrayList<URI>()
val service = createService(startServer(requests))
val platforms = service.platforms()
val plans = service.plans(7)
val spacecraftPlans = service.spacecraftPlans("56756", 14)
assertEquals("Kondor-FKA No.2", platforms.single().name)
assertEquals("56756", plans.single().spacecraftId)
assertEquals("WAITING_DECISION", spacecraftPlans.single().status)
assertEquals(
listOf("/api/platforms", "/api/plans", "/api/plans/56756"),
requests.map { it.path }
)
assertEquals(listOf(null, "historyDays=7", "historyDays=14"), requests.map { it.rawQuery })
}
@Test
fun `decision method calls test controller endpoint`() {
val requests = CopyOnWriteArrayList<URI>()
val service = createService(startServer(requests))
val response = service.sendDecision(
java.util.UUID.fromString("6d0941e4-61d6-4f96-8fa8-90ba11db121e"),
TguPlanDecision.ACCEPTED,
"UI TEST ACCEPTED"
)
assertEquals(TguPlanDecision.ACCEPTED, response.decision)
assertEquals("/test/tgu/plans/6d0941e4-61d6-4f96-8fa8-90ba11db121e/decision", requests.last().path)
assertEquals("decision=ACCEPTED&reason=UI%20TEST%20ACCEPTED", requests.last().rawQuery)
}
@Test
fun `decision conflict is mapped to operator message`() {
val service = createService(
startServer(
statusByPath = mapOf(
"/test/tgu/plans/00000000-0000-0000-0000-000000000000/decision" to 409
)
)
)
val exception = assertThrows<ResponseStatusException> {
service.sendDecision(
java.util.UUID.fromString("00000000-0000-0000-0000-000000000000"),
TguPlanDecision.REJECTED,
"reason"
)
}
assertEquals(HttpStatus.CONFLICT, exception.statusCode)
assertEquals(
"План не является активным планом, ожидающим решения, или для него нет active attempt.",
exception.reason
)
}
private fun createService(serverUrl: String): TguPlanningService {
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 = TguPlanningService(provider)
ReflectionTestUtils.setField(service, "tguServiceUrl", serverUrl)
return service
}
private fun startServer(
requests: MutableList<URI> = CopyOnWriteArrayList(),
statusByPath: Map<String, Int> = emptyMap()
): String {
val startedServer = HttpServer.create(InetSocketAddress(0), 0)
startedServer.createContext("/") { exchange ->
requests.add(exchange.requestURI)
val forcedStatus = statusByPath[exchange.requestURI.path]
when {
forcedStatus != null -> respond(exchange, forcedStatus, """{"error":"conflict"}""")
exchange.requestURI.path == "/api/platforms" -> respond(
exchange,
200,
"""[{"id":"nsi-id","businessKey":"2024-149E","name":"Kondor-FKA No.2","status":"OPERATIONAL","noradId":56756,"mission":"KONDOR_FKA","validFrom":"2026-05-21T15:06:00Z","validTo":"9999-12-31T23:59:59Z"}]"""
)
exchange.requestURI.path == "/api/plans" || exchange.requestURI.path == "/api/plans/56756" -> respond(
exchange,
200,
"""[{"planId":"6d0941e4-61d6-4f96-8fa8-90ba11db121e","spacecraftId":"56756","startTime":"2026-05-29T10:00:00","endTime":"2026-05-29T10:15:00","kppId":"KPP-1","status":"WAITING_DECISION"}]"""
)
exchange.requestURI.path.startsWith("/test/tgu/plans/") -> respond(
exchange,
200,
"""{"eventId":"1c857555-f02b-4051-b56a-f112b842624a","spacecraftId":"56756","planId":"6d0941e4-61d6-4f96-8fa8-90ba11db121e","attemptId":"a82394f2-545f-4cd6-9ab1-876ca403d33a","decision":"ACCEPTED","decisionTime":"2026-05-29T10:01:00","reason":"UI TEST ACCEPTED"}"""
)
else -> respond(exchange, 404, """{"error":"not found"}""")
}
}
startedServer.start()
server = startedServer
return "http://localhost:${startedServer.address.port}"
}
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) }
}
}
@@ -13,3 +13,4 @@ spring:
settings:
dynamic-plan-service: http://localhost:65535
tgu-service: http://localhost:65535