AsyncAPI 3.x: an E2E test, NCS driven through Kafka - #1810
Draft
LautaroPetaccio wants to merge 4 commits into
Draft
LautaroPetaccio wants to merge 4 commits into
LautaroPetaccio wants to merge 4 commits into
Conversation
The first search against a real broker. The SUT is the NCS case study re-expressed as a Kafka service: six request topics answered on six reply topics with a result or an error, as ncs-kafka.yaml describes. It speaks only Kafka; there is no HTTP endpoint. Its routines are ported from EMB's NCS and cited as such. The driver owns the broker, a testcontainers apache/kafka, and is the reference implementation of SutController.executeAsyncApiAction: publish with the correlation id where the document says it goes, wait for the reply that carries it back, report and do not judge. kafka-clients and testcontainers-kafka are used only here, in the test tree, and are managed in the root pom like everything else. The test asserts that every operation answers and that expint and gammq each reach both their declared replies. Those two are the operations whose rejected inputs the schema does not rule out; bessj's and remainder's bounds are in the schema, so their errors need invalid data, which the search does not publish yet. 662 targets in 26 seconds, on two runs.
Neither side of the experimental oracle gate was exercised against a live broker. A second search now runs with the oracles on and a reply deadline no round trip can meet, so every message goes unanswered and the fault is reported, while the existing search asserts that a service which answers correctly has nothing reported against it. Nothing in the service is broken on purpose for this. The driver ignores replies whose correlation id does not match the request it is waiting on, so the late answers piling up on the reply topics cannot be taken for fresh ones.
The search was covered by a run against a real broker; what the run wrote was not. Both output languages are now generated, compiled and executed against the same broker: Kotlin in the existing run, Java in a second one, because a Java half that never compiles is invisible to a Kotlin-only check. The service gains one deliberate defect, marked as such where it is written: a non-positive triangle edge is answered with a payload the document declares nowhere for that reply. It correlates, it arrives, and a client written against the contract still cannot read it, so it is the one fault only the reply classifier finds. The suite is expected to report it. The two replies whose errors no conforming client can reach say so in the document, beside the reply itself, so the next reader does not take them for a gap in the search. The driver compared the correlation location against the two string constants that have since become an enum, and the shared compile step assumed Kotlin sources.
The module starts a broker in a container, so it wants Docker and it is slower than the rest. It gets a profile of its own and a flag every other profile turns off, which is how the database E2Es are kept out of the runs that have no business paying for them, and CI gains the job that turns it on.
LautaroPetaccio
added this pull request to stack #1812
September 30, 2026 17:25
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Last of the AsyncAPI 3.x series, on top of #1809. Replaces #1760, which could not be retargeted in place.
The SUT — NCS over Kafka
The NCS case study re-expressed as a message-driven service: six request topics (
ncs.<op>.request), each answered on a reply topic with a result message or, for the inputs the REST version answers with a 400, an error message — exactly whatncs-kafka.yaml(already in core's test resources) describes. The routines are ported from EMB'sorg.restncs.impand cited at the top of each file; the rejection rules mirrorNcsRest. A Spring Boot application with no web layer: it speaks only Kafka, which is the point.One deliberate deviation: a result that is
NaNor infinite is answered as an error. JSON has no such numbers, and a string where the contract promises a number would be a false fault.And one deliberate defect, marked as such in
NcsServicewhere it is written: a triangle with a non-positive edge is answered with{"classification": ...}, a shape the document declares nowhere for that reply. The original NCS routine classifies it as0and answers with the declared result. It is here because the reply classifier's fault needs a service that commits it: the message arrives, it correlates, nothing times out, and a client written against the contract still cannot read it. Nothing else in the run notices, and the generated suite is expected to report it. Worth knowing before reading the fixture as a faithful port — it is faithful everywhere else.The driver — the reference
executeAsyncApiActionNcsKafkaControllerowns the broker (testcontainers,apache/kafka:3.8.0, KRaft), creates the twelve topics up front, and implements the hook exactly as #1738's javadoc sketches it:replyTimeoutMsruns out; a reply carrying someone else's id is skipped, one carrying none is taken and reported ascorrelationMatched = false;AsyncApiReplyDtoand judge nothing.It also answers
getAsyncApiServerAddress, so a generated test reaches the broker this run actually started rather than the address the document declares.kafka-clientsandtestcontainers-kafkaappear only in this test module, managed in the rootpom.xmlas the doc requires. Nothing is added to any shipped jar.The search
AsyncApiTestBasejoins the other bases ine2e-tests-utils:initAndRun, and helpers that readAsyncApiCallResultoff the solution.NcsKafkaEMTestruns 300 action evaluations and asserts:checkTriangle,bessjandremainderreached their result reply;expintandgammqreached both their result and their error — the two operations whose rejected inputs (x < 0;a ≤ 0 || x < 0) the schema does not rule out.bessj's order andremainder's operands are bounded in the schema, so their error replies need invalid data, which the search does not publish yet. Both documents now say so beside the reply, so the next reader does not take it for a gap;checkTriangle, and nothing is reported against the operations that answer correctly;NO_REPLY, noPUBLISH_FAILED.A second search runs with the oracles on and a reply deadline no round trip can meet, so every message goes unanswered and the no-reply fault is reported. Neither side of that gate had been exercised against a live broker before.
Because the driver is embedded, the SUT is instrumented, so the search covers lines and branches of a message-driven service as well: 662 targets in 26 seconds, identical on two runs.
The generated suite
The run now also writes the suite, compiles it and runs it against the same broker, so #1809 is covered by the same E2E. Both output languages are generated: Kotlin in the first run, Java in a second one, because a Java half that never compiles is invisible to a Kotlin-only check. The shared compile step learned to tell
.javasources from.ktones to make that possible.CI
The module starts a broker in a container, so it wants Docker and is slower than the rest. It gets an
AsyncApiTestsprofile and askipAsyncApiflag every other profile turns off, which is how the database E2Es are kept out of runs that have no business paying for them, and CI gains the matching job.