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,13 @@
package space.nstart.pcp.pcp_request_service
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
class PcpBallisticsServiceApplicationTests {
@Test
fun contextLoads() {
}
}
@@ -0,0 +1,106 @@
package space.nstart.pcp.pcp_request_service.message
import org.junit.jupiter.api.Test
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.readText
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class SatelliteDeletedKafkaListenerGroupIdTest {
private val listeners = mapOf(
"slots-service" to ListenerExpectation(
path = "services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/service/SatelliteDeletedKafkaListener.kt",
groupId = "pcp-slots-service-satellite-deleted",
hasEventFilter = true
),
"pcp-ballistics-service" to ListenerExpectation(
path = "services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/message/SatelliteDeletedKafkaListener.kt",
groupId = "pcp-ballistics-service-satellite-deleted",
hasEventFilter = true
),
"tle-monitoring-service" to ListenerExpectation(
path = "services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/message/SatelliteDeletedKafkaListener.kt",
groupId = "pcp-tle-monitoring-service-satellite-deleted",
hasEventFilter = true
),
"pcp-complex-mission-service" to ListenerExpectation(
path = "services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/service/SatelliteDeletedKafkaListener.kt",
groupId = "pcp-complex-mission-service-satellite-deleted"
),
"pcp-mission-planing-service" to ListenerExpectation(
path = "services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/SatelliteDeletedKafkaListener.kt",
groupId = "pcp-mission-planing-service-satellite-deleted"
),
"pcp-dynamic-plan-service" to ListenerExpectation(
path = "services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/service/SatelliteDeletedKafkaListener.kt",
groupId = "pcp-dynamic-plan-service-satellite-deleted"
)
)
@Test
fun `satellite deleted listeners use unique explicit consumer groups`() {
val resolvedGroupIds = listeners.mapValues { (_, expectation) ->
val source = projectRoot().resolve(expectation.path).readText()
val annotationBody = kafkaListenerAnnotationBody(source)
val groupReference = annotationParameter(annotationBody, "groupId")
assertNotNull(groupReference, "SatelliteDeletedEvent listener must define explicit groupId: ${expectation.path}")
assertEquals("SATELLITE_DELETED_CONSUMER_GROUP", groupReference)
assertTrue(
annotationBody.contains("""topics = ["\${'$'}{app.kafka.topics.satellites:pcp.satellites}"]"""),
"SatelliteDeletedEvent listener must keep satellite topic: ${expectation.path}"
)
if (expectation.hasEventFilter) {
assertTrue(
annotationBody.contains("""filter = "satelliteDeletedFilter""""),
"SatelliteDeletedEvent listener must keep event filter: ${expectation.path}"
)
}
val groupId = consumerGroupConstantValue(source)
assertEquals(expectation.groupId, groupId)
groupId
}
assertEquals(
resolvedGroupIds.size,
resolvedGroupIds.values.toSet().size,
"SatelliteDeletedEvent consumer groups must be unique per service: $resolvedGroupIds"
)
}
private fun kafkaListenerAnnotationBody(source: String): String {
val match = Regex("@KafkaListener\\((.*?)\\)", RegexOption.DOT_MATCHES_ALL).find(source)
assertNotNull(match, "SatelliteDeletedEvent listener must have @KafkaListener")
return match.groupValues[1]
}
private fun annotationParameter(annotationBody: String, parameterName: String): String? =
Regex("""$parameterName\s*=\s*([^,\n)]+)""")
.find(annotationBody)
?.groupValues
?.get(1)
?.trim()
private fun consumerGroupConstantValue(source: String): String {
val match = Regex("""private const val SATELLITE_DELETED_CONSUMER_GROUP\s*=\s*"([^"]+)"""").find(source)
assertNotNull(match, "SatelliteDeletedEvent listener must declare SATELLITE_DELETED_CONSUMER_GROUP")
return match.groupValues[1]
}
private fun projectRoot(): Path {
generateSequence(Path.of(System.getProperty("user.dir")).toAbsolutePath()) { it.parent }
.firstOrNull { Files.exists(it.resolve("settings.gradle.kts")) }
?.let { return it }
error("Cannot locate project root from ${System.getProperty("user.dir")}")
}
private data class ListenerExpectation(
val path: String,
val groupId: String,
val hasEventFilter: Boolean = false
)
}
@@ -0,0 +1,96 @@
package space.nstart.pcp.pcp_request_service.message
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.ObjectProvider
import space.nstart.pcp.pcp_request_service.service.SatelliteIcEventService
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
import tools.jackson.databind.ObjectMapper
class SatelliteIcRvKafkaListenerTest {
private val objectMapper = ObjectMapper()
private val satelliteIcEventService = RecordingSatelliteIcEventService()
private val listener = SatelliteIcRvKafkaListener(
objectMapperProvider = SingleObjectProvider(objectMapper),
satelliteIcEventService = satelliteIcEventService
)
@Test
fun `listener parses ICRV placed event payload and delegates to service`() {
listener.consume(
"""
{
"type": "ICRVPlacedEvent",
"data": {
"satelliteId": 56756,
"ic": {
"orbPoint": {
"time": "2026-04-16T12:00:00",
"revolution": 42,
"vx": 1.0,
"vy": 2.0,
"vz": 3.0,
"x": 4.0,
"y": 5.0,
"z": 6.0
},
"sBall": 0.7,
"f81": 140.0
}
}
}
""".trimIndent()
)
assertEquals(56756L, satelliteIcEventService.lastMessage?.satelliteId)
assertEquals(42L, satelliteIcEventService.lastMessage?.ic?.orbPoint?.revolution)
assertEquals(140.0, satelliteIcEventService.lastMessage?.ic?.f81)
}
@Test
fun `listener substitutes default initial condition coefficients when payload contains nulls`() {
listener.consume(
"""
{
"type": "ICRVPlacedEvent",
"data": {
"satelliteId": 56756,
"ic": {
"orbPoint": {
"time": "2026-04-16T12:00:00",
"revolution": 42,
"vx": 1.0,
"vy": 2.0,
"vz": 3.0,
"x": 4.0,
"y": 5.0,
"z": 6.0
},
"sBall": null,
"f81": null
}
}
}
""".trimIndent()
)
assertEquals(0.0, satelliteIcEventService.lastMessage?.ic?.sBall)
assertEquals(147.8, satelliteIcEventService.lastMessage?.ic?.f81)
}
private class RecordingSatelliteIcEventService : SatelliteIcEventService() {
var lastMessage: SatelliteICDTO? = null
override fun handlePlacedIcRv(message: SatelliteICDTO) {
lastMessage = message
}
}
private class SingleObjectProvider<T : Any>(private val value: T) : ObjectProvider<T> {
override fun getObject(vararg args: Any?): T = value
override fun getIfAvailable(): T = value
override fun getIfUnique(): T = value
override fun getObject(): T = value
}
}
@@ -0,0 +1,125 @@
package space.nstart.pcp.pcp_request_service.repository
import jakarta.persistence.EntityManager
import org.apache.kafka.clients.consumer.ConsumerConfig
import org.apache.kafka.common.serialization.StringDeserializer
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory
import org.springframework.kafka.core.ConsumerFactory
import org.springframework.kafka.core.DefaultKafkaConsumerFactory
import org.springframework.kafka.listener.adapter.RecordFilterStrategy
import org.springframework.transaction.annotation.Transactional
import space.nstart.pcp.pcp_request_service.entity.PDCMEntity
import java.time.LocalDateTime
import kotlin.test.assertEquals
@SpringBootTest(
properties = [
"spring.cloud.config.enabled=false",
"spring.cloud.config.import-check.enabled=false",
"spring.kafka.bootstrap-servers=false",
"spring.kafka.listener.auto-startup=false",
"spring.datasource.driver-class-name=org.h2.Driver",
"spring.datasource.url=jdbc:h2:mem:pdcm-repository-test;MODE=PostgreSQL;DB_CLOSE_DELAY=-1",
"spring.datasource.username=sa",
"spring.datasource.password=",
"spring.flyway.enabled=false",
"spring.jpa.hibernate.ddl-auto=create-drop",
"spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect"
]
)
@Transactional
class PDCMRepositoryTest {
@Autowired
private lateinit var entityManager: EntityManager
@Autowired
private lateinit var repository: PDCMRepository
@Test
fun `find satellite orbit availability returns all satellites with points when time is null`() {
persistPoint(11L, LocalDateTime.of(2026, 4, 14, 10, 0))
persistPoint(11L, LocalDateTime.of(2026, 4, 14, 12, 0))
persistPoint(22L, LocalDateTime.of(2026, 4, 15, 9, 30))
persistPoint(22L, LocalDateTime.of(2026, 4, 15, 18, 45))
val actual = repository.findSatelliteOrbitAvailability()
assertEquals(2, actual.size)
assertEquals(11L, actual[0].satelliteId)
assertEquals(LocalDateTime.of(2026, 4, 14, 10, 0), actual[0].timeStart)
assertEquals(LocalDateTime.of(2026, 4, 14, 12, 0), actual[0].timeStop)
assertEquals(22L, actual[1].satelliteId)
assertEquals(LocalDateTime.of(2026, 4, 15, 9, 30), actual[1].timeStart)
assertEquals(LocalDateTime.of(2026, 4, 15, 18, 45), actual[1].timeStop)
}
@Test
fun `find satellite orbit availability filters satellites whose interval contains requested time`() {
persistPoint(11L, LocalDateTime.of(2026, 4, 14, 10, 0))
persistPoint(11L, LocalDateTime.of(2026, 4, 14, 12, 0))
persistPoint(22L, LocalDateTime.of(2026, 4, 15, 9, 30))
persistPoint(22L, LocalDateTime.of(2026, 4, 15, 18, 45))
val actual = repository.findSatelliteOrbitAvailabilityAtTime(LocalDateTime.of(2026, 4, 14, 11, 0))
assertEquals(1, actual.size)
assertEquals(11L, actual.single().satelliteId)
assertEquals(LocalDateTime.of(2026, 4, 14, 10, 0), actual.single().timeStart)
assertEquals(LocalDateTime.of(2026, 4, 14, 12, 0), actual.single().timeStop)
}
private fun persistPoint(satelliteId: Long, time: LocalDateTime) {
entityManager.persist(
PDCMEntity(
satelliteId = satelliteId,
time = time,
revolution = 1,
x = 1.0,
y = 2.0,
z = 3.0,
vx = 4.0,
vy = 5.0,
vz = 6.0
)
)
entityManager.flush()
}
@TestConfiguration
class KafkaFilterStubConfiguration {
@Bean("kafkaListenerContainerFactory")
fun kafkaListenerContainerFactory(): ConcurrentKafkaListenerContainerFactory<String, String> =
ConcurrentKafkaListenerContainerFactory<String, String>().apply {
setConsumerFactory(testConsumerFactory())
setAutoStartup(false)
}
private fun testConsumerFactory(): ConsumerFactory<String, String> =
DefaultKafkaConsumerFactory(
mapOf(
ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to "localhost:9092",
ConsumerConfig.GROUP_ID_CONFIG to "test",
ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java,
ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java
)
)
@Bean("placedIcFilter")
fun placedIcFilter(): RecordFilterStrategy<String, String> = RecordFilterStrategy { false }
@Bean("placedIcRvFilter")
fun placedIcRvFilter(): RecordFilterStrategy<String, String> = RecordFilterStrategy { false }
@Bean("updatedIcFilter")
fun updatedIcFilter(): RecordFilterStrategy<String, String> = RecordFilterStrategy { false }
@Bean("satelliteDeletedFilter")
fun satelliteDeletedFilter(): RecordFilterStrategy<String, String> = RecordFilterStrategy { false }
}
}
@@ -0,0 +1,56 @@
package space.nstart.pcp.pcp_request_service.service
import ballistics.types.Orientation
import ballistics.types.PointViewParams
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
class SatelliteServiceDeterminismTest {
@Test
fun `sortPointViews orders mpl points deterministically`() {
val service = SatelliteService()
val sorted = service.sortPointViews(
listOf(
pointView(traverz = 120.0, vit = 3, pointNumb = 2, pv = 1, kren = 0.3),
pointView(traverz = 100.0, vit = 4, pointNumb = 1, pv = 0, kren = 0.5),
pointView(traverz = 100.0, vit = 3, pointNumb = 2, pv = 0, kren = 0.4),
pointView(traverz = 100.0, vit = 3, pointNumb = 1, pv = 1, kren = 0.2)
)
)
assertEquals(
listOf(
100.0 to 3,
100.0 to 3,
100.0 to 4,
120.0 to 3
),
sorted.map { it.traverz to it.vit }
)
assertEquals(listOf(1, 2, 1, 2), sorted.map { it.pointNumb })
}
private fun pointView(
traverz: Double,
vit: Int,
pointNumb: Int,
pv: Int,
kren: Double
) = PointViewParams(
objON = 0,
objN = 0,
objUUID = "",
pointNumb = pointNumb,
vit = vit,
traverz = traverz,
latTraverz = 0.0,
longTraverz = 0.0,
orientation = Orientation(0.0, kren, 0.0),
range = 0.0,
sunAngle = 0.0,
sightAngle = 0.0,
pv = pv
)
}
@@ -0,0 +1,69 @@
spring:
application:
name: pcp-ballistics-service
cloud:
config:
enabled: false
lifecycle.timeout-per-shutdown-phase: 40s
jackson:
default-property-inclusion: non_null
kafka:
bootstrap-servers: 192.168.60.68:29092
consumer:
group-id: pcp-ballistics-service
auto-offset-reset: earliest
template:
default-topic: pcp.tle
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://192.168.60.68:5432/pcp_ballistics
username: postgres
password: password
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
jdbc:
lob:
non_contextual_creation: true
flyway:
enabled: true
baseline-on-migrate: true
locations: classpath:db/migration
codec:
max-in-memory-size: 20MB
springdoc:
swagger-ui:
enabled: true
layout: BaseLayout
path: /swagger/ui
api-docs:
enabled: true
path: /api-docs
logging:
level:
.: ERROR
file:
name: ./logs/application.log
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
info:
enabled: true
settings:
calculation-interval: 28
ic-service: http://192.168.60.68:9080
server:
port: 7003