Перевод pcp-tgu на новый НСИ
This commit is contained in:
File diff suppressed because one or more lines are too long
+4
-1
@@ -9,5 +9,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
data class ClassifierProperties(
|
||||
|
||||
/** URL классификатора космических аппаратов. */
|
||||
val platformsUrl: String
|
||||
val platformsUrl: String,
|
||||
|
||||
/** Статусы КА из НСИ, с которыми сервису разрешено работать. */
|
||||
val allowedPlatformStatuses: Set<String>
|
||||
)
|
||||
|
||||
+16
-5
@@ -1,7 +1,8 @@
|
||||
package space.nstart.pcp_tgu_service.controller
|
||||
|
||||
/** Содержит методы API только на чтение для платформ из внешнего классификатора. */
|
||||
import space.nstart.pcp_tgu_service.dto.ContentItemDTO
|
||||
import space.nstart.pcp_tgu_service.domain.PlatformCatalogItem
|
||||
import space.nstart.pcp_tgu_service.dto.PlatformResponse
|
||||
import space.nstart.pcp_tgu_service.service.PlatformService
|
||||
import io.swagger.v3.oas.annotations.Operation
|
||||
import io.swagger.v3.oas.annotations.tags.Tag
|
||||
@@ -25,14 +26,24 @@ class PlatformController(
|
||||
/**
|
||||
* Возвращает список космических аппаратов из внешнего классификатора.
|
||||
*/
|
||||
/** Возвращает платформы с необязательной фильтрацией по типу и статусу. */
|
||||
@GetMapping
|
||||
@Operation(summary = "Получить список космических аппаратов")
|
||||
fun getPlatforms(
|
||||
@RequestParam(required = false) platformTypeCode: String?,
|
||||
@RequestParam(required = false) status: String?
|
||||
): Mono<List<ContentItemDTO>> =
|
||||
): Mono<List<PlatformResponse>> =
|
||||
Mono.fromCallable {
|
||||
platformService.loadPlatforms(platformTypeCode, status)
|
||||
platformService.loadPlatforms(status).map { platform -> platform.toResponse() }
|
||||
}.subscribeOn(Schedulers.boundedElastic())
|
||||
|
||||
private fun PlatformCatalogItem.toResponse(): PlatformResponse =
|
||||
PlatformResponse(
|
||||
id = nsiId,
|
||||
businessKey = businessKey,
|
||||
name = name,
|
||||
status = status,
|
||||
noradId = noradId,
|
||||
mission = mission,
|
||||
validFrom = validFrom,
|
||||
validTo = validTo
|
||||
)
|
||||
}
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package space.nstart.pcp_tgu_service.domain
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
/** Платформа КА из НСИ в минимальном виде, нужном pcp-tgu-service. */
|
||||
data class PlatformCatalogItem(
|
||||
val nsiId: String,
|
||||
val businessKey: String?,
|
||||
val name: String?,
|
||||
val status: String?,
|
||||
val noradId: Long?,
|
||||
val mission: String?,
|
||||
val validFrom: Instant?,
|
||||
val validTo: Instant?
|
||||
)
|
||||
+21
-235
@@ -2,249 +2,35 @@ package space.nstart.pcp_tgu_service.dto
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Корневой DTO JSON-ответа.
|
||||
* Соответствует всему объекту JSON.
|
||||
*/
|
||||
/** Платформа из root-array ответа НСИ. */
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class PlatformsResponseDTO(
|
||||
|
||||
/**
|
||||
* UUID справочника.
|
||||
*/
|
||||
val uuid: String?,
|
||||
|
||||
/**
|
||||
* API имя сущности.
|
||||
*/
|
||||
val apiName: String?,
|
||||
|
||||
/**
|
||||
* GUI имя сущности.
|
||||
*/
|
||||
val guiName: String?,
|
||||
|
||||
/**
|
||||
* Статус справочника.
|
||||
*/
|
||||
val status: String?,
|
||||
|
||||
/**
|
||||
* Признак иерархии.
|
||||
*/
|
||||
val hierarchy: Boolean?,
|
||||
|
||||
/**
|
||||
* Правило валидации.
|
||||
*/
|
||||
val validationRule: String?,
|
||||
|
||||
/**
|
||||
* Список описаний полей справочника.
|
||||
*/
|
||||
val fields: List<FieldDTO>?,
|
||||
|
||||
/**
|
||||
* Основной список элементов.
|
||||
*/
|
||||
val content: List<ContentItemDTO>?,
|
||||
|
||||
/**
|
||||
* Информация о пагинации.
|
||||
*/
|
||||
val page: PageDTO?
|
||||
data class NsiPlatformDto(
|
||||
val id: String?,
|
||||
val businessKey: String?,
|
||||
val data: NsiPlatformDataDto?,
|
||||
val validFrom: Instant?,
|
||||
val validTo: Instant?
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO описания одного поля.
|
||||
*/
|
||||
/** Согласованные поля объекта data платформы НСИ. */
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class FieldDTO(
|
||||
|
||||
/**
|
||||
* UUID поля.
|
||||
*/
|
||||
val uuid: String?,
|
||||
|
||||
/**
|
||||
* API имя поля.
|
||||
*/
|
||||
val apiName: String?,
|
||||
|
||||
/**
|
||||
* GUI имя поля.
|
||||
*/
|
||||
val guiName: String?,
|
||||
|
||||
/**
|
||||
* Статус поля.
|
||||
*/
|
||||
data class NsiPlatformDataDto(
|
||||
val name: String?,
|
||||
val status: String?,
|
||||
|
||||
/**
|
||||
* Тип поля.
|
||||
*/
|
||||
val type: String?,
|
||||
|
||||
/**
|
||||
* API имя связанного классификатора.
|
||||
*/
|
||||
val fkClassifierApiName: String?,
|
||||
|
||||
/**
|
||||
* API имя связанного поля.
|
||||
*/
|
||||
val fkFieldApiName: String?
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO одного элемента массива content.
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class ContentItemDTO(
|
||||
|
||||
/**
|
||||
* UUID записи.
|
||||
*/
|
||||
val uuid: String?,
|
||||
|
||||
/**
|
||||
* UUID родительского элемента.
|
||||
*/
|
||||
val parentItemUuid: String?,
|
||||
|
||||
/**
|
||||
* Статус записи.
|
||||
*/
|
||||
val status: String?,
|
||||
|
||||
/**
|
||||
* Дата начала действия.
|
||||
*/
|
||||
val initiation: String?,
|
||||
|
||||
/**
|
||||
* Дата окончания действия.
|
||||
*/
|
||||
val expiration: String?,
|
||||
|
||||
/**
|
||||
* Вложенный объект данных.
|
||||
*/
|
||||
val data: ContentDataDTO?
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO вложенного объекта data внутри content.
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class ContentDataDTO(
|
||||
|
||||
/**
|
||||
* Код типа платформы.
|
||||
*/
|
||||
@param:JsonProperty("platform_type_code")
|
||||
val platformTypeCode: String?,
|
||||
|
||||
/**
|
||||
* NORAD ID.
|
||||
*/
|
||||
@param:JsonProperty("norad_id")
|
||||
val noradId: Long?,
|
||||
val mission: String?
|
||||
)
|
||||
|
||||
/**
|
||||
* Уникальный номер изделия (УМН).
|
||||
*/
|
||||
val umn: String?,
|
||||
|
||||
/**
|
||||
* Название аппарата.
|
||||
*/
|
||||
data class PlatformResponse(
|
||||
val id: String,
|
||||
val businessKey: String?,
|
||||
val name: String?,
|
||||
|
||||
/**
|
||||
* Описание аппарата.
|
||||
*/
|
||||
val description: String?,
|
||||
|
||||
/**
|
||||
* Название типа платформы.
|
||||
*/
|
||||
@param:JsonProperty("platform_type_name")
|
||||
val platformTypeName: String?
|
||||
val status: String?,
|
||||
val noradId: Long?,
|
||||
val mission: String?,
|
||||
val validFrom: Instant?,
|
||||
val validTo: Instant?
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO блока page (пагинация).
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class PageDTO(
|
||||
|
||||
/**
|
||||
* Размер страницы.
|
||||
*/
|
||||
val size: Int?,
|
||||
|
||||
/**
|
||||
* Номер страницы.
|
||||
*/
|
||||
val number: Int?,
|
||||
|
||||
/**
|
||||
* Общее количество элементов.
|
||||
*/
|
||||
val totalElements: Long?,
|
||||
|
||||
/**
|
||||
* Общее количество страниц.
|
||||
*/
|
||||
val totalPages: Int?
|
||||
)
|
||||
|
||||
/**
|
||||
* Класс парсера JSON.
|
||||
*/
|
||||
class PlatformsJsonParser(
|
||||
|
||||
/**
|
||||
* ObjectMapper Jackson.
|
||||
* Предоставляется Spring Boot автоматически.
|
||||
*/
|
||||
private val objectMapper: ObjectMapper
|
||||
) {
|
||||
|
||||
/**
|
||||
* Парсит весь JSON в корневой DTO.
|
||||
*
|
||||
* @param json JSON строка
|
||||
* @return PlatformsResponseDTO
|
||||
*/
|
||||
fun parse(json: String): PlatformsResponseDTO {
|
||||
|
||||
/**
|
||||
* Десериализация JSON -> Kotlin объект.
|
||||
*/
|
||||
return objectMapper.readValue(json, PlatformsResponseDTO::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
* Парсит JSON и возвращает только массив content.
|
||||
*
|
||||
* @param json JSON строка
|
||||
* @return список элементов content
|
||||
*/
|
||||
fun parseContent(json: String): List<ContentItemDTO> {
|
||||
|
||||
/**
|
||||
* Парсим корневой объект.
|
||||
*/
|
||||
val response = objectMapper.readValue(json, PlatformsResponseDTO::class.java)
|
||||
|
||||
/**
|
||||
* Если content отсутствует — возвращаем пустой список.
|
||||
*/
|
||||
return response.content.orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
+14
-13
@@ -5,8 +5,8 @@ import space.nstart.pcp_tgu_service.config.ExternalApiProperties
|
||||
import space.nstart.pcp_tgu_service.config.PlanningProperties
|
||||
import space.nstart.pcp_tgu_service.domain.InsertionPoint
|
||||
import space.nstart.pcp_tgu_service.domain.ObservationWindow
|
||||
import space.nstart.pcp_tgu_service.domain.PlatformCatalogItem
|
||||
import space.nstart.pcp_tgu_service.domain.SpacecraftPoints
|
||||
import space.nstart.pcp_tgu_service.dto.ContentItemDTO
|
||||
import space.nstart.pcp_tgu_service.dto.ExternalRvaItemDto
|
||||
import space.nstart.pcp_tgu_service.service.PlatformService
|
||||
import org.slf4j.LoggerFactory
|
||||
@@ -39,16 +39,15 @@ class ExternalPointsClient(
|
||||
|
||||
/**
|
||||
* Загружает окна видимости по всем КА из классификатора,
|
||||
* у которых заполнен NORAD ID.
|
||||
* которые разрешены для работы TGU.
|
||||
*/
|
||||
/** Загружает окна видимости для всех КА с заполненным NORAD-идентификатором в классификаторе. */
|
||||
fun fetchAllSpacecraftPoints(): List<SpacecraftPoints> {
|
||||
val timeStart = LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES)
|
||||
val timeStop = timeStart.plusDays(planningProperties.externalPointsLookaheadDays)
|
||||
|
||||
return platformService.loadPlatforms()
|
||||
return platformService.loadAllowedPlatforms()
|
||||
.mapNotNull { platform ->
|
||||
val noradId = platform.data?.noradId ?: return@mapNotNull null
|
||||
val noradId = requireNotNull(platform.noradId)
|
||||
fetchSpacecraftPointsSafely(
|
||||
platform = platform,
|
||||
satelliteId = noradId,
|
||||
@@ -60,7 +59,7 @@ class ExternalPointsClient(
|
||||
|
||||
/** Оборачивает загрузку по одному КА и подавляет транспортные ошибки только для него. */
|
||||
private fun fetchSpacecraftPointsSafely(
|
||||
platform: ContentItemDTO,
|
||||
platform: PlatformCatalogItem,
|
||||
satelliteId: Long,
|
||||
timeStart: LocalDateTime,
|
||||
timeStop: LocalDateTime
|
||||
@@ -138,19 +137,19 @@ class ExternalPointsClient(
|
||||
}
|
||||
|
||||
private fun logExternalApiError(
|
||||
platform: ContentItemDTO,
|
||||
platform: PlatformCatalogItem,
|
||||
satelliteId: Long,
|
||||
timeStart: LocalDateTime,
|
||||
timeStop: LocalDateTime,
|
||||
ex: WebClientException
|
||||
) {
|
||||
val platformName = platform.data?.name
|
||||
|
||||
if (ex is WebClientResponseException) {
|
||||
log.warn(
|
||||
"Skipping spacecraft due to external API response error. name={}, noradId={}, timeStart={}, timeStop={}, status={}, responseBody={}",
|
||||
platformName,
|
||||
"Skipping spacecraft due to external API response error. name={}, noradId={}, nsiId={}, businessKey={}, timeStart={}, timeStop={}, status={}, responseBody={}",
|
||||
platform.name,
|
||||
satelliteId,
|
||||
platform.nsiId,
|
||||
platform.businessKey,
|
||||
timeStart,
|
||||
timeStop,
|
||||
ex.statusCode.value(),
|
||||
@@ -160,9 +159,11 @@ class ExternalPointsClient(
|
||||
}
|
||||
|
||||
log.warn(
|
||||
"Skipping spacecraft due to external API transport error. name={}, noradId={}, timeStart={}, timeStop={}, message={}",
|
||||
platformName,
|
||||
"Skipping spacecraft due to external API transport error. name={}, noradId={}, nsiId={}, businessKey={}, timeStart={}, timeStop={}, message={}",
|
||||
platform.name,
|
||||
satelliteId,
|
||||
platform.nsiId,
|
||||
platform.businessKey,
|
||||
timeStart,
|
||||
timeStop,
|
||||
ex.message
|
||||
|
||||
+5
-5
@@ -2,7 +2,7 @@ package space.nstart.pcp_tgu_service.integration.api
|
||||
|
||||
/** Вызывает сервис классификатора и возвращает сырые метаданные платформ. */
|
||||
import space.nstart.pcp_tgu_service.config.ClassifierProperties
|
||||
import space.nstart.pcp_tgu_service.dto.PlatformsResponseDTO
|
||||
import space.nstart.pcp_tgu_service.dto.NsiPlatformDto
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
|
||||
@@ -19,13 +19,13 @@ class PlatformsClassifierClient(
|
||||
.build()
|
||||
|
||||
/**
|
||||
* Загружает полный ответ классификатора платформ.
|
||||
* Загружает список платформ из классификатора НСИ.
|
||||
*/
|
||||
/** Загружает полный ответ классификатора вместе с содержимым платформ. */
|
||||
fun fetchPlatforms(): PlatformsResponseDTO =
|
||||
fun fetchPlatforms(): List<NsiPlatformDto> =
|
||||
webClient.get()
|
||||
.retrieve()
|
||||
.bodyToMono(PlatformsResponseDTO::class.java)
|
||||
.bodyToMono(Array<NsiPlatformDto>::class.java)
|
||||
.map { platforms -> platforms.toList() }
|
||||
.block()
|
||||
?: throw IllegalStateException("Classifier response body is empty: ${classifierProperties.platformsUrl}")
|
||||
}
|
||||
|
||||
+41
-14
@@ -1,29 +1,56 @@
|
||||
package space.nstart.pcp_tgu_service.service
|
||||
|
||||
/** Загружает метаданные платформ КА и применяет простую in-memory фильтрацию. */
|
||||
import space.nstart.pcp_tgu_service.dto.ContentItemDTO
|
||||
import space.nstart.pcp_tgu_service.config.ClassifierProperties
|
||||
import space.nstart.pcp_tgu_service.domain.PlatformCatalogItem
|
||||
import space.nstart.pcp_tgu_service.dto.NsiPlatformDto
|
||||
import space.nstart.pcp_tgu_service.integration.api.PlatformsClassifierClient
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import java.time.Instant
|
||||
|
||||
@Service
|
||||
class PlatformService(
|
||||
private val platformsClassifierClient: PlatformsClassifierClient
|
||||
private val platformsClassifierClient: PlatformsClassifierClient,
|
||||
private val classifierProperties: ClassifierProperties
|
||||
) {
|
||||
/** Возвращает платформы из классификатора с необязательной фильтрацией по коду типа и статусу. */
|
||||
fun loadPlatforms(
|
||||
platformTypeCode: String? = null,
|
||||
status: String? = null
|
||||
): List<ContentItemDTO> {
|
||||
private val log = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
val platforms = platformsClassifierClient.fetchPlatforms().content.orEmpty()
|
||||
/** Возвращает платформы из классификатора с необязательной фильтрацией по статусу НСИ. */
|
||||
fun loadPlatforms(status: String? = null): List<PlatformCatalogItem> {
|
||||
val platforms = platformsClassifierClient.fetchPlatforms()
|
||||
.mapNotNull { platform -> platform.toCatalogItem() }
|
||||
|
||||
return platforms.filter { platform ->
|
||||
val matchesPlatformTypeCode = platformTypeCode.isNullOrBlank() ||
|
||||
platform.data?.platformTypeCode == platformTypeCode
|
||||
val matchesStatus = status.isNullOrBlank() ||
|
||||
platform.status == status
|
||||
return platforms.filter { platform -> status.isNullOrBlank() || platform.status == status }
|
||||
}
|
||||
|
||||
matchesPlatformTypeCode && matchesStatus
|
||||
/** Возвращает платформы, с которыми TGU разрешено работать сейчас. */
|
||||
fun loadAllowedPlatforms(now: Instant = Instant.now()): List<PlatformCatalogItem> =
|
||||
loadPlatforms().filter { platform ->
|
||||
platform.noradId != null &&
|
||||
platform.status in classifierProperties.allowedPlatformStatuses &&
|
||||
platform.validFrom != null &&
|
||||
platform.validTo != null &&
|
||||
!platform.validFrom.isAfter(now) &&
|
||||
now.isBefore(platform.validTo)
|
||||
}
|
||||
|
||||
private fun NsiPlatformDto.toCatalogItem(): PlatformCatalogItem? {
|
||||
val nsiId = id
|
||||
if (nsiId == null) {
|
||||
log.warn("Skipping NSI platform without id. businessKey={}, noradId={}", businessKey, data?.noradId)
|
||||
return null
|
||||
}
|
||||
|
||||
return PlatformCatalogItem(
|
||||
nsiId = nsiId,
|
||||
businessKey = businessKey,
|
||||
name = data?.name,
|
||||
status = data?.status,
|
||||
noradId = data?.noradId,
|
||||
mission = data?.mission,
|
||||
validFrom = validFrom,
|
||||
validTo = validTo
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -151,8 +151,8 @@ class SatelliteMissionProcessService(
|
||||
/** Проверяет, что КА присутствует во внешнем классификаторе платформ. */
|
||||
private fun ensurePlatformExists(noradId: Long) {
|
||||
/** Признак того, что платформа с указанным NORAD-идентификатором существует во внешнем классификаторе. */
|
||||
val platformExists: Boolean = platformService.loadPlatforms()
|
||||
.any { platform -> platform.data?.noradId == noradId }
|
||||
val platformExists: Boolean = platformService.loadAllowedPlatforms()
|
||||
.any { platform -> platform.noradId == noradId }
|
||||
|
||||
if (!platformExists) {
|
||||
throw ResponseStatusException(
|
||||
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package space.nstart.pcp_tgu_service.integration.api
|
||||
|
||||
import com.sun.net.httpserver.HttpServer
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito.mock
|
||||
import space.nstart.pcp_tgu_service.config.ClassifierProperties
|
||||
import space.nstart.pcp_tgu_service.config.ExternalApiProperties
|
||||
import space.nstart.pcp_tgu_service.config.PlanningProperties
|
||||
import space.nstart.pcp_tgu_service.domain.PlatformCatalogItem
|
||||
import space.nstart.pcp_tgu_service.service.PlatformService
|
||||
import java.net.InetSocketAddress
|
||||
import java.time.Instant
|
||||
import java.util.Collections
|
||||
|
||||
class ExternalPointsClientTest {
|
||||
|
||||
@Test
|
||||
fun `fetchAllSpacecraftPoints uses allowed platforms and keeps noradId as spacecraftId`() {
|
||||
val requestedPaths = Collections.synchronizedList(mutableListOf<String>())
|
||||
|
||||
withServer(requestedPaths) { baseUrl ->
|
||||
val client = ExternalPointsClient(
|
||||
externalApiProperties = ExternalApiProperties(baseUrl = baseUrl),
|
||||
platformService = FixedPlatformService(
|
||||
listOf(
|
||||
platform(nsiId = "allowed-1", noradId = 25544L),
|
||||
platform(nsiId = "allowed-2", noradId = 41386L)
|
||||
)
|
||||
),
|
||||
planningProperties = planningProperties()
|
||||
)
|
||||
|
||||
val points = client.fetchAllSpacecraftPoints()
|
||||
|
||||
assertEquals(1, points.size)
|
||||
assertEquals("25544", points.single().spacecraftId)
|
||||
assertEquals(listOf("7"), points.single().points.map { it.pointId })
|
||||
assertEquals(1, points.single().points.single().observationWindows.size)
|
||||
assertEquals(
|
||||
listOf(
|
||||
"/api/satellites/25544/rva",
|
||||
"/api/satellites/41386/rva"
|
||||
),
|
||||
requestedPaths.map { path -> path.substringBefore("?") }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class FixedPlatformService(
|
||||
private val allowedPlatforms: List<PlatformCatalogItem>
|
||||
) : PlatformService(
|
||||
mock(PlatformsClassifierClient::class.java),
|
||||
ClassifierProperties(
|
||||
platformsUrl = "http://localhost",
|
||||
allowedPlatformStatuses = setOf("OPERATIONAL", "STANDBY")
|
||||
)
|
||||
) {
|
||||
override fun loadPlatforms(status: String?): List<PlatformCatalogItem> =
|
||||
error("ExternalPointsClient must use loadAllowedPlatforms")
|
||||
|
||||
override fun loadAllowedPlatforms(now: Instant): List<PlatformCatalogItem> = allowedPlatforms
|
||||
}
|
||||
|
||||
private fun withServer(requestedPaths: MutableList<String>, block: (String) -> Unit) {
|
||||
val server = HttpServer.create(InetSocketAddress(0), 0)
|
||||
server.createContext("/") { exchange ->
|
||||
requestedPaths.add(exchange.requestURI.toString())
|
||||
val responseBody = when (exchange.requestURI.path) {
|
||||
"/api/satellites/25544/rva" -> """[{"noradId":25544,"stationId":7,"revolution":1,"onStart":{"time":"2026-05-26T10:00:00Z","elevation":1.0},"onMaximum":{"time":"2026-05-26T10:05:00Z","elevation":45.0},"onStop":{"time":"2026-05-26T10:10:00Z","elevation":1.0}}]"""
|
||||
else -> """{"error":"failed"}"""
|
||||
}
|
||||
val status = if (exchange.requestURI.path == "/api/satellites/25544/rva") 200 else 500
|
||||
val responseBytes = responseBody.toByteArray()
|
||||
exchange.responseHeaders.add("Content-Type", "application/json")
|
||||
exchange.sendResponseHeaders(status, responseBytes.size.toLong())
|
||||
exchange.responseBody.use { body -> body.write(responseBytes) }
|
||||
}
|
||||
server.start()
|
||||
try {
|
||||
block("http://localhost:${server.address.port}")
|
||||
} finally {
|
||||
server.stop(0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun platform(nsiId: String, noradId: Long): PlatformCatalogItem =
|
||||
PlatformCatalogItem(
|
||||
nsiId = nsiId,
|
||||
businessKey = "business-$nsiId",
|
||||
name = "Satellite $nsiId",
|
||||
status = "OPERATIONAL",
|
||||
noradId = noradId,
|
||||
mission = "MISSION",
|
||||
validFrom = Instant.parse("2026-01-01T00:00:00Z"),
|
||||
validTo = Instant.parse("9999-12-31T23:59:59Z")
|
||||
)
|
||||
|
||||
private fun planningProperties(): PlanningProperties =
|
||||
PlanningProperties(
|
||||
notificationBeforeStartMinutes = 30,
|
||||
targetPlanDurationMinutes = 24 * 60L,
|
||||
targetPlanDurationDeltaMinutes = 60L,
|
||||
externalUpdateEnabled = false,
|
||||
externalPointsLookaheadDays = 3,
|
||||
inMemoryHistoryDays = 14,
|
||||
externalUpdateFixedDelayMs = 0,
|
||||
notificationCheckFixedDelayMs = 30_000
|
||||
)
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package space.nstart.pcp_tgu_service.integration.api
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.sun.net.httpserver.HttpServer
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.Test
|
||||
import space.nstart.pcp_tgu_service.config.ClassifierProperties
|
||||
import space.nstart.pcp_tgu_service.dto.NsiPlatformDto
|
||||
import java.net.InetSocketAddress
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
|
||||
class PlatformsClassifierClientTest {
|
||||
|
||||
@Test
|
||||
fun `sample platforms json is deserialized as root array and ignores unknown fields`() {
|
||||
val objectMapper = jacksonObjectMapper().findAndRegisterModules()
|
||||
val json = Files.readString(Path.of("platforms.json"))
|
||||
|
||||
val platforms = objectMapper.readValue(json, Array<NsiPlatformDto>::class.java).toList()
|
||||
|
||||
val operationalPlatform = platforms.first { platform -> platform.data?.status == "OPERATIONAL" }
|
||||
assertNotNull(operationalPlatform.id)
|
||||
assertNotNull(operationalPlatform.businessKey)
|
||||
assertNotNull(operationalPlatform.data?.name)
|
||||
assertEquals("OPERATIONAL", operationalPlatform.data?.status)
|
||||
assertNotNull(operationalPlatform.data?.noradId)
|
||||
assertNotNull(operationalPlatform.validFrom)
|
||||
assertNotNull(operationalPlatform.validTo)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetchPlatforms reads root array response`() {
|
||||
withServer("""[{"id":"nsi-id","businessKey":"business-key","data":{"name":"Satellite","status":"OPERATIONAL","norad_id":25544,"mission":"MISSION","orbit":{"type":"SSO"}},"validFrom":"2026-01-01T00:00:00Z","validTo":"9999-12-31T23:59:59Z","_meta":{"locale":"en-US"}}]""") { baseUrl ->
|
||||
val client = PlatformsClassifierClient(
|
||||
ClassifierProperties(
|
||||
platformsUrl = baseUrl,
|
||||
allowedPlatformStatuses = setOf("OPERATIONAL", "STANDBY")
|
||||
)
|
||||
)
|
||||
|
||||
val platforms = client.fetchPlatforms()
|
||||
|
||||
assertEquals(1, platforms.size)
|
||||
assertEquals("nsi-id", platforms.single().id)
|
||||
assertEquals("business-key", platforms.single().businessKey)
|
||||
assertEquals("Satellite", platforms.single().data?.name)
|
||||
assertEquals("OPERATIONAL", platforms.single().data?.status)
|
||||
assertEquals(25544L, platforms.single().data?.noradId)
|
||||
assertEquals("MISSION", platforms.single().data?.mission)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetchPlatforms throws clear error on empty body`() {
|
||||
withServer("") { baseUrl ->
|
||||
val client = PlatformsClassifierClient(
|
||||
ClassifierProperties(
|
||||
platformsUrl = baseUrl,
|
||||
allowedPlatformStatuses = setOf("OPERATIONAL", "STANDBY")
|
||||
)
|
||||
)
|
||||
|
||||
val exception = assertThrows(IllegalStateException::class.java) {
|
||||
client.fetchPlatforms()
|
||||
}
|
||||
|
||||
assertEquals("Classifier response body is empty: $baseUrl", exception.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun withServer(responseBody: String, block: (String) -> Unit) {
|
||||
val server = HttpServer.create(InetSocketAddress(0), 0)
|
||||
server.createContext("/") { exchange ->
|
||||
val responseBytes = responseBody.toByteArray()
|
||||
exchange.responseHeaders.add("Content-Type", "application/json")
|
||||
exchange.sendResponseHeaders(200, responseBytes.size.toLong())
|
||||
exchange.responseBody.use { body -> body.write(responseBytes) }
|
||||
}
|
||||
server.start()
|
||||
try {
|
||||
block("http://localhost:${server.address.port}")
|
||||
} finally {
|
||||
server.stop(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package space.nstart.pcp_tgu_service.service
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.`when`
|
||||
import space.nstart.pcp_tgu_service.config.ClassifierProperties
|
||||
import space.nstart.pcp_tgu_service.dto.NsiPlatformDataDto
|
||||
import space.nstart.pcp_tgu_service.dto.NsiPlatformDto
|
||||
import space.nstart.pcp_tgu_service.integration.api.PlatformsClassifierClient
|
||||
import java.time.Instant
|
||||
|
||||
class PlatformServiceTest {
|
||||
|
||||
@Test
|
||||
fun `loadPlatforms maps NSI DTO and filters by status`() {
|
||||
val client = mock(PlatformsClassifierClient::class.java)
|
||||
`when`(client.fetchPlatforms()).thenReturn(
|
||||
listOf(
|
||||
platformDto(id = "operational-id", status = "OPERATIONAL", noradId = 25544L),
|
||||
platformDto(id = "standby-id", status = "STANDBY", noradId = 41386L),
|
||||
platformDto(id = "lost-id", status = "LOST", noradId = 39186L),
|
||||
platformDto(id = null, status = "OPERATIONAL", noradId = 2L)
|
||||
)
|
||||
)
|
||||
val service = service(client)
|
||||
|
||||
val allPlatforms = service.loadPlatforms()
|
||||
val platforms = service.loadPlatforms(status = "STANDBY")
|
||||
|
||||
assertEquals(listOf("operational-id", "standby-id", "lost-id"), allPlatforms.map { it.nsiId })
|
||||
assertEquals(1, platforms.size)
|
||||
assertEquals("standby-id", platforms.single().nsiId)
|
||||
assertEquals("business-standby-id", platforms.single().businessKey)
|
||||
assertEquals("Satellite standby-id", platforms.single().name)
|
||||
assertEquals("STANDBY", platforms.single().status)
|
||||
assertEquals(41386L, platforms.single().noradId)
|
||||
assertEquals("MISSION", platforms.single().mission)
|
||||
assertEquals(Instant.parse("2026-01-01T00:00:00Z"), platforms.single().validFrom)
|
||||
assertEquals(Instant.parse("2026-12-31T00:00:00Z"), platforms.single().validTo)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadAllowedPlatforms uses configured statuses noradId and validity interval`() {
|
||||
val now = Instant.parse("2026-05-26T12:00:00Z")
|
||||
val client = mock(PlatformsClassifierClient::class.java)
|
||||
`when`(client.fetchPlatforms()).thenReturn(
|
||||
listOf(
|
||||
platformDto(id = "operational-id", status = "OPERATIONAL", noradId = 1L),
|
||||
platformDto(id = "standby-id", status = "STANDBY", noradId = 2L),
|
||||
platformDto(id = "lost-id", status = "LOST", noradId = 3L),
|
||||
platformDto(id = "without-norad-id", status = "OPERATIONAL", noradId = null),
|
||||
platformDto(id = "future-id", status = "OPERATIONAL", noradId = 4L, validFrom = "2026-06-01T00:00:00Z"),
|
||||
platformDto(id = "expired-id", status = "OPERATIONAL", noradId = 5L, validTo = "2026-05-26T12:00:00Z"),
|
||||
platformDto(id = "without-valid-from", status = "OPERATIONAL", noradId = 6L, validFrom = null),
|
||||
platformDto(id = "without-valid-to", status = "OPERATIONAL", noradId = 7L, validTo = null)
|
||||
)
|
||||
)
|
||||
val service = service(client)
|
||||
|
||||
val allowedPlatforms = service.loadAllowedPlatforms(now)
|
||||
|
||||
assertEquals(listOf("operational-id", "standby-id"), allowedPlatforms.map { it.nsiId })
|
||||
}
|
||||
|
||||
private fun service(client: PlatformsClassifierClient): PlatformService =
|
||||
PlatformService(
|
||||
platformsClassifierClient = client,
|
||||
classifierProperties = ClassifierProperties(
|
||||
platformsUrl = "http://localhost",
|
||||
allowedPlatformStatuses = setOf("OPERATIONAL", "STANDBY")
|
||||
)
|
||||
)
|
||||
|
||||
private fun platformDto(
|
||||
id: String?,
|
||||
status: String,
|
||||
noradId: Long?,
|
||||
validFrom: String? = "2026-01-01T00:00:00Z",
|
||||
validTo: String? = "2026-12-31T00:00:00Z"
|
||||
): NsiPlatformDto =
|
||||
NsiPlatformDto(
|
||||
id = id,
|
||||
businessKey = id?.let { "business-$it" },
|
||||
data = NsiPlatformDataDto(
|
||||
name = id?.let { "Satellite $it" },
|
||||
status = status,
|
||||
noradId = noradId,
|
||||
mission = "MISSION"
|
||||
),
|
||||
validFrom = validFrom?.let(Instant::parse),
|
||||
validTo = validTo?.let(Instant::parse)
|
||||
)
|
||||
}
|
||||
+42
-20
@@ -13,11 +13,11 @@ import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import space.nstart.pcp_tgu_service.config.ClassifierProperties
|
||||
import space.nstart.pcp_tgu_service.config.PlanningProperties
|
||||
import space.nstart.pcp_tgu_service.domain.Plan
|
||||
import space.nstart.pcp_tgu_service.domain.PlatformCatalogItem
|
||||
import space.nstart.pcp_tgu_service.domain.SpacecraftPoints
|
||||
import space.nstart.pcp_tgu_service.dto.ContentDataDTO
|
||||
import space.nstart.pcp_tgu_service.dto.ContentItemDTO
|
||||
import space.nstart.pcp_tgu_service.dto.CreateSatelliteMissionVariables
|
||||
import space.nstart.pcp_tgu_service.dto.PointMessage
|
||||
import space.nstart.pcp_tgu_service.dto.SpacecraftPointsMessage
|
||||
@@ -28,6 +28,7 @@ import space.nstart.pcp_tgu_service.integration.api.PlatformsClassifierClient
|
||||
import space.nstart.pcp_tgu_service.repository.PlanRepository
|
||||
import space.nstart.pcp_tgu_service.repository.SpacecraftPointsRepository
|
||||
import space.nstart.pcp_tgu_service.repository.TrackedPlanJpaRepository
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@@ -199,10 +200,36 @@ class SatelliteMissionProcessServiceTest {
|
||||
verify(planQueryService).reuseExistingPlan(waitingPlan)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `startCreateSatelliteMissionManually returns not found when platform is not allowed`() {
|
||||
val service = SatelliteMissionProcessService(
|
||||
camundaClient = mock(CamundaClient::class.java),
|
||||
platformService = FixedPlatformService(emptyList()),
|
||||
spacecraftPointsService = FixedSpacecraftPointsService(
|
||||
SpacecraftPointsMessage("25544", emptyList())
|
||||
),
|
||||
planCalculationService = mock(PlanCalculationService::class.java),
|
||||
planQueryService = mock(PlanQueryService::class.java),
|
||||
trackedPlanService = FixedTrackedPlanService()
|
||||
)
|
||||
|
||||
val exception = assertThrows(ResponseStatusException::class.java) {
|
||||
service.startCreateSatelliteMissionManually(25544L)
|
||||
}
|
||||
|
||||
assertEquals(HttpStatus.NOT_FOUND, exception.statusCode)
|
||||
}
|
||||
|
||||
private class FixedPlatformService(
|
||||
private val platforms: List<ContentItemDTO>
|
||||
) : PlatformService(mock(PlatformsClassifierClient::class.java)) {
|
||||
override fun loadPlatforms(platformTypeCode: String?, status: String?): List<ContentItemDTO> = platforms
|
||||
private val platforms: List<PlatformCatalogItem>
|
||||
) : PlatformService(
|
||||
mock(PlatformsClassifierClient::class.java),
|
||||
ClassifierProperties(
|
||||
platformsUrl = "http://localhost",
|
||||
allowedPlatformStatuses = setOf("OPERATIONAL", "STANDBY")
|
||||
)
|
||||
) {
|
||||
override fun loadAllowedPlatforms(now: Instant): List<PlatformCatalogItem> = platforms
|
||||
}
|
||||
|
||||
private class FixedSpacecraftPointsService(
|
||||
@@ -284,21 +311,16 @@ class SatelliteMissionProcessServiceTest {
|
||||
}
|
||||
}
|
||||
|
||||
private fun platform(noradId: Long): ContentItemDTO =
|
||||
ContentItemDTO(
|
||||
uuid = UUID.randomUUID().toString(),
|
||||
parentItemUuid = null,
|
||||
status = "ACTIVE",
|
||||
initiation = null,
|
||||
expiration = null,
|
||||
data = ContentDataDTO(
|
||||
platformTypeCode = null,
|
||||
noradId = noradId,
|
||||
umn = null,
|
||||
name = "satellite",
|
||||
description = null,
|
||||
platformTypeName = null
|
||||
)
|
||||
private fun platform(noradId: Long): PlatformCatalogItem =
|
||||
PlatformCatalogItem(
|
||||
nsiId = UUID.randomUUID().toString(),
|
||||
businessKey = "business-key-$noradId",
|
||||
name = "satellite",
|
||||
status = "OPERATIONAL",
|
||||
noradId = noradId,
|
||||
mission = "MISSION",
|
||||
validFrom = Instant.parse("2026-01-01T00:00:00Z"),
|
||||
validTo = Instant.parse("9999-12-31T23:59:59Z")
|
||||
)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
|
||||
Reference in New Issue
Block a user