Init
This commit is contained in:
+13
@@ -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() {
|
||||
}
|
||||
|
||||
}
|
||||
+106
@@ -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
|
||||
)
|
||||
}
|
||||
+96
@@ -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
|
||||
}
|
||||
}
|
||||
+125
@@ -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 }
|
||||
}
|
||||
}
|
||||
+56
@@ -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
|
||||
Reference in New Issue
Block a user