- 新增 POST /refdata/sync:STYP 14 类校验,RSTA 须带 RTYP - REQ_TRACK 加 styp/rtyp 列(V8 迁移);RQRD/RQFD 各自单开放槽 - OutboundRequestService 加 registerRqrdSync,派发按行目标编码 - Pump 加 REF-RESP 守卫:无在途 RQRD 时 SKIPPED - 回归测试覆盖 C-12 登记/派发/400/409/独立槽/守卫
203 lines
8.2 KiB
Kotlin
203 lines
8.2 KiB
Kotlin
package com.gzzn.omms.msgexchange.processing
|
|
|
|
import com.gzzn.omms.msgexchange.MutableClock
|
|
import com.gzzn.omms.msgexchange.codec.JacksonXmlCodec
|
|
import com.gzzn.omms.msgexchange.config.OperationDayProps
|
|
import com.gzzn.omms.msgexchange.config.PipelineProps
|
|
import com.gzzn.omms.msgexchange.domain.OutboundRequestKeys
|
|
import com.gzzn.omms.msgexchange.domain.RqfdTimeFilter
|
|
import com.gzzn.omms.msgexchange.ingress.SchdSyncService
|
|
import com.gzzn.omms.msgexchange.infra.persistence.CoutmsgOutboxRepository
|
|
import com.gzzn.omms.msgexchange.infra.persistence.ReqTrackRepository
|
|
import com.gzzn.omms.msgexchange.infra.stub.StubCoutmsgOutbox
|
|
import com.gzzn.omms.msgexchange.infra.stub.StubReqTrack
|
|
import org.junit.jupiter.api.Assertions.assertEquals
|
|
import org.junit.jupiter.api.Assertions.assertFalse
|
|
import org.junit.jupiter.api.Assertions.assertTrue
|
|
import org.junit.jupiter.api.Test
|
|
import java.time.Duration
|
|
import java.time.LocalDate
|
|
|
|
class OutboundRequestTest {
|
|
|
|
private val clock = MutableClock(MutableClock.BASE)
|
|
private val opDay = OperationDayProps().apply { zone = "Asia/Shanghai"; cutoffHour = 0 }
|
|
private val props = PipelineProps().apply { outbound.responseTimeout = Duration.ofMinutes(5) }
|
|
|
|
private fun service(req: StubReqTrack, outbox: StubCoutmsgOutbox) =
|
|
OutboundRequestService(req, outbox, JacksonXmlCodec(), props, opDay, clock)
|
|
|
|
@Test
|
|
fun `schd sync registers PENDING then confirms COUTMSGS as SENT`() {
|
|
val req = StubReqTrack(clock)
|
|
val outbox = StubCoutmsgOutbox()
|
|
val svc = service(req, outbox)
|
|
|
|
val outcome = svc.registerRqfdSync()
|
|
assertTrue(outcome is OutboundRequestService.RegisterOutcome.Registered)
|
|
val open = req.findLatest(
|
|
OutboundRequestKeys.RQFD_REQ_TYPE,
|
|
LocalDate.of(2026, 9, 6),
|
|
OutboundRequestKeys.SENDER,
|
|
listOf(ReqTrackRepository.ReqState.SENT),
|
|
)!!
|
|
assertEquals(1L, open.coutmsgsId)
|
|
assertTrue(outbox.messages.containsValue(outbox.messages.values.first()))
|
|
assertTrue(outbox.messages.values.first().contains("<SNDR>OMMS</SNDR>"))
|
|
assertTrue(outbox.messages.values.first().contains("<TYPE>RQFD</TYPE>"))
|
|
}
|
|
|
|
@Test
|
|
fun `schd sync writes the page time filter into COUTMSGS and leaves out the rest`() {
|
|
val req = StubReqTrack(clock)
|
|
val outbox = StubCoutmsgOutbox()
|
|
val svc = service(req, outbox)
|
|
|
|
svc.registerRqfdSync(RqfdTimeFilter(stdb = "12JAN041730"))
|
|
val xml = outbox.messages.values.first()
|
|
assertTrue(xml.contains("<STDB>12JAN041730</STDB>"))
|
|
assertFalse(xml.contains("STDE"))
|
|
assertFalse(xml.contains("ETDB"))
|
|
assertFalse(xml.contains("ETDE"))
|
|
}
|
|
|
|
@Test
|
|
fun `invalid time filter is rejected before registration`() {
|
|
val req = StubReqTrack(clock)
|
|
val sync = SchdSyncService(service(req, StubCoutmsgOutbox()))
|
|
assertEquals(SchdSyncService.Outcome.InvalidTime, sync.trigger(stdb = "today"))
|
|
assertTrue(req.rows.isEmpty())
|
|
}
|
|
|
|
@Test
|
|
fun `open slot rejects duplicate schd sync registration`() {
|
|
val req = StubReqTrack(clock)
|
|
val svc = service(req, StubCoutmsgOutbox())
|
|
svc.registerRqfdSync()
|
|
assertEquals(OutboundRequestService.RegisterOutcome.OpenExists, svc.registerRqfdSync())
|
|
}
|
|
|
|
@Test
|
|
fun `restart retries PENDING without coutmsgs after job dispatch`() {
|
|
val req = StubReqTrack(clock)
|
|
val outbox = StubCoutmsgOutbox()
|
|
val day = LocalDate.of(2026, 9, 6)
|
|
val reqId = req.insert(OutboundRequestKeys.RQFD_REQ_TYPE, day, OutboundRequestKeys.SENDER)
|
|
assertEquals(ReqTrackRepository.ReqState.PENDING, req.rows[reqId]!!.state)
|
|
|
|
val svc = service(req, outbox)
|
|
assertEquals(1, svc.dispatchPending())
|
|
|
|
assertEquals(ReqTrackRepository.ReqState.SENT, req.rows[reqId]!!.state)
|
|
}
|
|
|
|
@Test
|
|
fun `ambiguous write marks uncertain and does not blind resend`() {
|
|
val req = StubReqTrack(clock)
|
|
val outbox = StubCoutmsgOutbox().apply { nextInsertOutcome = CoutmsgOutboxRepository.InsertOutcome.AMBIGUOUS }
|
|
val day = LocalDate.of(2026, 9, 6)
|
|
val reqId = req.insert(OutboundRequestKeys.RQFD_REQ_TYPE, day, OutboundRequestKeys.SENDER)
|
|
val svc = service(req, outbox)
|
|
|
|
assertFalse(svc.dispatchPending() > 0)
|
|
assertTrue(req.rows[reqId]!!.writeUncertain)
|
|
assertEquals(ReqTrackRepository.ReqState.PENDING, req.rows[reqId]!!.state)
|
|
outbox.nextInsertOutcome = CoutmsgOutboxRepository.InsertOutcome.CONFIRMED
|
|
assertEquals(0, svc.dispatchPending())
|
|
}
|
|
|
|
@Test
|
|
fun `timeout expires SENT request`() {
|
|
val req = StubReqTrack(clock)
|
|
val day = LocalDate.of(2026, 9, 6)
|
|
val reqId = req.insert(OutboundRequestKeys.RQFD_REQ_TYPE, day, OutboundRequestKeys.SENDER)
|
|
req.linkCoutmsgs(reqId, 10L, 99L)
|
|
req.markSent(reqId, clock.instant())
|
|
clock.advance(Duration.ofMinutes(10).toMillis())
|
|
|
|
val svc = service(req, StubCoutmsgOutbox())
|
|
assertEquals(1, svc.expireTimedOut())
|
|
assertEquals(ReqTrackRepository.ReqState.EXPIRED, req.rows[reqId]!!.state)
|
|
}
|
|
|
|
@Test
|
|
fun `EROR fails matching open outbound by seqn`() {
|
|
val req = StubReqTrack(clock)
|
|
val day = LocalDate.of(2026, 9, 6)
|
|
val reqId = req.insert(OutboundRequestKeys.RQFD_REQ_TYPE, day, OutboundRequestKeys.SENDER)
|
|
req.linkCoutmsgs(reqId, 1L, 42L)
|
|
req.markSent(reqId, clock.instant())
|
|
|
|
val svc = service(req, StubCoutmsgOutbox())
|
|
assertTrue(svc.failFromEror(42L, "RQFD", "NONE"))
|
|
assertEquals(ReqTrackRepository.ReqState.FAILED, req.rows[reqId]!!.state)
|
|
}
|
|
|
|
@Test
|
|
fun `refdata sync registers AIRL and dispatches RQRD into COUTMSGS`() {
|
|
val req = StubReqTrack(clock)
|
|
val outbox = StubCoutmsgOutbox()
|
|
val sync = com.gzzn.omms.msgexchange.ingress.RefdataSyncService(service(req, outbox))
|
|
|
|
val outcome = sync.trigger("AIRL", null)
|
|
assertTrue(outcome is com.gzzn.omms.msgexchange.ingress.RefdataSyncService.Outcome.Registered)
|
|
val xml = outbox.messages.values.first()
|
|
assertTrue(xml.contains("<TYPE>RQRD</TYPE>"))
|
|
assertTrue(xml.contains("<STYP>AIRL</STYP>"))
|
|
}
|
|
|
|
@Test
|
|
fun `refdata sync writes RTYP only for RSTA`() {
|
|
val req = StubReqTrack(clock)
|
|
val outbox = StubCoutmsgOutbox()
|
|
val svc = service(req, outbox)
|
|
|
|
svc.registerRqrdSync(com.gzzn.omms.msgexchange.domain.RqrdTarget("RSTA", "GATE"))
|
|
val xml = outbox.messages.values.first()
|
|
assertTrue(xml.contains("<STYP>RSTA</STYP>"))
|
|
assertTrue(xml.contains("<RTYP>GATE</RTYP>"))
|
|
}
|
|
|
|
@Test
|
|
fun `refdata sync rejects unknown styp and rtyp misuse before registration`() {
|
|
val req = StubReqTrack(clock)
|
|
val sync = com.gzzn.omms.msgexchange.ingress.RefdataSyncService(service(req, StubCoutmsgOutbox()))
|
|
|
|
assertEquals(
|
|
com.gzzn.omms.msgexchange.ingress.RefdataSyncService.Outcome.UnknownStyp,
|
|
sync.trigger("NOPE", null),
|
|
)
|
|
assertEquals(
|
|
com.gzzn.omms.msgexchange.ingress.RefdataSyncService.Outcome.MissingRtyp,
|
|
sync.trigger("RSTA", null),
|
|
)
|
|
assertEquals(
|
|
com.gzzn.omms.msgexchange.ingress.RefdataSyncService.Outcome.UnexpectedRtyp,
|
|
sync.trigger("AIRL", "GATE"),
|
|
)
|
|
assertTrue(req.rows.isEmpty())
|
|
}
|
|
|
|
@Test
|
|
fun `open slot rejects duplicate refdata sync registration`() {
|
|
val req = StubReqTrack(clock)
|
|
val sync = com.gzzn.omms.msgexchange.ingress.RefdataSyncService(service(req, StubCoutmsgOutbox()))
|
|
|
|
sync.trigger("AIRL", null)
|
|
assertEquals(
|
|
com.gzzn.omms.msgexchange.ingress.RefdataSyncService.Outcome.OpenExists,
|
|
sync.trigger("AIRL", null),
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `RQRD and RQFD use separate open slots`() {
|
|
val req = StubReqTrack(clock)
|
|
val rqrd = com.gzzn.omms.msgexchange.ingress.RefdataSyncService(service(req, StubCoutmsgOutbox()))
|
|
val rqfd = SchdSyncService(service(req, StubCoutmsgOutbox()))
|
|
|
|
rqrd.trigger("AIRL", null)
|
|
assertTrue(rqfd.trigger() is SchdSyncService.Outcome.Registered)
|
|
}
|
|
}
|