From 9bbfa2f4b7d519520234424c4a776edfd8aab816 Mon Sep 17 00:00:00 2001 From: Lautaro Petaccio Date: Mon, 24 Aug 2026 14:06:50 -0300 Subject: [PATCH 01/12] AsyncAPI 3.x: the action and individual a search holds Turns the parsed document into something the search can sample and mutate. AsyncApiAction is one message published on one channel, following the shape of RPCCallAction -- the closest analogue in EvoMaster, being a call with no URL, made through a driver, whose mutable state is the input and whose response is read afterwards rather than searched over. What is deliberately not a gene is the point of the design: - the address, which the contract fixes. Sending to a channel the service does not read would only waste executions. - the correlation id, stamped fresh at each execution. Searching over it could achieve nothing, since the service only echoes it, and pairing a reply with its request needs a value unique per execution rather than one carried in the genome. The header it is stamped into is therefore left out of the header genes, and when it is the only header there are no header genes at all -- an empty schema would otherwise build a free-form map and invite the search to invent headers the contract never declared. - the reply, which is an observation, read at fitness time. AsyncApiIndividual is one individual for every transport rather than one per transport: Kafka versus AMQP versus a socket appears nowhere in it. What is searched over is the operation and the payload, which are the same whatever moves the bytes. AsyncApiActionBuilder fills an action cluster, keeping the contract the REST and GraphQL builders use: one entry per action name, and a list of what had to be skipped rather than an exception. Only operations the service consumes become actions, since a 'send' is something to subscribe to rather than to drive. An operation carrying several messages becomes several actions -- which message to send is a choice made by picking an action, not by mutating a gene -- and the message is named in the action only when there is more than one, so a generated test reads as the operation where it can. That multi-message case is not hypothetical, but it does not arise the way one would expect. No document in the corpus narrows to more than one message with a `messages:` array; what happens is the opposite -- an operation declares no array at all, and so inherits everything its channel carries. scalar.yaml is the real case, vendored unmodified under MIT with its source noted: its userEvents channel carries five genuinely different payloads, and subscribeToUserEvents does not say which of them it means. Note RPCIndividual.seeIndexedRPCCalls is not copied over. It looks up children by action type, but the children are enterprise action groups, so it returns nothing; it has no callers. seeMainExecutableActions does the job. --- .../problem/asyncapi/auth/AsyncApiNoAuth.kt | 14 + .../asyncapi/builder/AsyncApiActionBuilder.kt | 132 ++ .../asyncapi/builder/AsyncApiGeneBuilder.kt | 60 +- .../problem/asyncapi/data/AsyncApiAction.kt | 106 + .../asyncapi/data/AsyncApiIndividual.kt | 116 ++ .../problem/asyncapi/param/AsyncApiParam.kt | 21 + .../builder/AsyncApiActionBuilderTest.kt | 453 ++++ .../builder/AsyncApiGeneBuilderTest.kt | 13 +- .../test/resources/asyncapi/sut/microcks.yaml | 418 ++++ .../test/resources/asyncapi/sut/scalar.yaml | 1853 +++++++++++++++++ 10 files changed, 3180 insertions(+), 6 deletions(-) create mode 100644 core/src/main/kotlin/org/evomaster/core/problem/asyncapi/auth/AsyncApiNoAuth.kt create mode 100644 core/src/main/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiActionBuilder.kt create mode 100644 core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiAction.kt create mode 100644 core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividual.kt create mode 100644 core/src/main/kotlin/org/evomaster/core/problem/asyncapi/param/AsyncApiParam.kt create mode 100644 core/src/test/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiActionBuilderTest.kt create mode 100644 core/src/test/resources/asyncapi/sut/microcks.yaml create mode 100644 core/src/test/resources/asyncapi/sut/scalar.yaml diff --git a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/auth/AsyncApiNoAuth.kt b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/auth/AsyncApiNoAuth.kt new file mode 100644 index 0000000000..c81e8d7e71 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/auth/AsyncApiNoAuth.kt @@ -0,0 +1,14 @@ +package org.evomaster.core.problem.asyncapi.auth + +import org.evomaster.core.problem.enterprise.auth.AuthenticationInfo +import org.evomaster.core.problem.enterprise.auth.NoAuth + +/** + * An AsyncAPI action with no authentication set up. + * + * Authentication in AsyncAPI is a property of the connection to the broker rather than of an + * individual message: a security scheme is declared on a server, and the client is already + * authenticated by the time a message is published. So there is nothing per-action to vary + * yet, and every action carries this. + */ +class AsyncApiNoAuth : AuthenticationInfo(NoAuth.NAME), NoAuth diff --git a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiActionBuilder.kt b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiActionBuilder.kt new file mode 100644 index 0000000000..104253abf3 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiActionBuilder.kt @@ -0,0 +1,132 @@ +package org.evomaster.core.problem.asyncapi.builder + +import com.webfuzzing.asyncapi.models.AsyncApiDocument +import com.webfuzzing.asyncapi.models.AsyncApiOperation +import org.evomaster.core.problem.api.param.Param +import org.evomaster.core.problem.asyncapi.data.AsyncApiAction +import org.evomaster.core.problem.asyncapi.param.AsyncApiParam +import org.evomaster.core.problem.rest.builder.RestActionBuilderV3 +import org.evomaster.core.problem.util.ActionBuilderUtil +import org.evomaster.core.search.action.Action + +/** + * Turns a parsed AsyncAPI document into the actions a search samples from, one per message that + * can actually be published. + * + * This is the AsyncAPI counterpart of + * [org.evomaster.core.problem.rest.builder.RestActionBuilderV3.addActionsFromSwagger] and + * [org.evomaster.core.problem.graphql.builder.GraphQLActionBuilder.addActionsFromSchema], and + * keeps their contract: fill a cluster keyed by action name, and return what had to be skipped + * rather than raising. + */ +object AsyncApiActionBuilder { + + /** + * Build one action per publishable message and add them to [actionCluster]. + * + * @return anything that could not be built, to be reported to the user + */ + fun addActionsFromSchema( + schema: AsyncApiDocument, + actionCluster: MutableMap, + options: RestActionBuilderV3.Options + ): List { + + actionCluster.clear() + + val messages = mutableListOf() + var skipped = 0 + + schema.operations.values.forEach { operation -> + + /* + Only what the service consumes can be published to. An operation the service + sends is something to subscribe to, not to drive, so it is counted rather than + turned into an action. + */ + if (operation.action != AsyncApiOperation.Action.RECEIVE) { + skipped++ + return@forEach + } + + val built = buildActionsFor(operation, schema, options, messages) + + if (built.isEmpty()) { + skipped++ + } + + built.forEach { actionCluster[it.getName()] = it } + } + + ActionBuilderUtil.printActionNumberInfo("AsyncAPI", actionCluster.size, skipped, 0) + + return messages + } + + private fun buildActionsFor( + operation: AsyncApiOperation, + schema: AsyncApiDocument, + options: RestActionBuilderV3.Options, + messages: MutableList + ): List { + + val carried = schema.messagesOf(operation) + + val actions = carried.mapNotNull { message -> + + val payload = try { + AsyncApiGeneBuilder.buildPayloadGene(schema, message, options) + } catch (e: Exception) { + /* + One message that cannot be built must not cost the whole document, which is + the same rule the parser follows. + */ + messages.add( + "Failed to build the payload of message '${message.id}' for operation" + + " '${operation.name}': ${e.message}" + ) + return@mapNotNull null + } + + val headers = try { + AsyncApiGeneBuilder.buildHeadersGene(schema, message, options) + } catch (e: Exception) { + messages.add( + "Failed to build the headers of message '${message.id}' for operation" + + " '${operation.name}': ${e.message}" + ) + null + } + + if (payload == null && headers == null) { + //nothing to vary and nothing to send: there is no action to make of it + messages.add( + "Message '${message.id}' of operation '${operation.name}' declares neither a" + + " payload nor headers, so there is nothing to publish" + ) + return@mapNotNull null + } + + val parameters = mutableListOf() + payload?.let { parameters.add(AsyncApiParam(AsyncApiParam.PAYLOAD, it)) } + headers?.let { parameters.add(AsyncApiParam(AsyncApiParam.HEADERS, it)) } + + AsyncApiAction( + operationId = operation.name, + channelName = operation.channelName, + messageId = message.id, + inputParameters = parameters, + replyTemplate = operation.reply + ) + } + + if (carried.isEmpty()) { + messages.add("Operation '${operation.name}' carries no message that can be published") + } + + //only when an operation carries more than one does the message need naming in the action + actions.forEach { it.singleMessage = actions.size == 1 } + + return actions + } +} diff --git a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiGeneBuilder.kt b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiGeneBuilder.kt index e423b25249..1e42c076ee 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiGeneBuilder.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiGeneBuilder.kt @@ -1,8 +1,10 @@ package org.evomaster.core.problem.asyncapi.builder import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.node.ArrayNode import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.fasterxml.jackson.databind.node.ObjectNode +import com.webfuzzing.asyncapi.models.AsyncApiCorrelationId import com.webfuzzing.asyncapi.models.AsyncApiDocument import com.webfuzzing.asyncapi.models.AsyncApiMessage import com.webfuzzing.asyncapi.resolver.AsyncApiRefResolver @@ -15,7 +17,7 @@ import org.evomaster.core.search.gene.Gene * * The parser deliberately stops at the schema: it leaves every `$ref` inside a payload alone, * and guarantees that whatever those references reach is present in - * [AsyncApiDocument.getComponentSchemas]. That guarantee is what this builder trades on -- it hands + * [AsyncApiDocument.componentSchemas]. That guarantee is what this builder trades on -- it hands * the whole schema map to [RestActionBuilderV3.createGeneForDTO], which wraps it in a synthetic * OpenAPI document and lets the existing machinery resolve the references and build the genes. * @@ -57,12 +59,66 @@ object AsyncApiGeneBuilder { * * Headers are built separately from the payload because they travel separately on the wire: * a transport with metadata puts them beside the body rather than in it. + * + * The header carrying the correlation id, where the message declares one, is left out. That + * value is stamped fresh at each execution so that a reply can be paired with the request + * that caused it, and a gene holding a value that is about to be overwritten is worse than + * no gene at all: the search would spend mutations on something that never reaches the wire. + * + * Only an id declared one level deep is left out, which is how every document seen so far + * writes it. One pointing further in -- `$message.header#/meta/id` -- keeps its gene, and + * the search wastes a few mutations on a field that is overwritten before it is sent. That + * is the mild failure of the two, and preferable to descending into a headers schema whose + * shape is not known here. */ fun buildHeadersGene( schema: AsyncApiDocument, message: AsyncApiMessage, options: RestActionBuilderV3.Options - ): Gene? = build(message.headers, "${message.id}.headers", schema, options) + ): Gene? = build(withoutCorrelationId(message), "${message.id}.headers", schema, options) + + /** + * The headers schema without the property the correlation id is stamped into. + */ + private fun withoutCorrelationId(message: AsyncApiMessage): JsonNode? { + + val headers = message.headers ?: return null + val correlation = message.correlationId + + if (correlation == null || correlation.source != AsyncApiCorrelationId.Source.HEADER) { + return headers + } + + val field = correlation.fieldName ?: return headers + val properties = headers.get("properties") + + if (properties == null || !properties.has(field)) { + return headers + } + + val copy = headers.deepCopy() as ObjectNode + val kept = (copy.get("properties") as ObjectNode).apply { remove(field) } + + /* + When the stamped id was the only header, there is nothing left to vary. Returning + the empty schema would build a free-form map gene, which is worse than nothing: it + would invite the search to invent headers the contract never declared. + */ + if (kept.isEmpty) { + return null + } + + //a field that is no longer there cannot be required either + (copy.get("required") as? ArrayNode)?.let { required -> + val kept = required.filter { it.asText() != field } + copy.remove("required") + if (kept.isNotEmpty()) { + copy.putArray("required").apply { kept.forEach { add(it) } } + } + } + + return copy + } /** * Options for building AsyncAPI payloads. diff --git a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiAction.kt b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiAction.kt new file mode 100644 index 0000000000..9781b343b6 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiAction.kt @@ -0,0 +1,106 @@ +package org.evomaster.core.problem.asyncapi.data + +import com.webfuzzing.asyncapi.models.AsyncApiReply +import org.evomaster.core.problem.api.ApiWsAction +import org.evomaster.core.problem.api.param.Param +import org.evomaster.core.problem.asyncapi.auth.AsyncApiNoAuth +import org.evomaster.core.problem.enterprise.auth.AuthenticationInfo +import org.evomaster.core.search.gene.Gene + +/** + * Publishing one message on one channel: the thing an AsyncAPI search actually does. + * + * It follows the shape of [org.evomaster.core.problem.rpc.RPCCallAction], which is the closest + * analogue in EvoMaster -- a call with no URL, made through a driver, whose mutable state is + * the input and whose response is read afterwards rather than searched over. + * + * Note what is deliberately *not* a gene: + * + * - the **address**, which is fixed by the contract. There is nothing to search over in where + * a message goes; sending to a channel the service does not read would only waste executions. + * - the **correlation id**, which is stamped fresh at each execution. Searching over it could + * achieve nothing, since the service only echoes it back, and pairing a reply with its + * request needs a value unique to each execution rather than one carried in the genome. + * - the **reply**, which is an observation. It is read at fitness time to decide what was + * covered, exactly as RPC reads its response. + */ +class AsyncApiAction( + + /** + * Key of the operation in the document. + * + * This is the unit coverage is counted against, so it is taken from the document verbatim + * and never synthesised: `(reply variant x operation)` is the AsyncAPI analogue of REST's + * `(status x endpoint)`, and it only means anything if the operation is stable. + */ + val operationId: String, + + /** + * Key of the channel the message is published on. The address it resolves to depends on the + * transport, so it is left to be resolved by whatever holds the connection. + */ + val channelName: String, + + /** + * Id of the message being published. An operation may carry several, in which case there is + * one action per message: which message to send is a choice the search makes by picking an + * action, not by mutating a gene. + */ + val messageId: String, + + /** + * The payload and, when the message declares them, the headers. These are the genes. + */ + inputParameters: MutableList, + + /** + * What the contract says a reply may be, when the operation declares one. Immutable, and + * not part of the children of this action: it is a description of what to expect, not + * something to vary. Null for a fire-and-forget operation. + */ + val replyTemplate: AsyncApiReply? = null, + + override var auth: AuthenticationInfo = AsyncApiNoAuth() + +) : ApiWsAction(auth, false, inputParameters) { + + companion object { + /** + * The name an action is known by, which must be unique within a search. + * + * An operation carrying a single message is named after the operation alone, since + * that reads better in a generated test; only when there are several does the message + * need naming too. + */ + fun nameFor(operationId: String, messageId: String, alone: Boolean) = + if (alone) operationId else "$operationId:$messageId" + } + + override fun getName(): String = nameFor(operationId, messageId, singleMessage) + + /** + * Whether this action is the only one built for its operation. Set by whoever builds the + * cluster, since it depends on the other actions rather than on this one. + */ + var singleMessage: Boolean = true + + override fun seeTopGenes(): List = parameters.flatMap { it.seeGenes() } + + override fun copyContent(): AsyncApiAction = + AsyncApiAction( + operationId, + channelName, + messageId, + parameters.asSequence().map(Param::copy).toMutableList(), + replyTemplate, + auth + ).also { it.singleMessage = singleMessage } + + /** + * Whether the contract promises something observable comes back. Only such an operation can + * be judged from outside without instrumentation. + */ + fun expectsReply(): Boolean = replyTemplate != null + + override fun toString(): String = "${getName()} on $channelName" +} diff --git a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividual.kt b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividual.kt new file mode 100644 index 0000000000..05cdbd2b86 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividual.kt @@ -0,0 +1,116 @@ +package org.evomaster.core.problem.asyncapi.data + +import org.evomaster.core.problem.api.ApiWsIndividual +import org.evomaster.core.problem.enterprise.EnterpriseActionGroup +import org.evomaster.core.problem.enterprise.EnterpriseChildTypeVerifier +import org.evomaster.core.problem.enterprise.SampleType +import org.evomaster.core.search.GroupsOfChildren +import org.evomaster.core.search.StructuralElement +import org.evomaster.core.search.action.ActionComponent +import org.evomaster.core.search.tracer.TrackOperator +import org.evomaster.core.database.sql.SqlAction +import kotlin.math.max + +/** + * A sequence of messages to publish, and whatever is needed to set the service up first. + * + * There is one individual for every transport rather than one per transport: Kafka versus AMQP + * versus a socket appears nowhere in here. What is being searched over is the operation and the + * payload, which are the same whatever moves the bytes; which wire is used is decided below the + * driver interface. Were the transport to leak in here there would have to be a + * `KafkaIndividual` and an `AmqpIndividual`, and nothing would be shared between them. + * + * Everything structural is inherited: initialization actions for seeding a database, a main + * group of the messages under test, and cleanup. + */ +class AsyncApiIndividual( + sampleType: SampleType, + trackOperator: TrackOperator? = null, + index: Int = -1, + allActions: MutableList, + mainSize: Int = allActions.size, + sqlSize: Int = 0, + mongoSize: Int = 0, + redisSize: Int = 0, + dnsSize: Int = 0, + groups: GroupsOfChildren = + getEnterpriseTopGroups(allActions, mainSize, sqlSize, mongoSize, redisSize, dnsSize, 0, 0) +) : ApiWsIndividual( + sampleType, + trackOperator, + index, + allActions, + childTypeVerifier = EnterpriseChildTypeVerifier(AsyncApiAction::class.java), + groups +) { + + constructor( + sampleType: SampleType, + actions: MutableList, + dbInitialization: MutableList = mutableListOf(), + trackOperator: TrackOperator? = null, + index: Int = -1 + ) : this( + sampleType = sampleType, + trackOperator = trackOperator, + index = index, + allActions = mutableListOf().apply { + addAll(dbInitialization) + addAll(actions.map { EnterpriseActionGroup(mutableListOf(it), AsyncApiAction::class.java) }) + }, + mainSize = actions.size, + sqlSize = dbInitialization.size + ) + + override fun canMutateStructure(): Boolean = true + + /** + * Add a message to publish, at [relativePosition] within the main group, or at the end. + */ + fun addAction(relativePosition: Int = -1, action: AsyncApiAction) { + + val main = GroupsOfChildren.MAIN + val group = EnterpriseActionGroup(mutableListOf(action), AsyncApiAction::class.java) + + if (relativePosition == -1) { + addChildToGroup(group, main) + } else { + val base = groupsView()!!.startIndexForGroupInsertionInclusive(main) + addChildToGroup(base + relativePosition, group, main) + } + } + + /** + * Remove the message at [position] of the main group. + */ + fun removeAction(position: Int) { + killChildByIndex(firstIndexOfMainGroup() + position) + } + + private fun firstIndexOfMainGroup() = max( + 0, + max( + children.indexOfLast { it is SqlAction } + 1, + children.indexOfFirst { it is EnterpriseActionGroup<*> } + ) + ) + + /* + Every group has to be measured, not just the two this class creates for itself. The + children are copied wholesale, so a size left at its default would not match what is + actually being handed over, and the group bookkeeping rejects that outright -- a copy + would fail for an individual that had picked up, say, a Mongo insertion along the way. + */ + override fun copyContent(): AsyncApiIndividual = + AsyncApiIndividual( + sampleType, + trackOperator, + index, + children.map { it.copy() }.toMutableList() as MutableList, + mainSize = groupsView()!!.sizeOfGroup(GroupsOfChildren.MAIN), + sqlSize = groupsView()!!.sizeOfGroup(GroupsOfChildren.INITIALIZATION_SQL), + mongoSize = groupsView()!!.sizeOfGroup(GroupsOfChildren.INITIALIZATION_MONGO), + redisSize = groupsView()!!.sizeOfGroup(GroupsOfChildren.INITIALIZATION_REDIS), + dnsSize = groupsView()!!.sizeOfGroup(GroupsOfChildren.INITIALIZATION_DNS) + ) +} diff --git a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/param/AsyncApiParam.kt b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/param/AsyncApiParam.kt new file mode 100644 index 0000000000..5a9fe400b6 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/param/AsyncApiParam.kt @@ -0,0 +1,21 @@ +package org.evomaster.core.problem.asyncapi.param + +import org.evomaster.core.problem.api.param.Param +import org.evomaster.core.search.gene.Gene + +/** + * One part of a message the search can vary: its payload, or its headers. + * + * The two are separate parameters rather than one because they travel separately on the wire: + * a transport with metadata puts headers beside the body rather than inside it. + */ +class AsyncApiParam(name: String, gene: Gene) : Param(name, gene) { + + companion object { + const val PAYLOAD = "payload" + + const val HEADERS = "headers" + } + + override fun copyContent(): AsyncApiParam = AsyncApiParam(name, gene.copy()) +} diff --git a/core/src/test/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiActionBuilderTest.kt b/core/src/test/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiActionBuilderTest.kt new file mode 100644 index 0000000000..b2267797d0 --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiActionBuilderTest.kt @@ -0,0 +1,453 @@ +package org.evomaster.core.problem.asyncapi.builder + +import com.webfuzzing.asyncapi.access.AsyncApiAccess +import com.webfuzzing.asyncapi.models.AsyncApiDocument +import org.evomaster.core.EMConfig +import org.evomaster.core.database.mongo.MongoDbAction +import org.evomaster.core.problem.asyncapi.data.AsyncApiAction +import org.evomaster.core.problem.asyncapi.data.AsyncApiIndividual +import org.evomaster.core.problem.asyncapi.param.AsyncApiParam +import org.evomaster.core.problem.enterprise.SampleType +import org.evomaster.core.problem.rest.builder.RestActionBuilderV3 +import org.evomaster.core.search.action.Action +import org.evomaster.core.search.gene.ObjectGene +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class AsyncApiActionBuilderTest { + + private val options = AsyncApiGeneBuilder.options(EMConfig()) + + @BeforeEach + fun reset() { + RestActionBuilderV3.cleanCache() + } + + private fun build(resourcePath: String): Pair, List> { + val schema = AsyncApiAccess.getAsyncApiFromResource(resourcePath) + return build(schema) + } + + private fun build(schema: AsyncApiDocument): Pair, List> { + val cluster = mutableMapOf() + val messages = AsyncApiActionBuilder.addActionsFromSchema(schema, cluster, options) + return cluster to messages + } + + private fun actionsOf(resourcePath: String) = + build(resourcePath).first.values.map { it as AsyncApiAction } + + // ------------------------------------------------------------------ what becomes an action + + @Test + fun testOneActionPerConsumedOperation() { + + val (cluster, messages) = build("/asyncapi/sut/ncs-kafka.yaml") + + assertTrue(messages.isEmpty(), "unexpected problems: $messages") + + //six operations, each consuming a single message, so six actions named after them + assertEquals( + setOf("checkTriangle", "bessj", "expint", "fisher", "gammq", "remainder"), + cluster.keys + ) + + val bessj = cluster.getValue("bessj") as AsyncApiAction + assertEquals("bessj", bessj.operationId) + assertEquals("bessjRequest", bessj.channelName) + assertEquals("bessjRequest", bessj.messageId) + } + + @Test + fun testOperationsTheServiceEmitsAreNotDrivable() { + + /* + A 'send' operation is what the service emits. There is nothing to publish to it, so + it is not turned into an action; only what the service consumes can be driven. + */ + val schema = AsyncApiAccess.parseFromText( + """ + asyncapi: 3.0.0 + info: + title: One of each direction + version: 1.0.0 + channels: + c: + address: a + messages: + m: + payload: + type: object + operations: + consumed: + action: receive + channel: + ${'$'}ref: '#/channels/c' + emitted: + action: send + channel: + ${'$'}ref: '#/channels/c' + """.trimIndent() + ) + + assertEquals(setOf("consumed"), build(schema).first.keys) + } + + @Test + fun testAnOperationCarryingSeveralMessagesGetsOnePerMessage() { + + val cluster = build("/asyncapi/artificial/websocket-reply.yaml").first + + /* + Which message to send is a choice made by picking an action, not by mutating a gene, + so an operation carrying several becomes several actions. Here each carries one, so + each keeps the operation's own name. + */ + assertTrue(cluster.containsKey("recv_list_legs")) + assertTrue(cluster.containsKey("recv_get_leg")) + + val listLegs = cluster.getValue("recv_list_legs") as AsyncApiAction + assertEquals("listLegs", listLegs.messageId) + assertEquals("vsi", listLegs.channelName) + } + + @Test + fun testTheMessageIsNamedOnlyWhenThereIsMoreThanOne() { + + val schema = AsyncApiAccess.parseFromText( + """ + asyncapi: 3.0.0 + info: + title: An operation that can send either of two messages + version: 1.0.0 + channels: + c: + address: a + messages: + first: + ${'$'}ref: '#/components/messages/first' + second: + ${'$'}ref: '#/components/messages/second' + operations: + o: + action: receive + channel: + ${'$'}ref: '#/channels/c' + components: + messages: + first: + payload: + type: object + second: + payload: + type: object + """.trimIndent() + ) + + assertEquals(setOf("o:first", "o:second"), build(schema).first.keys) + } + + @Test + fun testAnOperationInheritsEveryMessageOfItsChannelWhenItNarrowsToNone() { + + val (cluster, _) = build("/asyncapi/sut/scalar.yaml") + + /* + This is how an operation ends up carrying several messages in practice. No document + in the corpus narrows to more than one with a `messages:` array; what happens is + the opposite -- an operation declares no array at all, and so inherits everything + its channel carries. Here one channel carries five genuinely different payloads and + the operation does not say which, so each becomes an action of its own: they are + different things to send, and picking between them is a structural choice rather + than a mutation. + */ + val userEvents = cluster.keys.filter { it.startsWith("subscribeToUserEvents") }.sorted() + + assertEquals( + listOf( + "subscribeToUserEvents:LoginAttempt", + "subscribeToUserEvents:UserAuthenticated", + "subscribeToUserEvents:UserDeleted", + "subscribeToUserEvents:UserProfileUpdated", + "subscribeToUserEvents:UserSignedUp" + ), + userEvents + ) + + //they share the operation they came from, which is what the document names + val actions = userEvents.map { cluster.getValue(it) as AsyncApiAction } + assertTrue(actions.all { it.operationId == "subscribeToUserEvents" }) + assertTrue(actions.all { it.channelName == "userEvents" }) + //but each carries its own message, and so its own payload genes + assertEquals(userEvents.size, actions.map { it.messageId }.distinct().size) + } + + // ------------------------------------------------------------------ what an action holds + + @Test + fun testThePayloadIsTheGenes() { + + val bessj = actionsOf("/asyncapi/sut/ncs-kafka.yaml").first { it.operationId == "bessj" } + + val payload = bessj.parameters.first { it.name == AsyncApiParam.PAYLOAD } + assertTrue(payload.gene is ObjectGene) + + //the genes the search may vary are exactly the input, and nothing else + assertEquals(payload.seeGenes().size, bessj.seeTopGenes().size) + } + + @Test + fun testTheReplyIsDescribedButNotSearchedOver() { + + val bessj = actionsOf("/asyncapi/sut/ncs-kafka.yaml").first { it.operationId == "bessj" } + + assertTrue(bessj.expectsReply()) + //two declared outcomes, which is what gives a black-box search something to tell apart + assertEquals(listOf("doubleResult", "error"), bessj.replyTemplate!!.messageIds) + assertEquals("bessjReply", bessj.replyTemplate!!.channelName) + + //but none of that is a gene: it is what to expect, not what to vary + assertTrue(bessj.seeTopGenes().none { it.name.contains("result", ignoreCase = true) }) + } + + @Test + fun testAFireAndForgetOperationHasNoReplyTemplate() { + + val action = actionsOf("/asyncapi/sut/microcks.yaml").first() + + assertFalse(action.expectsReply()) + assertNull(action.replyTemplate) + } + + @Test + fun testHeadersAreASeparateParameterFromThePayload() { + + val action = build(AsyncApiAccess.parseFromText(headerDocument(true))).first + .values.map { it as AsyncApiAction }.first() + + assertEquals( + listOf(AsyncApiParam.PAYLOAD, AsyncApiParam.HEADERS), + action.parameters.map { it.name } + ) + } + + @Test + fun testTheStampedCorrelationHeaderIsNotAGene() { + + val action = build(AsyncApiAccess.parseFromText(headerDocument(true))).first + .values.map { it as AsyncApiAction }.first() + + /* + The document declares where the correlation id travels, and the headers schema + declares a property of that name. The value is stamped fresh at each execution so a + reply can be paired with its request, so a gene holding it would only be overwritten + -- the search would spend mutations on something that never travels. + */ + val headers = action.parameters.first { it.name == AsyncApiParam.HEADERS }.gene as ObjectGene + assertEquals(listOf("tenant"), headers.fields.map { it.name }) + } + + @Test + fun testNoHeadersParameterWhenTheStampedIdIsTheOnlyHeader() { + + val action = build(AsyncApiAccess.parseFromText(headerDocument(false))).first + .values.map { it as AsyncApiAction }.first() + + //an empty headers schema would invite the search to invent headers never declared + assertEquals(listOf(AsyncApiParam.PAYLOAD), action.parameters.map { it.name }) + } + + /** + * A document whose message declares a correlation id in its headers, with or without + * another header of its own beside it. + */ + private fun headerDocument(withOtherHeader: Boolean) = + if (withOtherHeader) { + """ + asyncapi: 3.0.0 + info: + title: A stamped id and a header of its own + version: 1.0.0 + channels: + c: + address: a + messages: + m: + ${'$'}ref: '#/components/messages/m' + operations: + o: + action: receive + channel: + ${'$'}ref: '#/channels/c' + components: + messages: + m: + correlationId: + location: '${'$'}message.header#/correlationId' + payload: + type: object + properties: + value: + type: string + headers: + type: object + properties: + correlationId: + type: string + tenant: + type: string + """.trimIndent() + } else { + """ + asyncapi: 3.0.0 + info: + title: A stamped id and nothing else + version: 1.0.0 + channels: + c: + address: a + messages: + m: + ${'$'}ref: '#/components/messages/m' + operations: + o: + action: receive + channel: + ${'$'}ref: '#/channels/c' + components: + messages: + m: + correlationId: + location: '${'$'}message.header#/correlationId' + payload: + type: object + properties: + value: + type: string + headers: + type: object + properties: + correlationId: + type: string + """.trimIndent() + } + + // ------------------------------------------------------------------ degrading gracefully + + @Test + fun testAMessageThatCannotBePublishedIsReported() { + + val schema = AsyncApiAccess.parseFromText( + """ + asyncapi: 3.0.0 + info: + title: A message with nothing to send + version: 1.0.0 + channels: + c: + address: a + messages: + empty: + name: Empty + operations: + o: + action: receive + channel: + ${'$'}ref: '#/channels/c' + """.trimIndent() + ) + + val (cluster, messages) = build(schema) + + assertTrue(cluster.isEmpty()) + assertTrue( + messages.any { it.contains("neither a payload nor headers") }, + messages.toString() + ) + } + + @Test + fun testBuildingIsRepeatable() { + + //the gene builder keeps a static cache, so building twice must give the same shape + val first = build("/asyncapi/sut/ncs-kafka.yaml").first + val second = build("/asyncapi/sut/ncs-kafka.yaml").first + + assertEquals(first.keys, second.keys) + assertEquals( + (first.getValue("bessj") as AsyncApiAction).seeTopGenes().size, + (second.getValue("bessj") as AsyncApiAction).seeTopGenes().size + ) + } + + // ------------------------------------------------------------------ the individual + + @Test + fun testAnIndividualHoldsTheMessagesToPublish() { + + val actions = actionsOf("/asyncapi/sut/ncs-kafka.yaml").take(2).map { it.copy() as AsyncApiAction } + + val individual = AsyncApiIndividual(SampleType.RANDOM, actions.toMutableList()) + + assertEquals(2, individual.seeMainExecutableActions().size) + assertTrue(individual.canMutateStructure()) + } + + @Test + fun testMessagesCanBeAddedAndRemoved() { + + val actions = actionsOf("/asyncapi/sut/ncs-kafka.yaml").map { it.copy() as AsyncApiAction } + + val individual = AsyncApiIndividual(SampleType.RANDOM, mutableListOf(actions[0])) + assertEquals(1, individual.seeMainExecutableActions().size) + + individual.addAction(action = actions[1]) + assertEquals(2, individual.seeMainExecutableActions().size) + + individual.removeAction(0) + assertEquals(1, individual.seeMainExecutableActions().size) + assertEquals(actions[1].getName(), individual.seeMainExecutableActions().first().getName()) + } + + @Test + fun testCopyingAnIndividualKeepsItsMessages() { + + val actions = actionsOf("/asyncapi/sut/ncs-kafka.yaml").take(3).map { it.copy() as AsyncApiAction } + val individual = AsyncApiIndividual(SampleType.RANDOM, actions.toMutableList()) + + val copy = individual.copy() as AsyncApiIndividual + + assertEquals( + individual.seeMainExecutableActions().map { it.getName() }, + copy.seeMainExecutableActions().map { it.getName() } + ) + //a copy must be independent, or mutating one would change the other + assertNotSame(individual.seeMainExecutableActions()[0], copy.seeMainExecutableActions()[0]) + } + + @Test + fun testCopyingAnIndividualThatWasSetUpWithMoreThanSql() { + + /* + Only SQL is put in front of the messages today, but the individual inherits every + other kind of setup an enterprise individual can hold. The children are copied + wholesale, so a group whose size was not measured would not match what is handed + over, and the copy would fail outright rather than come back wrong. + */ + val actions = actionsOf("/asyncapi/sut/ncs-kafka.yaml").take(1).map { it.copy() as AsyncApiAction } + val individual = AsyncApiIndividual(SampleType.RANDOM, actions.toMutableList()) + + individual.addInitializingMongoDbActions( + actions = listOf(MongoDbAction("db", "collection", "collection", listOf())) + ) + + val copy = individual.copy() as AsyncApiIndividual + + assertEquals(1, copy.seeMainExecutableActions().size) + assertEquals( + individual.seeInitializingActions().size, + copy.seeInitializingActions().size + ) + } +} diff --git a/core/src/test/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiGeneBuilderTest.kt b/core/src/test/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiGeneBuilderTest.kt index 9cbefb232c..5086ecaf4a 100644 --- a/core/src/test/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiGeneBuilderTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiGeneBuilderTest.kt @@ -97,15 +97,20 @@ class AsyncApiGeneBuilderTest { fun testHeadersAreBuiltSeparatelyFromThePayload() { val schema = AsyncApiAccess.getAsyncApiFromResource("/asyncapi/artificial/messages.yaml") - val message = schema.messages.getValue("signupRequest") - - val headers = AsyncApiGeneBuilder.buildHeadersGene(schema, message, options)!! - assertTrue(field(headers, "correlationId") is StringGene) //a message declaring no headers gets none, rather than an empty object assertNull( AsyncApiGeneBuilder.buildHeadersGene(schema, schema.messages.getValue("heartbeat"), options) ) + + /* + signupRequest declares one header, and it is the one the correlation id is stamped + into. That value is written fresh at each execution, so a gene holding it would only + be overwritten -- and with nothing else left, there are no header genes at all. + */ + assertNull( + AsyncApiGeneBuilder.buildHeadersGene(schema, schema.messages.getValue("signupRequest"), options) + ) } @Test diff --git a/core/src/test/resources/asyncapi/sut/microcks.yaml b/core/src/test/resources/asyncapi/sut/microcks.yaml new file mode 100644 index 0000000000..89afd315c4 --- /dev/null +++ b/core/src/test/resources/asyncapi/sut/microcks.yaml @@ -0,0 +1,418 @@ +# Source: https://github.com/microcks/microcks (Apache-2.0), unmodified. +asyncapi: 3.0.0 +info: + title: Microcks Events API v1.10 + version: 1.10.1 + description: "Events API offered by Microcks, the Kubernetes native tool for API and microservices\ + \ mocking and testing (microcks.io)" + contact: + name: Laurent Broudoux + url: https://github.com/microcks + email: laurent@microcks.io + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 + x-logo: + backgroundColor: '#ffffff' + url: https://microcks.io/images/microcks-logo-blue.png +defaultContentType: application/json +channels: + service-changes: + description: A channel where Services changes are published + messages: + serviceChangeEvent: + $ref: '#/components/messages/serviceChangeEvent' + bindings: + kafka: + key: + type: string + bindings: + ws: + method: POST + kafka: + topic: 'microcks-services-updates' +operations: + receivedServiceChanges: + action: receive + channel: + $ref: '#/channels/service-changes' + summary: Receive information about Service changes + messages: + - $ref: '#/channels/service-changes/messages/serviceChangeEvent' +components: + messages: + serviceChangeEvent: + description: An event describing that Service has been modified (created, updated or deleted) + payload: + $ref: '#/components/schemas/ServiceChangeEvent' + schemas: + ServiceChangeEvent: + type: object + required: + - serviceId + - serviceView + - changeType + - timestamp + properties: + serviceId: + type: string + serviceView: + type: object + schema: + $ref: '#/components/schemas/ServiceView' + changeType: + type: string + enum: + - CREATED + - UPDATED + - DELETED + timestamp: + type: number + format: int32 + additionalProperties: true + ServiceView: + description: Aggregate bean for grouping a Service an its messages pairs + type: object + required: + - service + - messagesMap + properties: + service: + $ref: '#/components/schemas/Service' + description: Wrapped service description + messagesMap: + type: object + description: "Map of messages for this Service. Keys are operation name,\ + \ values are array of messages for this operation" + additionalProperties: + $ref: '#/components/schemas/MessageArray' + additionalProperties: true + Service: + description: Represents a Service or API definition as registred into Microcks + repository + required: + - name + - version + - type + - sourceArtifact + properties: + id: + description: Unique identifier for this Service or API + type: string + name: + description: Distinct name for this Service or API (maybe shared among many + versions) + type: string + version: + description: Distinct version for a named Service or API + type: string + type: + description: Service or API Type + type: string + enum: + - REST + - SOAP_HTTP + - GENERIC_REST + - GENERIC_EVENT + - EVENT + - GRPC + - GRAPHQL + operations: + description: Set of Operations for Service or API + type: array + items: + $ref: '#/components/schemas/Operation' + xmlNS: + description: Associated Xml Namespace in case of Xml based Service + type: string + metadata: + $ref: '#/components/schemas/Metadata' + description: Metadata of Service + sourceArtifact: + description: Short name of the main/primary artifact this service was created + from + type: string + Metadata: + description: Commodity object for holding metadata on any entity. This object + is inspired by Kubernetes metadata. + type: object + required: + - createdOn + - lastUpdate + properties: + createdOn: + description: Creation date of attached object + type: number + readOnly: true + lastUpdate: + description: Last update of attached object + type: number + readOnly: true + annotations: + description: Annotations of attached object + type: object + additionalProperties: + type: string + labels: + description: Labels put on attached object + type: object + additionalProperties: + type: string + Operation: + description: An Operation of a Service or API + type: object + required: + - name + - method + properties: + name: + description: Unique name of this Operation within Service scope + type: string + method: + description: Represents transport method + type: string + inputName: + description: Name of input parameters in case of Xml based Service + type: string + outputName: + description: Name of output parameters in case of Xml based Service + type: string + dispatcher: + description: Dispatcher strategy used for mocks + type: string + dispatcherRules: + description: DispatcherRules used for mocks + type: string + defaultDelay: + description: Default response time delay for mocks + type: number + resourcePaths: + description: Paths the mocks endpoints are mapped on + type: array + items: + type: string + parameterContraints: + description: Contraints that may apply to mock invocatino on this operation + type: array + items: + $ref: '#/components/schemas/ParameterConstraint' + bindings: + description: Map of protocol binding details for this operation + type: object + additionalProperties: + $ref: '#/components/schemas/Binding' + ParameterConstraint: + description: Companion object for Operation that may be used to express constraints + on request parameters + type: object + required: + - name + properties: + name: + description: Parameter name + type: string + required: + description: Whether it's a required constraint + type: boolean + recopy: + description: Whether it's a recopy constraint + type: boolean + mustMatchRegexp: + description: Whether it's a regular expression matching constraint + type: string + in: + description: Parameter location + type: string + enum: + - path + - query + - header + Binding: + description: Protocol binding details for asynchronous operations + type: object + required: + - type + - destinationName + properties: + type: + description: Protocol binding identifier + type: string + enum: + - KAFKA + - MQTT + - WS + - AMQP + - NATS + - GOOGLEPUBSUB + - SQS + - SNS + keyType: + description: Type of key for Kafka messages + type: string + destinationType: + description: Type of destination for asynchronous messages of this operation + type: string + destinationName: + description: Name of destination for asynchronous messages of this operation + type: string + qoS: + description: Quality of Service attribute for MQTT binding + type: string + persistent: + description: Persistent attribute for MQTT binding + type: boolean + method: + description: HTTP method for WebSocket binding + type: string + MessageArray: + description: Array of Message for Service operations + type: array + items: + $ref: '#/components/schemas/Exchange' + Exchange: + description: "Abstract representation of a Service or API exchange type (request/response,\ + \ event based, ...)" + oneOf: + - $ref: '#/components/schemas/RequestResponsePair' + - $ref: '#/components/schemas/UnidirectionalEvent' + discriminator: type + AbstractExchange: + description: Abstract bean representing a Service or API Exchange. + type: object + required: + - type + properties: + type: + description: Discriminant type for identifying kind of exchange + type: string + enum: + - reqRespPair + - unidirEvent + RequestResponsePair: + description: Request associated with corresponding Response + type: object + allOf: + - type: object + properties: + request: + $ref: '#/components/schemas/Request' + description: The request part of the pair + response: + $ref: '#/components/schemas/Response' + description: The Response part of the pair + required: + - request + - response + - $ref: '#/components/schemas/AbstractExchange' + UnidirectionalEvent: + description: Representation of an unidirectional exchange as an event message + type: object + allOf: + - type: object + required: + - eventMessage + properties: + eventMessage: + $ref: '#/components/schemas/EventMessage' + description: Asynchronous message for this unidirectional event + - $ref: '#/components/schemas/AbstractExchange' + Request: + description: A mock invocation or test request + required: + - name + - operationId + properties: + id: + description: Unique identifier of Request + type: string + name: + description: Unique distinct name of this Request + type: string + content: + description: Body content for this request + type: string + operationId: + description: Identifier of Operation this Request is associated to + type: string + testCaseId: + description: Unique identifier of TestCase this Request is attached (in + case of a test) + type: string + headers: + description: Headers for this Request + type: array + items: + $ref: '#/components/schemas/Header' + Response: + description: A mock invocation or test response + required: + - operationId + - name + properties: + operationId: + description: Identifier of Operation this Response is associated to + type: string + content: + description: Body content of this Response + type: string + id: + description: Unique identifier of Response + type: string + name: + description: Unique distinct name of this Response + type: string + testCaseId: + description: Unique identifier of TestCase this Response is attached (in + case of a test) + type: string + headers: + description: Headers for this Response + type: array + items: + $ref: '#/components/schemas/Header' + Header: + description: Transport headers for both Requests and Responses + required: + - name + - values + type: object + properties: + name: + description: Unique distinct name of this Header + type: string + values: + description: Values for this Header + type: array + items: + type: string + EventMessage: + description: A mock event message + required: + - id + - mediaType + type: object + properties: + id: + description: Unique identifier of this message + type: string + mediaType: + description: Content type of message + type: string + name: + description: Unique distinct name of this message + type: string + content: + description: Body content for this message + type: string + operationId: + description: Identifier of Operation this message is associated to + type: string + testCaseId: + description: Unique identifier of TestCase this message is attached (in + case of a test) + type: string + headers: + description: Headers for this message + type: array + items: + $ref: '#/components/schemas/Header' diff --git a/core/src/test/resources/asyncapi/sut/scalar.yaml b/core/src/test/resources/asyncapi/sut/scalar.yaml new file mode 100644 index 0000000000..94e6f977aa --- /dev/null +++ b/core/src/test/resources/asyncapi/sut/scalar.yaml @@ -0,0 +1,1853 @@ +# Source: https://github.com/scalar/scalar (MIT), unmodified. +asyncapi: 3.0.0 +info: + title: Scalar Galaxy Events + version: 1.0.0 + description: | + The Scalar Galaxy Events provides real-time notifications and event-driven capabilities for the Scalar Galaxy API. This AsyncAPI document complements the [REST API](https://galaxy.scalar.com) by enabling event-driven interactions. + + ## Event-Driven Features + + * Real-time Planet Updates: Get notified when planets are created, updated, or experience cosmic events + * User Activity Streams: Track user signups, authentication events, and profile changes + * Celestial Body Monitoring: Monitor changes to satellites, asteroids, and other celestial objects + * System Notifications: Receive alerts about system maintenance, rate limits, and security events + + ## Resources + + * https://github.com/scalar/scalar + * https://github.com/asyncapi/spec + * https://scalar.com + contact: + name: Scalar Support + url: https://scalar.com + email: support@scalar.com + license: + name: MIT + url: https://opensource.org/license/MIT + externalDocs: + description: Documentation + url: https://github.com/scalar/scalar +servers: + production: + host: galaxy.scalar.com + protocol: wss + description: Production WebSocket server for real-time events + security: + - $ref: '#/components/securitySchemes/bearerAuth' + - $ref: '#/components/securitySchemes/httpApiKey' + tags: + - name: production + description: Production environment + development: + host: localhost:8080 + protocol: ws + description: Local development server + security: [] + tags: + - name: development + description: Development environment +defaultContentType: application/json +channels: + planetEvents: + address: 'planets/{planetId}/events' + description: | + Real-time events related to specific planets. Subscribe to get notified about: + + * Planet creation and updates + * Atmospheric changes + * Discovery of new satellites + * Cosmic events (explosions, collisions, etc.) + * Image uploads and media updates + parameters: + planetId: + $ref: '#/components/parameters/planetId' + messages: + planetCreated: + $ref: '#/components/messages/PlanetCreated' + planetUpdated: + $ref: '#/components/messages/PlanetUpdated' + planetDeleted: + $ref: '#/components/messages/PlanetDeleted' + planetExploded: + $ref: '#/components/messages/PlanetExploded' + satelliteDiscovered: + $ref: '#/components/messages/SatelliteDiscovered' + atmosphereChanged: + $ref: '#/components/messages/AtmosphereChanged' + imageUploaded: + $ref: '#/components/messages/ImageUploaded' + bindings: + ws: + method: GET + query: + type: object + properties: + includeHistory: + type: boolean + description: Include historical events + default: false + eventTypes: + type: array + items: + type: string + description: Filter by specific event types + userEvents: + address: 'users/{userId}/events' + description: | + User-related events including authentication, profile changes, and activity tracking. + + Security Note: These events contain sensitive user information and require proper authentication. + parameters: + userId: + $ref: '#/components/parameters/userId' + messages: + userSignedUp: + $ref: '#/components/messages/UserSignedUp' + userAuthenticated: + $ref: '#/components/messages/UserAuthenticated' + userProfileUpdated: + $ref: '#/components/messages/UserProfileUpdated' + userDeleted: + $ref: '#/components/messages/UserDeleted' + loginAttempt: + $ref: '#/components/messages/LoginAttempt' + bindings: + ws: + method: GET + systemEvents: + address: 'system/events' + description: | + System-wide events including maintenance notifications, rate limit alerts, and security events. + + These events are broadcast to all connected clients and don't require specific subscriptions. + messages: + systemMaintenance: + $ref: '#/components/messages/SystemMaintenance' + rateLimitExceeded: + $ref: '#/components/messages/RateLimitExceeded' + securityAlert: + $ref: '#/components/messages/SecurityAlert' + apiVersionDeprecated: + $ref: '#/components/messages/ApiVersionDeprecated' + bindings: + ws: + method: GET + celestialBodyEvents: + address: 'celestial-bodies/events' + description: | + Events related to all types of celestial bodies (planets, satellites, asteroids, comets). + + This is a general channel for monitoring the entire galaxy's celestial activity. + messages: + celestialBodyCreated: + $ref: '#/components/messages/CelestialBodyCreated' + celestialBodyUpdated: + $ref: '#/components/messages/CelestialBodyUpdated' + orbitalCollision: + $ref: '#/components/messages/OrbitalCollision' + newDiscovery: + $ref: '#/components/messages/NewDiscovery' + bindings: + ws: + method: GET + query: + type: object + properties: + bodyType: + type: string + enum: [planet, satellite, asteroid, comet] + description: Filter by celestial body type + minHabitability: + type: number + minimum: 0 + maximum: 1 + description: Filter by minimum habitability index +operations: + subscribeToPlanetEvents: + action: receive + channel: + $ref: '#/channels/planetEvents' + title: Subscribe to Planet Events + description: Subscribe to real-time events for a specific planet + traits: + - $ref: '#/components/operationTraits/authenticated' + bindings: + ws: + bindingVersion: 0.1.0 + method: GET + subscribeToUserEvents: + action: receive + channel: + $ref: '#/channels/userEvents' + title: Subscribe to User Events + description: Subscribe to events for a specific user (requires authentication) + traits: + - $ref: '#/components/operationTraits/authenticated' + - $ref: '#/components/operationTraits/rateLimited' + bindings: + ws: + bindingVersion: 0.1.0 + method: GET + subscribeToSystemEvents: + action: receive + channel: + $ref: '#/channels/systemEvents' + title: Subscribe to System Events + description: Subscribe to system-wide notifications and alerts + bindings: + ws: + bindingVersion: 0.1.0 + method: GET + subscribeToCelestialBodyEvents: + action: receive + channel: + $ref: '#/channels/celestialBodyEvents' + title: Subscribe to Celestial Body Events + description: Subscribe to events for all celestial bodies in the galaxy + traits: + - $ref: '#/components/operationTraits/rateLimited' + bindings: + ws: + bindingVersion: 0.1.0 + method: GET +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: JWT Bearer token authentication for WebSocket connections + httpApiKey: + type: httpApiKey + in: query + name: api_key + description: API key for WebSocket authentication + oauth2: + type: oauth2 + flows: + implicit: + authorizationUrl: https://galaxy.scalar.com/oauth/authorize + availableScopes: + read:events: Subscribe to events + write:events: Publish events (admin only) + read:user:events: Subscribe to user events + parameters: + planetId: + description: The ID of the planet + userId: + description: The ID of the user + operationTraits: + authenticated: + description: This operation requires authentication + security: + - $ref: '#/components/securitySchemes/bearerAuth' + - $ref: '#/components/securitySchemes/httpApiKey' + rateLimited: + description: This operation is subject to rate limiting + bindings: + ws: + bindingVersion: 0.1.0 + headers: + type: object + properties: + X-RateLimit-Limit: + type: integer + description: Maximum requests per minute + X-RateLimit-Remaining: + type: integer + description: Remaining requests in current window + messages: + PlanetCreated: + name: PlanetCreated + title: Planet Created Event + summary: A new planet has been discovered or created + description: | + This event is published when a new planet is added to the Scalar Galaxy. + + Event Flow: + 1. Planet creation request via REST API + 2. Planet validation and storage + 3. This event published to subscribers + 4. Optional webhook callbacks triggered + contentType: application/json + payload: + $ref: '#/components/schemas/PlanetCreatedEvent' + examples: + - name: Mars Discovery + summary: Mars was just added to the galaxy + payload: + eventId: 'evt_1234567890' + eventType: 'planet.created' + timestamp: '2024-01-15T14:30:00Z' + planet: + id: 4 + name: 'Mars' + description: 'The red planet' + type: 'terrestrial' + habitabilityIndex: 0.68 + physicalProperties: + mass: 0.107 + radius: 0.532 + gravity: 0.378 + atmosphere: + - compound: 'CO2' + percentage: 95.3 + discoveredAt: '1610-01-07T00:00:00Z' + creator: + id: 1 + name: 'Marc' + metadata: + source: 'rest-api' + userId: 1 + requestId: 'req_abc123' + bindings: + ws: + bindingVersion: 0.1.0 + message: + type: object + properties: + eventId: + type: string + description: Unique event identifier + eventType: + type: string + description: Type of event + timestamp: + type: string + format: date-time + description: When the event occurred + PlanetUpdated: + name: PlanetUpdated + title: Planet Updated Event + summary: An existing planet has been modified + description: Published when planet properties are updated via the REST API + contentType: application/json + payload: + $ref: '#/components/schemas/PlanetUpdatedEvent' + examples: + - name: Mars Atmosphere Update + summary: Mars atmosphere composition was updated + payload: + eventId: 'evt_1234567891' + eventType: 'planet.updated' + timestamp: '2024-01-15T15:45:00Z' + planet: + id: 4 + name: 'Mars' + atmosphere: + - compound: 'CO2' + percentage: 95.3 + - compound: 'N2' + percentage: 2.7 + changes: + - field: 'atmosphere' + oldValue: [{ 'compound': 'CO2', 'percentage': 95.3 }] + newValue: + [ + { 'compound': 'CO2', 'percentage': 95.3 }, + { 'compound': 'N2', 'percentage': 2.7 }, + ] + metadata: + source: 'rest-api' + userId: 1 + requestId: 'req_def456' + bindings: + ws: + bindingVersion: 0.1.0 + PlanetDeleted: + name: PlanetDeleted + title: Planet Deleted Event + summary: A planet has been removed from the galaxy + description: | + ⚠️ Warning: This event indicates a planet has been deleted. + + This is a destructive operation that may affect related celestial bodies and data. + contentType: application/json + payload: + $ref: '#/components/schemas/PlanetDeletedEvent' + examples: + - name: Planet Destruction + summary: A planet was deleted (hopefully by accident!) + payload: + eventId: 'evt_1234567892' + eventType: 'planet.deleted' + timestamp: '2024-01-15T16:00:00Z' + planetId: 999 + planetName: 'Test Planet' + reason: 'Experimental deletion' + metadata: + source: 'rest-api' + userId: 1 + requestId: 'req_ghi789' + bindings: + ws: + bindingVersion: 0.1.0 + PlanetExploded: + name: PlanetExploded + title: Planet Exploded Event + summary: A planet has experienced a cosmic explosion + description: | + 🚨 Cosmic Event: A planet has exploded due to natural or artificial causes. + + This is a rare but dramatic event that may create new celestial bodies or affect nearby objects. + contentType: application/json + payload: + $ref: '#/components/schemas/PlanetExplodedEvent' + examples: + - name: Supernova Event + summary: A planet went supernova + payload: + eventId: 'evt_1234567893' + eventType: 'planet.exploded' + timestamp: '2024-01-15T16:30:00Z' + planet: + id: 42 + name: 'Krypton' + type: 'super_earth' + explosion: + type: 'supernova' + intensity: 9.8 + energyReleased: '1.2e44' + newCelestialBodies: + - type: 'asteroid' + name: 'Krypton Fragment Alpha' + - type: 'asteroid' + name: 'Krypton Fragment Beta' + affectedObjects: + - planetId: 41 + distance: 1500000 + impact: 'moderate' + metadata: + source: 'cosmic-simulator' + severity: 'critical' + bindings: + ws: + bindingVersion: 0.1.0 + SatelliteDiscovered: + name: SatelliteDiscovered + title: Satellite Discovered Event + summary: A new satellite has been discovered orbiting a planet + description: Published when a new moon, asteroid, or other satellite is found + contentType: application/json + payload: + $ref: '#/components/schemas/SatelliteDiscoveredEvent' + examples: + - name: New Moon Discovery + summary: A new moon was discovered around Mars + payload: + eventId: 'evt_1234567894' + eventType: 'satellite.discovered' + timestamp: '2024-01-15T17:00:00Z' + satellite: + id: 15 + name: 'Deimos II' + type: 'moon' + diameter: 12.4 + orbit: + planet: + id: 4 + name: 'Mars' + orbitalPeriod: 1.26 + distance: 23460 + discoveryMethod: 'telescope_observation' + metadata: + source: 'astronomical-survey' + observerId: 2 + bindings: + ws: + bindingVersion: 0.1.0 + AtmosphereChanged: + name: AtmosphereChanged + title: Atmosphere Changed Event + summary: A planet's atmosphere composition has changed + description: Published when atmospheric properties are modified + contentType: application/json + payload: + $ref: '#/components/schemas/AtmosphereChangedEvent' + examples: + - name: Terraforming Progress + summary: Mars atmosphere is being terraformed + payload: + eventId: 'evt_1234567895' + eventType: 'atmosphere.changed' + timestamp: '2024-01-15T17:30:00Z' + planet: + id: 4 + name: 'Mars' + atmosphere: + - compound: 'CO2' + percentage: 95.3 + - compound: 'O2' + percentage: 2.1 + - compound: 'N2' + percentage: 2.6 + change: + type: 'terraforming' + progress: 0.15 + estimatedCompletion: '2050-12-31T00:00:00Z' + metadata: + source: 'terraforming-station' + stationId: 'mars-alpha-1' + bindings: + ws: + bindingVersion: 0.1.0 + ImageUploaded: + name: ImageUploaded + title: Image Uploaded Event + summary: A new image has been uploaded for a planet + description: Published when a planet image is successfully uploaded + contentType: application/json + payload: + $ref: '#/components/schemas/ImageUploadedEvent' + examples: + - name: Mars Photo Upload + summary: A new photo of Mars was uploaded + payload: + eventId: 'evt_1234567896' + eventType: 'image.uploaded' + timestamp: '2024-01-15T18:00:00Z' + planet: + id: 4 + name: 'Mars' + image: + url: 'https://cdn.scalar.com/images/mars-latest.jpg' + fileSize: 2048576 + mimeType: 'image/jpeg' + uploadedAt: '2024-01-15T18:00:00Z' + uploader: + id: 1 + name: 'Marc' + metadata: + source: 'rest-api' + requestId: 'req_jkl012' + bindings: + ws: + bindingVersion: 0.1.0 + UserSignedUp: + name: UserSignedUp + title: User Signed Up Event + summary: A new user has registered in the Scalar Galaxy + description: Published when a user successfully creates an account + contentType: application/json + payload: + $ref: '#/components/schemas/UserSignedUpEvent' + examples: + - name: New User Registration + summary: A new user joined the galaxy + payload: + eventId: 'evt_1234567897' + eventType: 'user.signed_up' + timestamp: '2024-01-15T18:30:00Z' + user: + id: 100 + name: 'Alice' + email: 'alice@example.com' + registration: + method: 'email' + source: 'web' + ipAddress: '192.168.1.100' + metadata: + source: 'rest-api' + requestId: 'req_mno345' + bindings: + ws: + bindingVersion: 0.1.0 + UserAuthenticated: + name: UserAuthenticated + title: User Authenticated Event + summary: A user has successfully authenticated + description: Published when a user logs in or refreshes their token + contentType: application/json + payload: + $ref: '#/components/schemas/UserAuthenticatedEvent' + examples: + - name: Successful Login + summary: User logged in successfully + payload: + eventId: 'evt_1234567898' + eventType: 'user.authenticated' + timestamp: '2024-01-15T19:00:00Z' + user: + id: 1 + name: 'Marc' + authentication: + method: 'bearer_token' + tokenType: 'JWT' + expiresAt: '2024-01-16T19:00:00Z' + session: + id: 'sess_abc123' + ipAddress: '192.168.1.100' + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)' + metadata: + source: 'auth-service' + requestId: 'req_pqr678' + bindings: + ws: + bindingVersion: 0.1.0 + UserProfileUpdated: + name: UserProfileUpdated + title: User Profile Updated Event + summary: A user's profile information has been modified + description: Published when user profile data is updated + contentType: application/json + payload: + $ref: '#/components/schemas/UserProfileUpdatedEvent' + examples: + - name: Profile Update + summary: User updated their profile + payload: + eventId: 'evt_1234567899' + eventType: 'user.profile_updated' + timestamp: '2024-01-15T19:30:00Z' + user: + id: 1 + name: 'Marc Updated' + changes: + - field: 'name' + oldValue: 'Marc' + newValue: 'Marc Updated' + metadata: + source: 'rest-api' + userId: 1 + requestId: 'req_stu901' + bindings: + ws: + bindingVersion: 0.1.0 + UserDeleted: + name: UserDeleted + title: User Deleted Event + summary: A user account has been deleted + description: Published when a user account is permanently removed + contentType: application/json + payload: + $ref: '#/components/schemas/UserDeletedEvent' + examples: + - name: Account Deletion + summary: A user deleted their account + payload: + eventId: 'evt_1234567900' + eventType: 'user.deleted' + timestamp: '2024-01-15T20:00:00Z' + user: + id: 99 + name: 'Deleted User' + deletion: + reason: 'user_request' + dataRetention: '30_days' + metadata: + source: 'user-service' + adminId: 1 + requestId: 'req_vwx234' + bindings: + ws: + bindingVersion: 0.1.0 + LoginAttempt: + name: LoginAttempt + title: Login Attempt Event + summary: A user login attempt has been made + description: Published for both successful and failed login attempts + contentType: application/json + payload: + $ref: '#/components/schemas/LoginAttemptEvent' + examples: + - name: Failed Login + summary: Someone tried to log in with wrong credentials + payload: + eventId: 'evt_1234567901' + eventType: 'login.attempt' + timestamp: '2024-01-15T20:30:00Z' + attempt: + email: 'marc@scalar.com' + success: false + reason: 'invalid_credentials' + ipAddress: '192.168.1.200' + userAgent: 'curl/7.68.0' + security: + riskScore: 0.3 + suspiciousActivity: false + metadata: + source: 'auth-service' + requestId: 'req_yza567' + bindings: + ws: + bindingVersion: 0.1.0 + SystemMaintenance: + name: SystemMaintenance + title: System Maintenance Event + summary: System maintenance has been scheduled or is in progress + description: Published for planned and emergency maintenance events + contentType: application/json + payload: + $ref: '#/components/schemas/SystemMaintenanceEvent' + examples: + - name: Scheduled Maintenance + summary: System maintenance is starting + payload: + eventId: 'evt_1234567902' + eventType: 'system.maintenance' + timestamp: '2024-01-15T21:00:00Z' + maintenance: + type: 'scheduled' + status: 'starting' + duration: 'PT2H' + description: 'Database optimization and security updates' + affectedServices: + - 'api' + - 'events' + - 'storage' + schedule: + startTime: '2024-01-15T21:00:00Z' + endTime: '2024-01-15T23:00:00Z' + metadata: + source: 'system-monitor' + maintenanceId: 'maint_20240115' + bindings: + ws: + bindingVersion: 0.1.0 + RateLimitExceeded: + name: RateLimitExceeded + title: Rate Limit Exceeded Event + summary: A user or system has exceeded rate limits + description: Published when rate limits are exceeded + contentType: application/json + payload: + $ref: '#/components/schemas/RateLimitExceededEvent' + examples: + - name: API Rate Limit Hit + summary: A user hit the API rate limit + payload: + eventId: 'evt_1234567903' + eventType: 'rate_limit.exceeded' + timestamp: '2024-01-15T21:30:00Z' + rateLimit: + limit: 1000 + window: 'PT1H' + exceededBy: 50 + user: + id: 1 + name: 'Marc' + request: + endpoint: '/planets' + method: 'GET' + ipAddress: '192.168.1.100' + metadata: + source: 'rate-limiter' + requestId: 'req_bcd890' + bindings: + ws: + bindingVersion: 0.1.0 + SecurityAlert: + name: SecurityAlert + title: Security Alert Event + summary: A security-related event has been detected + description: Published for security incidents and suspicious activity + contentType: application/json + payload: + $ref: '#/components/schemas/SecurityAlertEvent' + examples: + - name: Suspicious Activity + summary: Suspicious login pattern detected + payload: + eventId: 'evt_1234567904' + eventType: 'security.alert' + timestamp: '2024-01-15T22:00:00Z' + alert: + severity: 'medium' + type: 'suspicious_login_pattern' + description: 'Multiple failed login attempts from different IP addresses' + activity: + attempts: 15 + timeWindow: 'PT5M' + ipAddresses: + - '192.168.1.100' + - '192.168.1.101' + - '10.0.0.50' + actions: + - 'account_temporarily_locked' + - 'admin_notified' + metadata: + source: 'security-monitor' + alertId: 'alert_sec_001' + bindings: + ws: + bindingVersion: 0.1.0 + ApiVersionDeprecated: + name: ApiVersionDeprecated + title: API Version Deprecated Event + summary: An API version has been deprecated + description: Published when API versions are marked as deprecated + contentType: application/json + payload: + $ref: '#/components/schemas/ApiVersionDeprecatedEvent' + examples: + - name: Version Deprecation + summary: API version 1.0 is being deprecated + payload: + eventId: 'evt_1234567905' + eventType: 'api.version_deprecated' + timestamp: '2024-01-15T22:30:00Z' + deprecation: + version: '1.0' + deprecatedAt: '2024-01-15T22:30:00Z' + sunsetAt: '2024-07-15T22:30:00Z' + migrationGuide: 'https://docs.scalar.com/migration/v1-to-v2' + impact: + affectedEndpoints: + - '/planets' + - '/users' + breakingChanges: true + metadata: + source: 'api-gateway' + deprecationId: 'dep_20240115' + bindings: + ws: + bindingVersion: 0.1.0 + CelestialBodyCreated: + name: CelestialBodyCreated + title: Celestial Body Created Event + summary: A new celestial body has been added to the galaxy + description: Published when any type of celestial body is created + contentType: application/json + payload: + $ref: '#/components/schemas/CelestialBodyCreatedEvent' + examples: + - name: New Asteroid + summary: A new asteroid was discovered + payload: + eventId: 'evt_1234567906' + eventType: 'celestial_body.created' + timestamp: '2024-01-15T23:00:00Z' + celestialBody: + type: 'satellite' + satellite: + id: 16 + name: 'Asteroid X-2024' + type: 'asteroid' + diameter: 1.2 + discovery: + method: 'automated_survey' + observatory: 'Hubble Space Telescope' + metadata: + source: 'astronomical-survey' + surveyId: 'survey_20240115' + bindings: + ws: + bindingVersion: 0.1.0 + CelestialBodyUpdated: + name: CelestialBodyUpdated + title: Celestial Body Updated Event + summary: An existing celestial body has been modified + description: Published when celestial body properties are updated + contentType: application/json + payload: + $ref: '#/components/schemas/CelestialBodyUpdatedEvent' + examples: + - name: Orbit Update + summary: A satellite's orbit was recalculated + payload: + eventId: 'evt_1234567907' + eventType: 'celestial_body.updated' + timestamp: '2024-01-15T23:30:00Z' + celestialBody: + type: 'satellite' + satellite: + id: 15 + name: 'Deimos II' + orbit: + orbitalPeriod: 1.28 + distance: 23500 + changes: + - field: 'orbit.orbitalPeriod' + oldValue: 1.26 + newValue: 1.28 + - field: 'orbit.distance' + oldValue: 23460 + newValue: 23500 + metadata: + source: 'orbital-mechanics' + calculationId: 'calc_20240115' + bindings: + ws: + bindingVersion: 0.1.0 + OrbitalCollision: + name: OrbitalCollision + title: Orbital Collision Event + summary: Two celestial bodies have collided + description: Published when orbital collisions occur + contentType: application/json + payload: + $ref: '#/components/schemas/OrbitalCollisionEvent' + examples: + - name: Asteroid Impact + summary: An asteroid collided with a planet + payload: + eventId: 'evt_1234567908' + eventType: 'orbital.collision' + timestamp: '2024-01-16T00:00:00Z' + collision: + type: 'asteroid_planet' + severity: 'minor' + energy: '1.2e12' + objects: + - type: 'satellite' + id: 17 + name: 'Asteroid Y-2024' + mass: 1.5e12 + - type: 'planet' + id: 4 + name: 'Mars' + impact: + location: 'Valles Marineris' + craterSize: 150 + atmosphericEffects: 'dust_cloud' + metadata: + source: 'collision-detector' + simulationId: 'sim_20240116' + bindings: + ws: + bindingVersion: 0.1.0 + NewDiscovery: + name: NewDiscovery + title: New Discovery Event + summary: A significant astronomical discovery has been made + description: Published for major discoveries and breakthroughs + contentType: application/json + payload: + $ref: '#/components/schemas/NewDiscoveryEvent' + examples: + - name: Habitable Planet + summary: A potentially habitable planet was discovered + payload: + eventId: 'evt_1234567909' + eventType: 'discovery.new' + timestamp: '2024-01-16T00:30:00Z' + discovery: + type: 'habitable_planet' + significance: 'high' + description: 'Earth-like planet with liquid water detected' + celestialBody: + type: 'planet' + planet: + id: 50 + name: 'Kepler-442b' + type: 'super_earth' + habitabilityIndex: 0.84 + physicalProperties: + mass: 2.36 + radius: 1.34 + temperature: + average: 233 + research: + method: 'transit_photometry' + observatory: 'Kepler Space Telescope' + paper: 'https://arxiv.org/abs/2401.15000' + metadata: + source: 'research-team' + discoveryId: 'disc_20240116' + bindings: + ws: + bindingVersion: 0.1.0 + schemas: + PlanetCreatedEvent: + type: object + required: [eventId, eventType, timestamp, planet] + properties: + eventId: + type: string + format: uuid + description: Unique identifier for this event + examples: ['evt_1234567890'] + eventType: + type: string + enum: ['planet.created'] + description: Type of event + timestamp: + type: string + format: date-time + description: When the event occurred + examples: ['2024-01-15T14:30:00Z'] + planet: + $ref: '#/components/schemas/Planet' + metadata: + $ref: '#/components/schemas/EventMetadata' + PlanetUpdatedEvent: + type: object + required: [eventId, eventType, timestamp, planet, changes] + properties: + eventId: + type: string + format: uuid + eventType: + type: string + enum: ['planet.updated'] + timestamp: + type: string + format: date-time + planet: + $ref: '#/components/schemas/Planet' + changes: + type: array + items: + $ref: '#/components/schemas/FieldChange' + description: List of fields that were modified + metadata: + $ref: '#/components/schemas/EventMetadata' + PlanetDeletedEvent: + type: object + required: [eventId, eventType, timestamp, planetId, planetName] + properties: + eventId: + type: string + format: uuid + eventType: + type: string + enum: ['planet.deleted'] + timestamp: + type: string + format: date-time + planetId: + type: integer + format: int64 + planetName: + type: string + reason: + type: string + description: Reason for deletion + metadata: + $ref: '#/components/schemas/EventMetadata' + PlanetExplodedEvent: + type: object + required: [eventId, eventType, timestamp, planet, explosion] + properties: + eventId: + type: string + format: uuid + eventType: + type: string + enum: ['planet.exploded'] + timestamp: + type: string + format: date-time + planet: + $ref: '#/components/schemas/Planet' + explosion: + type: object + required: [type, intensity] + properties: + type: + type: string + enum: ['supernova', 'nova', 'collision', 'artificial'] + intensity: + type: number + minimum: 0 + maximum: 10 + description: Explosion intensity on a scale of 0-10 + energyReleased: + type: string + description: Energy released in joules (scientific notation) + newCelestialBodies: + type: array + items: + type: object + properties: + type: + type: string + enum: ['asteroid', 'comet', 'planet'] + name: + type: string + affectedObjects: + type: array + items: + type: object + properties: + planetId: + type: integer + format: int64 + distance: + type: number + description: Distance in kilometers + impact: + type: string + enum: ['none', 'minor', 'moderate', 'severe'] + metadata: + $ref: '#/components/schemas/EventMetadata' + SatelliteDiscoveredEvent: + type: object + required: [eventId, eventType, timestamp, satellite] + properties: + eventId: + type: string + format: uuid + eventType: + type: string + enum: ['satellite.discovered'] + timestamp: + type: string + format: date-time + satellite: + $ref: '#/components/schemas/Satellite' + discoveryMethod: + type: string + enum: + [ + 'telescope_observation', + 'space_probe', + 'mathematical_prediction', + 'collision_remnant', + ] + metadata: + $ref: '#/components/schemas/EventMetadata' + AtmosphereChangedEvent: + type: object + required: [eventId, eventType, timestamp, planet, atmosphere] + properties: + eventId: + type: string + format: uuid + eventType: + type: string + enum: ['atmosphere.changed'] + timestamp: + type: string + format: date-time + planet: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + atmosphere: + type: array + items: + type: object + properties: + compound: + type: string + percentage: + type: number + format: float + change: + type: object + properties: + type: + type: string + enum: ['terraforming', 'natural', 'pollution', 'atmospheric_loss'] + progress: + type: number + minimum: 0 + maximum: 1 + description: Progress of the change (0-1) + estimatedCompletion: + type: string + format: date-time + metadata: + $ref: '#/components/schemas/EventMetadata' + ImageUploadedEvent: + type: object + required: [eventId, eventType, timestamp, planet, image] + properties: + eventId: + type: string + format: uuid + eventType: + type: string + enum: ['image.uploaded'] + timestamp: + type: string + format: date-time + planet: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + image: + type: object + required: [url, fileSize, mimeType, uploadedAt] + properties: + url: + type: string + format: uri + fileSize: + type: integer + description: File size in bytes + mimeType: + type: string + uploadedAt: + type: string + format: date-time + uploader: + $ref: '#/components/schemas/User' + metadata: + $ref: '#/components/schemas/EventMetadata' + UserSignedUpEvent: + type: object + required: [eventId, eventType, timestamp, user] + properties: + eventId: + type: string + format: uuid + eventType: + type: string + enum: ['user.signed_up'] + timestamp: + type: string + format: date-time + user: + $ref: '#/components/schemas/User' + registration: + type: object + properties: + method: + type: string + enum: ['email', 'oauth', 'sso'] + source: + type: string + enum: ['web', 'mobile', 'api'] + ipAddress: + type: string + format: ipv4 + metadata: + $ref: '#/components/schemas/EventMetadata' + UserAuthenticatedEvent: + type: object + required: [eventId, eventType, timestamp, user, authentication] + properties: + eventId: + type: string + format: uuid + eventType: + type: string + enum: ['user.authenticated'] + timestamp: + type: string + format: date-time + user: + $ref: '#/components/schemas/User' + authentication: + type: object + required: [method, tokenType] + properties: + method: + type: string + enum: ['bearer_token', 'basic_auth', 'oauth', 'api_key'] + tokenType: + type: string + enum: ['JWT', 'OAuth', 'Basic'] + expiresAt: + type: string + format: date-time + session: + type: object + properties: + id: + type: string + ipAddress: + type: string + format: ipv4 + userAgent: + type: string + metadata: + $ref: '#/components/schemas/EventMetadata' + UserProfileUpdatedEvent: + type: object + required: [eventId, eventType, timestamp, user, changes] + properties: + eventId: + type: string + format: uuid + eventType: + type: string + enum: ['user.profile_updated'] + timestamp: + type: string + format: date-time + user: + $ref: '#/components/schemas/User' + changes: + type: array + items: + $ref: '#/components/schemas/FieldChange' + metadata: + $ref: '#/components/schemas/EventMetadata' + UserDeletedEvent: + type: object + required: [eventId, eventType, timestamp, user] + properties: + eventId: + type: string + format: uuid + eventType: + type: string + enum: ['user.deleted'] + timestamp: + type: string + format: date-time + user: + $ref: '#/components/schemas/User' + deletion: + type: object + properties: + reason: + type: string + enum: + ['user_request', 'admin_action', 'policy_violation', 'inactive'] + dataRetention: + type: string + enum: ['immediate', '30_days', '90_days', '1_year'] + metadata: + $ref: '#/components/schemas/EventMetadata' + LoginAttemptEvent: + type: object + required: [eventId, eventType, timestamp, attempt] + properties: + eventId: + type: string + format: uuid + eventType: + type: string + enum: ['login.attempt'] + timestamp: + type: string + format: date-time + attempt: + type: object + required: [email, success] + properties: + email: + type: string + format: email + success: + type: boolean + reason: + type: string + enum: + [ + 'invalid_credentials', + 'account_locked', + 'rate_limited', + 'success', + ] + ipAddress: + type: string + format: ipv4 + userAgent: + type: string + security: + type: object + properties: + riskScore: + type: number + minimum: 0 + maximum: 1 + suspiciousActivity: + type: boolean + metadata: + $ref: '#/components/schemas/EventMetadata' + SystemMaintenanceEvent: + type: object + required: [eventId, eventType, timestamp, maintenance] + properties: + eventId: + type: string + format: uuid + eventType: + type: string + enum: ['system.maintenance'] + timestamp: + type: string + format: date-time + maintenance: + type: object + required: [type, status, duration] + properties: + type: + type: string + enum: ['scheduled', 'emergency', 'hotfix'] + status: + type: string + enum: + ['scheduled', 'starting', 'in_progress', 'completed', 'failed'] + duration: + type: string + format: duration + description: Expected duration in ISO 8601 format + description: + type: string + affectedServices: + type: array + items: + type: string + schedule: + type: object + properties: + startTime: + type: string + format: date-time + endTime: + type: string + format: date-time + metadata: + $ref: '#/components/schemas/EventMetadata' + RateLimitExceededEvent: + type: object + required: [eventId, eventType, timestamp, rateLimit] + properties: + eventId: + type: string + format: uuid + eventType: + type: string + enum: ['rate_limit.exceeded'] + timestamp: + type: string + format: date-time + rateLimit: + type: object + required: [limit, window, exceededBy] + properties: + limit: + type: integer + window: + type: string + format: duration + exceededBy: + type: integer + user: + $ref: '#/components/schemas/User' + request: + type: object + properties: + endpoint: + type: string + method: + type: string + ipAddress: + type: string + format: ipv4 + metadata: + $ref: '#/components/schemas/EventMetadata' + SecurityAlertEvent: + type: object + required: [eventId, eventType, timestamp, alert] + properties: + eventId: + type: string + format: uuid + eventType: + type: string + enum: ['security.alert'] + timestamp: + type: string + format: date-time + alert: + type: object + required: [severity, type, description] + properties: + severity: + type: string + enum: ['low', 'medium', 'high', 'critical'] + type: + type: string + enum: + [ + 'suspicious_login_pattern', + 'brute_force', + 'data_breach', + 'malware_detected', + ] + description: + type: string + activity: + type: object + properties: + attempts: + type: integer + timeWindow: + type: string + format: duration + ipAddresses: + type: array + items: + type: string + format: ipv4 + actions: + type: array + items: + type: string + metadata: + $ref: '#/components/schemas/EventMetadata' + ApiVersionDeprecatedEvent: + type: object + required: [eventId, eventType, timestamp, deprecation] + properties: + eventId: + type: string + format: uuid + eventType: + type: string + enum: ['api.version_deprecated'] + timestamp: + type: string + format: date-time + deprecation: + type: object + required: [version, deprecatedAt, sunsetAt] + properties: + version: + type: string + deprecatedAt: + type: string + format: date-time + sunsetAt: + type: string + format: date-time + migrationGuide: + type: string + format: uri + impact: + type: object + properties: + affectedEndpoints: + type: array + items: + type: string + breakingChanges: + type: boolean + metadata: + $ref: '#/components/schemas/EventMetadata' + CelestialBodyCreatedEvent: + type: object + required: [eventId, eventType, timestamp, celestialBody] + properties: + eventId: + type: string + format: uuid + eventType: + type: string + enum: ['celestial_body.created'] + timestamp: + type: string + format: date-time + celestialBody: + $ref: '#/components/schemas/CelestialBody' + discovery: + type: object + properties: + method: + type: string + enum: + [ + 'telescope_observation', + 'space_probe', + 'mathematical_prediction', + 'collision_remnant', + ] + observatory: + type: string + metadata: + $ref: '#/components/schemas/EventMetadata' + CelestialBodyUpdatedEvent: + type: object + required: [eventId, eventType, timestamp, celestialBody, changes] + properties: + eventId: + type: string + format: uuid + eventType: + type: string + enum: ['celestial_body.updated'] + timestamp: + type: string + format: date-time + celestialBody: + $ref: '#/components/schemas/CelestialBody' + changes: + type: array + items: + $ref: '#/components/schemas/FieldChange' + metadata: + $ref: '#/components/schemas/EventMetadata' + OrbitalCollisionEvent: + type: object + required: [eventId, eventType, timestamp, collision, objects] + properties: + eventId: + type: string + format: uuid + eventType: + type: string + enum: ['orbital.collision'] + timestamp: + type: string + format: date-time + collision: + type: object + required: [type, severity, energy] + properties: + type: + type: string + enum: + [ + 'asteroid_planet', + 'planet_planet', + 'satellite_planet', + 'asteroid_asteroid', + ] + severity: + type: string + enum: ['minor', 'moderate', 'major', 'catastrophic'] + energy: + type: string + description: Energy released in joules (scientific notation) + objects: + type: array + minItems: 2 + maxItems: 2 + items: + type: object + properties: + type: + type: string + enum: ['planet', 'satellite'] + id: + type: integer + format: int64 + name: + type: string + mass: + type: number + description: Mass in kilograms + impact: + type: object + properties: + location: + type: string + craterSize: + type: number + description: Crater size in meters + atmosphericEffects: + type: string + enum: + [ + 'none', + 'dust_cloud', + 'atmospheric_heating', + 'atmospheric_loss', + ] + metadata: + $ref: '#/components/schemas/EventMetadata' + NewDiscoveryEvent: + type: object + required: [eventId, eventType, timestamp, discovery, celestialBody] + properties: + eventId: + type: string + format: uuid + eventType: + type: string + enum: ['discovery.new'] + timestamp: + type: string + format: date-time + discovery: + type: object + required: [type, significance, description] + properties: + type: + type: string + enum: + [ + 'habitable_planet', + 'new_element', + 'alien_life', + 'wormhole', + 'black_hole', + ] + significance: + type: string + enum: ['low', 'medium', 'high', 'revolutionary'] + description: + type: string + celestialBody: + $ref: '#/components/schemas/CelestialBody' + research: + type: object + properties: + method: + type: string + observatory: + type: string + paper: + type: string + format: uri + metadata: + $ref: '#/components/schemas/EventMetadata' + # Shared schemas + EventMetadata: + type: object + properties: + source: + type: string + description: Source system that generated the event + userId: + type: integer + format: int64 + description: ID of the user who triggered the event (if applicable) + requestId: + type: string + description: ID of the request that triggered the event + correlationId: + type: string + description: Correlation ID for tracing related events + severity: + type: string + enum: ['info', 'warning', 'error', 'critical'] + description: Event severity level + FieldChange: + type: object + required: [field, oldValue, newValue] + properties: + field: + type: string + description: Name of the field that changed + oldValue: + description: Previous value of the field + newValue: + description: New value of the field + # Reuse schemas from OpenAPI document + Planet: + description: A planet in the Scalar Galaxy + type: object + required: [id, name] + properties: + id: + type: integer + format: int64 + readOnly: true + name: + type: string + description: + type: string + type: + type: string + enum: [terrestrial, gas_giant, ice_giant, dwarf, super_earth] + habitabilityIndex: + type: number + format: float + minimum: 0 + maximum: 1 + physicalProperties: + type: object + properties: + mass: + type: number + format: float + radius: + type: number + format: float + gravity: + type: number + format: float + temperature: + type: object + properties: + min: + type: number + format: float + max: + type: number + format: float + average: + type: number + format: float + atmosphere: + type: array + items: + type: object + properties: + compound: + type: string + percentage: + type: number + format: float + discoveredAt: + type: string + format: date-time + image: + type: string + nullable: true + satellites: + type: array + items: + $ref: '#/components/schemas/Satellite' + creator: + $ref: '#/components/schemas/User' + tags: + type: array + items: + type: string + lastUpdated: + type: string + format: date-time + readOnly: true + Satellite: + description: Every satellite in the Scalar Galaxy + type: object + required: [name] + properties: + id: + type: integer + format: int64 + readOnly: true + name: + type: string + description: + type: string + diameter: + type: number + format: float + type: + type: string + enum: [moon, asteroid, comet] + orbit: + type: object + properties: + planet: + $ref: '#/components/schemas/Planet' + orbitalPeriod: + type: number + format: float + distance: + type: number + format: float + CelestialBody: + type: object + required: [type] + properties: + type: + type: string + enum: ['planet', 'satellite'] + description: Type of celestial body + planet: + $ref: '#/components/schemas/Planet' + description: Planet data (when type is 'planet') + satellite: + $ref: '#/components/schemas/Satellite' + description: Satellite data (when type is 'satellite') + description: A celestial body which can be either a planet or a satellite + User: + description: A user + type: object + properties: + id: + type: integer + format: int64 + readOnly: true + name: + type: string From a039c89933b511925668f5d4768cfd17ee0b0fbe Mon Sep 17 00:00:00 2001 From: Lautaro Petaccio Date: Mon, 24 Aug 2026 14:08:46 -0300 Subject: [PATCH 02/12] AsyncAPI 3.x: list scalar.yaml in reused_code.md It is vendored unmodified from github.com/scalar/scalar under MIT and names its source at the top of the file, but docs/for_developers.md also asks for third-party material to be listed in docs/reused_code.md. --- docs/reused_code.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/reused_code.md b/docs/reused_code.md index 40813d5d15..303d914d5c 100644 --- a/docs/reused_code.md +++ b/docs/reused_code.md @@ -29,3 +29,7 @@ are listed here: * _asyncapi/sut/openagents-cache.yaml_: AsyncAPI description of the OpenAgents shared-cache API, used unmodified as a test resource of the `asyncapi-parser` module. Released under MIT license. + +* _asyncapi/sut/scalar.yaml_: AsyncAPI description from + [Scalar](https://github.com/scalar/scalar), used unmodified as a test resource of `core`. + Released under MIT license. From 6aa46b3ec11d911ca9563fe2b812dac0dc8f2c09 Mon Sep 17 00:00:00 2001 From: Lautaro Petaccio Date: Sat, 5 Sep 2026 15:05:34 -0300 Subject: [PATCH 03/12] AsyncAPI 3.x: name the JSON Schema keywords the gene builder rewrites Review feedback: the keywords read and written while turning a payload into genes -- properties, required, const, enum, type, minimum, maximum and the type names -- were literals at fifteen sites, two of them also inside the DATA_KEYWORDS and SCHEMA_MAPS sets. They are object-level constants now, beside INLINE_PREFIX, and the two sets are built from them. These are JSON Schema's vocabulary rather than AsyncAPI's, so they belong here and not with the parser's keywords. --- .../asyncapi/builder/AsyncApiGeneBuilder.kt | 57 +++++++++++++------ 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiGeneBuilder.kt b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiGeneBuilder.kt index 1e42c076ee..1be992ed8e 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiGeneBuilder.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiGeneBuilder.kt @@ -33,17 +33,40 @@ object AsyncApiGeneBuilder { */ private const val INLINE_PREFIX = "_asyncapi_" + /* + The JSON Schema keywords this builder reads or rewrites. These are JSON Schema's + vocabulary, not AsyncAPI's, which is why they live here rather than with the parser. + */ + private const val PROPERTIES = "properties" + private const val PATTERN_PROPERTIES = "patternProperties" + private const val DEFINITIONS = "definitions" + private const val DEFS = "\$defs" + private const val REQUIRED = "required" + private const val CONST = "const" + private const val DEFAULT = "default" + private const val ENUM = "enum" + private const val EXAMPLE = "example" + private const val EXAMPLES = "examples" + private const val TYPE = "type" + private const val MINIMUM = "minimum" + private const val MAXIMUM = "maximum" + + private const val TYPE_STRING = "string" + private const val TYPE_INTEGER = "integer" + private const val TYPE_NUMBER = "number" + private const val TYPE_BOOLEAN = "boolean" + /** * The keywords whose value is literal data rather than a schema, so nothing inside them is * a keyword either. */ - private val DATA_KEYWORDS = setOf("const", "default", "enum", "example", "examples") + private val DATA_KEYWORDS = setOf(CONST, DEFAULT, ENUM, EXAMPLE, EXAMPLES) /** * The keywords whose value maps arbitrary names to schemas. Their keys come from the * document, so a field a service happens to call "const" or "default" must still be walked. */ - private val SCHEMA_MAPS = setOf("properties", "patternProperties", "definitions", "\$defs") + private val SCHEMA_MAPS = setOf(PROPERTIES, PATTERN_PROPERTIES, DEFINITIONS, DEFS) /** * The genes for a message's payload, or null when it declares none. @@ -90,14 +113,14 @@ object AsyncApiGeneBuilder { } val field = correlation.fieldName ?: return headers - val properties = headers.get("properties") + val properties = headers.get(PROPERTIES) if (properties == null || !properties.has(field)) { return headers } val copy = headers.deepCopy() as ObjectNode - val kept = (copy.get("properties") as ObjectNode).apply { remove(field) } + val kept = (copy.get(PROPERTIES) as ObjectNode).apply { remove(field) } /* When the stamped id was the only header, there is nothing left to vary. Returning @@ -109,11 +132,11 @@ object AsyncApiGeneBuilder { } //a field that is no longer there cannot be required either - (copy.get("required") as? ArrayNode)?.let { required -> + (copy.get(REQUIRED) as? ArrayNode)?.let { required -> val kept = required.filter { it.asText() != field } - copy.remove("required") + copy.remove(REQUIRED) if (kept.isNotEmpty()) { - copy.putArray("required").apply { kept.forEach { add(it) } } + copy.putArray(REQUIRED).apply { kept.forEach { add(it) } } } } @@ -285,24 +308,24 @@ object AsyncApiGeneBuilder { } val obj = node as ObjectNode - val const = obj.get("const") + val const = obj.get(CONST) if (const != null && !const.isContainerNode) { when { const.isTextual -> { - obj.remove("const") - obj.putArray("enum").add(const) - obj.put("type", "string") + obj.remove(CONST) + obj.putArray(ENUM).add(const) + obj.put(TYPE, TYPE_STRING) } const.isNumber -> { - obj.remove("const") - obj.set("minimum", const) - obj.set("maximum", const) - obj.put("type", if (const.isIntegralNumber) "integer" else "number") + obj.remove(CONST) + obj.set(MINIMUM, const) + obj.set(MAXIMUM, const) + obj.put(TYPE, if (const.isIntegralNumber) TYPE_INTEGER else TYPE_NUMBER) } const.isBoolean -> { - obj.remove("const") - obj.put("type", "boolean") + obj.remove(CONST) + obj.put(TYPE, TYPE_BOOLEAN) } } } From ec38f0a25b5440f18baee741012e8f00ab867116 Mon Sep 17 00:00:00 2001 From: Lautaro Petaccio Date: Sat, 5 Sep 2026 15:05:34 -0300 Subject: [PATCH 04/12] AsyncAPI 3.x: no Redis group in the individual Review feedback: keep Redis out of AsyncAPI until it is fully supported for REST. Every Redis option in EMConfig is still @Experimental, so a new problem type should not inherit that seam on day one. The redisSize parameter goes, the group size is fixed at zero, and copyContent no longer measures it. Mongo and DNS stay: neither is experimental. RPCIndividual and RestIndividual both still carry a Redis group; this is deliberately stricter than the classes it is modelled on. --- .../core/problem/asyncapi/data/AsyncApiIndividual.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividual.kt b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividual.kt index 05cdbd2b86..d63c7de2d0 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividual.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividual.kt @@ -31,10 +31,14 @@ class AsyncApiIndividual( mainSize: Int = allActions.size, sqlSize: Int = 0, mongoSize: Int = 0, - redisSize: Int = 0, dnsSize: Int = 0, + /* + No Redis group. Every Redis option in EMConfig is still @Experimental, so a new problem + type should not inherit that seam; the size is fixed at zero until Redis is fully + supported for REST, at which point this is one parameter to add. + */ groups: GroupsOfChildren = - getEnterpriseTopGroups(allActions, mainSize, sqlSize, mongoSize, redisSize, dnsSize, 0, 0) + getEnterpriseTopGroups(allActions, mainSize, sqlSize, mongoSize, 0, dnsSize, 0, 0) ) : ApiWsIndividual( sampleType, trackOperator, @@ -110,7 +114,6 @@ class AsyncApiIndividual( mainSize = groupsView()!!.sizeOfGroup(GroupsOfChildren.MAIN), sqlSize = groupsView()!!.sizeOfGroup(GroupsOfChildren.INITIALIZATION_SQL), mongoSize = groupsView()!!.sizeOfGroup(GroupsOfChildren.INITIALIZATION_MONGO), - redisSize = groupsView()!!.sizeOfGroup(GroupsOfChildren.INITIALIZATION_REDIS), dnsSize = groupsView()!!.sizeOfGroup(GroupsOfChildren.INITIALIZATION_DNS) ) } From d80eeac2caf1c21bbf03ae90c75b29f02ba383f5 Mon Sep 17 00:00:00 2001 From: Lautaro Petaccio Date: Sat, 5 Sep 2026 19:44:00 -0300 Subject: [PATCH 05/12] AsyncAPI 3.x: name the method the transport hides behind Review feedback asked where the transport is. The KDoc said "below the driver interface" without naming it; it now says SutController.executeAsyncApiAction, the one driver method that publishes and awaits, so the reference can be found by search. The core hands it an address, a payload and a correlation id, and never learns what broker is behind them. --- .../core/problem/asyncapi/data/AsyncApiAction.kt | 5 +++-- .../core/problem/asyncapi/data/AsyncApiIndividual.kt | 8 +++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiAction.kt b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiAction.kt index 9781b343b6..cc912120b9 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiAction.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiAction.kt @@ -11,8 +11,9 @@ import org.evomaster.core.search.gene.Gene * Publishing one message on one channel: the thing an AsyncAPI search actually does. * * It follows the shape of [org.evomaster.core.problem.rpc.RPCCallAction], which is the closest - * analogue in EvoMaster -- a call with no URL, made through a driver, whose mutable state is - * the input and whose response is read afterwards rather than searched over. + * analogue in EvoMaster -- a call with no URL, made through the driver's + * `SutController.executeAsyncApiAction`, whose mutable state is the input and whose response is + * read afterwards rather than searched over. * * Note what is deliberately *not* a gene: * diff --git a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividual.kt b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividual.kt index d63c7de2d0..78fd15b2a5 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividual.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividual.kt @@ -16,9 +16,11 @@ import kotlin.math.max * * There is one individual for every transport rather than one per transport: Kafka versus AMQP * versus a socket appears nowhere in here. What is being searched over is the operation and the - * payload, which are the same whatever moves the bytes; which wire is used is decided below the - * driver interface. Were the transport to leak in here there would have to be a - * `KafkaIndividual` and an `AmqpIndividual`, and nothing would be shared between them. + * payload, which are the same whatever moves the bytes. Which wire carries a message is decided + * in the driver, inside its implementation of `SutController.executeAsyncApiAction`: the core + * hands it an address, a payload and a correlation id, and never learns what broker is behind + * them. Were the transport to leak in here there would have to be a `KafkaIndividual` and an + * `AmqpIndividual`, and nothing would be shared between them. * * Everything structural is inherited: initialization actions for seeding a database, a main * group of the messages under test, and cleanup. From dcbfa4cbef618bddebdc79bbb15ec0e0bbfd51b0 Mon Sep 17 00:00:00 2001 From: Lautaro Petaccio Date: Tue, 8 Sep 2026 13:24:32 -0300 Subject: [PATCH 06/12] AsyncAPI 3.x: fields before functions in the gene builder and the action Review feedback: DATA_KEYWORDS and SCHEMA_MAPS were declared after every function, and AsyncApiAction.singleMessage after its first override. docs/for_developers.md puts fields first. The two sets now follow the keyword constants they are built from, and singleMessage opens the class body. --- .../core/problem/asyncapi/data/AsyncApiAction.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiAction.kt b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiAction.kt index cc912120b9..74231320df 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiAction.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiAction.kt @@ -65,6 +65,12 @@ class AsyncApiAction( ) : ApiWsAction(auth, false, inputParameters) { + /** + * Whether this action is the only one built for its operation. Set by whoever builds the + * cluster, since it depends on the other actions rather than on this one. + */ + var singleMessage: Boolean = true + companion object { /** * The name an action is known by, which must be unique within a search. @@ -79,12 +85,6 @@ class AsyncApiAction( override fun getName(): String = nameFor(operationId, messageId, singleMessage) - /** - * Whether this action is the only one built for its operation. Set by whoever builds the - * cluster, since it depends on the other actions rather than on this one. - */ - var singleMessage: Boolean = true - override fun seeTopGenes(): List = parameters.flatMap { it.seeGenes() } override fun copyContent(): AsyncApiAction = From a7e5539fa47d9dfb87509840bdc7c8b72c8ae899 Mon Sep 17 00:00:00 2001 From: Lautaro Petaccio Date: Tue, 8 Sep 2026 13:24:32 -0300 Subject: [PATCH 07/12] AsyncAPI 3.x: note that more databases are coming Review feedback: EvoMaster is adding several more databases; they need no handling here yet, but a TODO beside the three that are handled says where they will go. --- .../core/problem/asyncapi/data/AsyncApiIndividual.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividual.kt b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividual.kt index 78fd15b2a5..d3c962681c 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividual.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividual.kt @@ -31,6 +31,11 @@ class AsyncApiIndividual( index: Int = -1, allActions: MutableList, mainSize: Int = allActions.size, + /* + TODO Only SQL, Mongo and DNS are handled here for now. EvoMaster is gaining support for + several more databases; when one of them is fully supported for REST, it gets a size + parameter here and in copyContent(), and a slot in the getEnterpriseTopGroups call. + */ sqlSize: Int = 0, mongoSize: Int = 0, dnsSize: Int = 0, From 23c1d1912f20d2df762df0b2c5afe97644fa04a2 Mon Sep 17 00:00:00 2001 From: Lautaro Petaccio Date: Tue, 8 Sep 2026 13:59:36 -0300 Subject: [PATCH 08/12] AsyncAPI 3.x: the individual gets its own test suite Review against docs/for_developers.md: a suite is named after the class it exercises, and the four tests of AsyncApiIndividual were sitting in AsyncApiActionBuilderTest. Moved, unchanged, to AsyncApiIndividualTest. --- .../builder/AsyncApiActionBuilderTest.kt | 73 ------------- .../asyncapi/data/AsyncApiIndividualTest.kt | 101 ++++++++++++++++++ 2 files changed, 101 insertions(+), 73 deletions(-) create mode 100644 core/src/test/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividualTest.kt diff --git a/core/src/test/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiActionBuilderTest.kt b/core/src/test/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiActionBuilderTest.kt index b2267797d0..dfe8c20170 100644 --- a/core/src/test/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiActionBuilderTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiActionBuilderTest.kt @@ -3,11 +3,8 @@ package org.evomaster.core.problem.asyncapi.builder import com.webfuzzing.asyncapi.access.AsyncApiAccess import com.webfuzzing.asyncapi.models.AsyncApiDocument import org.evomaster.core.EMConfig -import org.evomaster.core.database.mongo.MongoDbAction import org.evomaster.core.problem.asyncapi.data.AsyncApiAction -import org.evomaster.core.problem.asyncapi.data.AsyncApiIndividual import org.evomaster.core.problem.asyncapi.param.AsyncApiParam -import org.evomaster.core.problem.enterprise.SampleType import org.evomaster.core.problem.rest.builder.RestActionBuilderV3 import org.evomaster.core.search.action.Action import org.evomaster.core.search.gene.ObjectGene @@ -380,74 +377,4 @@ class AsyncApiActionBuilderTest { (second.getValue("bessj") as AsyncApiAction).seeTopGenes().size ) } - - // ------------------------------------------------------------------ the individual - - @Test - fun testAnIndividualHoldsTheMessagesToPublish() { - - val actions = actionsOf("/asyncapi/sut/ncs-kafka.yaml").take(2).map { it.copy() as AsyncApiAction } - - val individual = AsyncApiIndividual(SampleType.RANDOM, actions.toMutableList()) - - assertEquals(2, individual.seeMainExecutableActions().size) - assertTrue(individual.canMutateStructure()) - } - - @Test - fun testMessagesCanBeAddedAndRemoved() { - - val actions = actionsOf("/asyncapi/sut/ncs-kafka.yaml").map { it.copy() as AsyncApiAction } - - val individual = AsyncApiIndividual(SampleType.RANDOM, mutableListOf(actions[0])) - assertEquals(1, individual.seeMainExecutableActions().size) - - individual.addAction(action = actions[1]) - assertEquals(2, individual.seeMainExecutableActions().size) - - individual.removeAction(0) - assertEquals(1, individual.seeMainExecutableActions().size) - assertEquals(actions[1].getName(), individual.seeMainExecutableActions().first().getName()) - } - - @Test - fun testCopyingAnIndividualKeepsItsMessages() { - - val actions = actionsOf("/asyncapi/sut/ncs-kafka.yaml").take(3).map { it.copy() as AsyncApiAction } - val individual = AsyncApiIndividual(SampleType.RANDOM, actions.toMutableList()) - - val copy = individual.copy() as AsyncApiIndividual - - assertEquals( - individual.seeMainExecutableActions().map { it.getName() }, - copy.seeMainExecutableActions().map { it.getName() } - ) - //a copy must be independent, or mutating one would change the other - assertNotSame(individual.seeMainExecutableActions()[0], copy.seeMainExecutableActions()[0]) - } - - @Test - fun testCopyingAnIndividualThatWasSetUpWithMoreThanSql() { - - /* - Only SQL is put in front of the messages today, but the individual inherits every - other kind of setup an enterprise individual can hold. The children are copied - wholesale, so a group whose size was not measured would not match what is handed - over, and the copy would fail outright rather than come back wrong. - */ - val actions = actionsOf("/asyncapi/sut/ncs-kafka.yaml").take(1).map { it.copy() as AsyncApiAction } - val individual = AsyncApiIndividual(SampleType.RANDOM, actions.toMutableList()) - - individual.addInitializingMongoDbActions( - actions = listOf(MongoDbAction("db", "collection", "collection", listOf())) - ) - - val copy = individual.copy() as AsyncApiIndividual - - assertEquals(1, copy.seeMainExecutableActions().size) - assertEquals( - individual.seeInitializingActions().size, - copy.seeInitializingActions().size - ) - } } diff --git a/core/src/test/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividualTest.kt b/core/src/test/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividualTest.kt new file mode 100644 index 0000000000..143be32504 --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividualTest.kt @@ -0,0 +1,101 @@ +package org.evomaster.core.problem.asyncapi.data + +import com.webfuzzing.asyncapi.access.AsyncApiAccess +import org.evomaster.core.EMConfig +import org.evomaster.core.database.mongo.MongoDbAction +import org.evomaster.core.problem.asyncapi.builder.AsyncApiActionBuilder +import org.evomaster.core.problem.asyncapi.builder.AsyncApiGeneBuilder +import org.evomaster.core.problem.enterprise.SampleType +import org.evomaster.core.problem.rest.builder.RestActionBuilderV3 +import org.evomaster.core.search.action.Action +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class AsyncApiIndividualTest { + + @BeforeEach + fun reset() { + RestActionBuilderV3.cleanCache() + } + + /** + * The six actions of the NCS document, in document order. Each is a fresh copy, so that a + * test may put it in an individual without touching the template it came from. + */ + private fun ncsActions(): List { + val schema = AsyncApiAccess.getAsyncApiFromResource("/asyncapi/sut/ncs-kafka.yaml") + val cluster = mutableMapOf() + AsyncApiActionBuilder.addActionsFromSchema(schema, cluster, AsyncApiGeneBuilder.options(EMConfig())) + return cluster.values.map { it.copy() as AsyncApiAction } + } + + + @Test + fun testAnIndividualHoldsTheMessagesToPublish() { + + val actions = ncsActions().take(2) + + val individual = AsyncApiIndividual(SampleType.RANDOM, actions.toMutableList()) + + assertEquals(2, individual.seeMainExecutableActions().size) + assertTrue(individual.canMutateStructure()) + } + + @Test + fun testMessagesCanBeAddedAndRemoved() { + + val actions = ncsActions() + + val individual = AsyncApiIndividual(SampleType.RANDOM, mutableListOf(actions[0])) + assertEquals(1, individual.seeMainExecutableActions().size) + + individual.addAction(action = actions[1]) + assertEquals(2, individual.seeMainExecutableActions().size) + + individual.removeAction(0) + assertEquals(1, individual.seeMainExecutableActions().size) + assertEquals(actions[1].getName(), individual.seeMainExecutableActions().first().getName()) + } + + @Test + fun testCopyingAnIndividualKeepsItsMessages() { + + val actions = ncsActions().take(3) + val individual = AsyncApiIndividual(SampleType.RANDOM, actions.toMutableList()) + + val copy = individual.copy() as AsyncApiIndividual + + assertEquals( + individual.seeMainExecutableActions().map { it.getName() }, + copy.seeMainExecutableActions().map { it.getName() } + ) + //a copy must be independent, or mutating one would change the other + assertNotSame(individual.seeMainExecutableActions()[0], copy.seeMainExecutableActions()[0]) + } + + @Test + fun testCopyingAnIndividualThatWasSetUpWithMoreThanSql() { + + /* + Only SQL is put in front of the messages today, but the individual inherits every + other kind of setup an enterprise individual can hold. The children are copied + wholesale, so a group whose size was not measured would not match what is handed + over, and the copy would fail outright rather than come back wrong. + */ + val actions = ncsActions().take(1) + val individual = AsyncApiIndividual(SampleType.RANDOM, actions.toMutableList()) + + individual.addInitializingMongoDbActions( + actions = listOf(MongoDbAction("db", "collection", "collection", listOf())) + ) + + val copy = individual.copy() as AsyncApiIndividual + + assertEquals(1, copy.seeMainExecutableActions().size) + assertEquals( + individual.seeInitializingActions().size, + copy.seeInitializingActions().size + ) + } +} From bd110cb7919f017ba6586491a5ecbcbfe6884d39 Mon Sep 17 00:00:00 2001 From: Lautaro Petaccio Date: Tue, 8 Sep 2026 13:59:36 -0300 Subject: [PATCH 09/12] AsyncAPI 3.x: the individual rejects a position it does not have addAction and removeAction took any position and left the group bookkeeping to object, which it does only when the index falls off the children altogether. With a setup action in front of the messages, removing "message -1" removed the setup action instead. Both now check the position against the main group, as RestIndividual does, and say so. --- .../asyncapi/data/AsyncApiIndividual.kt | 22 ++++++++++ .../asyncapi/data/AsyncApiIndividualTest.kt | 43 +++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividual.kt b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividual.kt index d3c962681c..b0f210b9fe 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividual.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividual.kt @@ -77,10 +77,21 @@ class AsyncApiIndividual( /** * Add a message to publish, at [relativePosition] within the main group, or at the end. + * + * @throws IllegalArgumentException if [relativePosition] is neither -1 nor a position in + * the main group, one past its last message included */ fun addAction(relativePosition: Int = -1, action: AsyncApiAction) { val main = GroupsOfChildren.MAIN + val size = groupsView()!!.sizeOfGroup(main) + + if (relativePosition < -1 || relativePosition > size) { + throw IllegalArgumentException( + "Position $relativePosition is out of range: the individual holds $size messages" + ) + } + val group = EnterpriseActionGroup(mutableListOf(action), AsyncApiAction::class.java) if (relativePosition == -1) { @@ -93,8 +104,19 @@ class AsyncApiIndividual( /** * Remove the message at [position] of the main group. + * + * @throws IllegalArgumentException if there is no message at [position] */ fun removeAction(position: Int) { + + val size = groupsView()!!.sizeOfGroup(GroupsOfChildren.MAIN) + + if (position < 0 || position >= size) { + throw IllegalArgumentException( + "Position $position is out of range: the individual holds $size messages" + ) + } + killChildByIndex(firstIndexOfMainGroup() + position) } diff --git a/core/src/test/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividualTest.kt b/core/src/test/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividualTest.kt index 143be32504..8b03ec44c4 100644 --- a/core/src/test/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividualTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividualTest.kt @@ -30,6 +30,8 @@ class AsyncApiIndividualTest { return cluster.values.map { it.copy() as AsyncApiAction } } + private fun names(individual: AsyncApiIndividual) = + individual.seeMainExecutableActions().map { it.getName() } @Test fun testAnIndividualHoldsTheMessagesToPublish() { @@ -98,4 +100,45 @@ class AsyncApiIndividualTest { copy.seeInitializingActions().size ) } + + @Test + fun testAMessageCanBeInsertedAtAPosition() { + + val actions = ncsActions() + val individual = AsyncApiIndividual(SampleType.RANDOM, mutableListOf(actions[0], actions[1])) + + individual.addAction(0, actions[2]) + //one past the last message is where a new last one goes + individual.addAction(3, actions[3]) + + assertEquals( + listOf(actions[2], actions[0], actions[1], actions[3]).map { it.getName() }, + names(individual) + ) + } + + @Test + fun testAPositionTheIndividualDoesNotHaveIsRejected() { + + val actions = ncsActions() + val individual = AsyncApiIndividual(SampleType.RANDOM, mutableListOf(actions[0])) + + /* + With a setup action in front of the messages, a position off the front of the main + group lands on that action rather than off the end of the children. The check has + to be made in terms of messages, or removing "message -1" would remove the setup. + */ + individual.addInitializingMongoDbActions( + actions = listOf(MongoDbAction("db", "collection", "collection", listOf())) + ) + + assertThrows(IllegalArgumentException::class.java) { individual.addAction(2, actions[1]) } + assertThrows(IllegalArgumentException::class.java) { individual.addAction(-2, actions[1]) } + assertThrows(IllegalArgumentException::class.java) { individual.removeAction(1) } + assertThrows(IllegalArgumentException::class.java) { individual.removeAction(-1) } + + //and none of the attempts changed anything + assertEquals(listOf(actions[0].getName()), names(individual)) + assertEquals(1, individual.seeInitializingActions().size) + } } From e5730c93da39b007ded93edb14d6f7c0cee0a4de Mon Sep 17 00:00:00 2001 From: Lautaro Petaccio Date: Tue, 8 Sep 2026 13:59:36 -0300 Subject: [PATCH 10/12] AsyncAPI 3.x: say that building replaces the cluster, and that core uses microcks.yaml too addActionsFromSchema clears the cluster it is given before filling it, as the REST builder does; its KDoc now says so. reused_code.md said microcks.yaml was a test resource of the parser module only, but a copy sits in core as well since the action builder tests. --- .../core/problem/asyncapi/builder/AsyncApiActionBuilder.kt | 3 ++- docs/reused_code.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiActionBuilder.kt b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiActionBuilder.kt index 104253abf3..12edcdeb08 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiActionBuilder.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiActionBuilder.kt @@ -22,7 +22,8 @@ import org.evomaster.core.search.action.Action object AsyncApiActionBuilder { /** - * Build one action per publishable message and add them to [actionCluster]. + * Build one action per publishable message and add them to [actionCluster], replacing + * whatever it held. * * @return anything that could not be built, to be reported to the user */ diff --git a/docs/reused_code.md b/docs/reused_code.md index 303d914d5c..ea915bcaf8 100644 --- a/docs/reused_code.md +++ b/docs/reused_code.md @@ -25,7 +25,7 @@ are listed here: * _asyncapi/sut/microcks.yaml_: AsyncAPI description from [Microcks](https://github.com/microcks/microcks), used unmodified as a test resource of the - `asyncapi-parser` module. Released under Apache-2.0 license. + `asyncapi-parser` module and of `core`. Released under Apache-2.0 license. * _asyncapi/sut/openagents-cache.yaml_: AsyncAPI description of the OpenAgents shared-cache API, used unmodified as a test resource of the `asyncapi-parser` module. Released under MIT license. From 09f2bb607ab5e49bfa700cb944c2851423da6b05 Mon Sep 17 00:00:00 2001 From: Lautaro Petaccio Date: Fri, 11 Sep 2026 16:25:27 -0300 Subject: [PATCH 11/12] AsyncAPI 3.x: a shorter description of the individual Review feedback: the class comment argued the architecture instead of saying what the class is for. It now says that, in two sentences. --- .../problem/asyncapi/data/AsyncApiIndividual.kt | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividual.kt b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividual.kt index b0f210b9fe..9b3d5e7102 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividual.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividual.kt @@ -12,18 +12,11 @@ import org.evomaster.core.database.sql.SqlAction import kotlin.math.max /** - * A sequence of messages to publish, and whatever is needed to set the service up first. + * A test case for an AsyncAPI service: the messages to publish, in order, after whatever + * setup the service needs, such as rows in a database. * - * There is one individual for every transport rather than one per transport: Kafka versus AMQP - * versus a socket appears nowhere in here. What is being searched over is the operation and the - * payload, which are the same whatever moves the bytes. Which wire carries a message is decided - * in the driver, inside its implementation of `SutController.executeAsyncApiAction`: the core - * hands it an address, a payload and a correlation id, and never learns what broker is behind - * them. Were the transport to leak in here there would have to be a `KafkaIndividual` and an - * `AmqpIndividual`, and nothing would be shared between them. - * - * Everything structural is inherited: initialization actions for seeding a database, a main - * group of the messages under test, and cleanup. + * Which transport carries the messages is the driver's concern, so one individual serves + * them all. */ class AsyncApiIndividual( sampleType: SampleType, From 5f20d3ad0f94b4383baa45c3e74e6166fac34983 Mon Sep 17 00:00:00 2001 From: Lautaro Petaccio Date: Fri, 11 Sep 2026 16:33:25 -0300 Subject: [PATCH 12/12] AsyncAPI 3.x: shorter comments on the action, its builder and the auth Review feedback: the class comments argued design decisions at length. They now say what each class is for; the reasoning lives in the PR. --- .../problem/asyncapi/auth/AsyncApiNoAuth.kt | 8 ++------ .../asyncapi/builder/AsyncApiActionBuilder.kt | 11 +++-------- .../problem/asyncapi/data/AsyncApiAction.kt | 18 +++--------------- .../asyncapi/data/AsyncApiIndividual.kt | 6 ++---- .../builder/AsyncApiActionBuilderTest.kt | 10 +++------- 5 files changed, 13 insertions(+), 40 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/auth/AsyncApiNoAuth.kt b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/auth/AsyncApiNoAuth.kt index c81e8d7e71..838427b3ee 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/auth/AsyncApiNoAuth.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/auth/AsyncApiNoAuth.kt @@ -4,11 +4,7 @@ import org.evomaster.core.problem.enterprise.auth.AuthenticationInfo import org.evomaster.core.problem.enterprise.auth.NoAuth /** - * An AsyncAPI action with no authentication set up. - * - * Authentication in AsyncAPI is a property of the connection to the broker rather than of an - * individual message: a security scheme is declared on a server, and the client is already - * authenticated by the time a message is published. So there is nothing per-action to vary - * yet, and every action carries this. + * No authentication. In AsyncAPI, security is declared on the server and holds for the whole + * connection, so there is nothing to vary per message yet. */ class AsyncApiNoAuth : AuthenticationInfo(NoAuth.NAME), NoAuth diff --git a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiActionBuilder.kt b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiActionBuilder.kt index 12edcdeb08..ebbf36c4a4 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiActionBuilder.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiActionBuilder.kt @@ -10,14 +10,9 @@ import org.evomaster.core.problem.util.ActionBuilderUtil import org.evomaster.core.search.action.Action /** - * Turns a parsed AsyncAPI document into the actions a search samples from, one per message that - * can actually be published. - * - * This is the AsyncAPI counterpart of - * [org.evomaster.core.problem.rest.builder.RestActionBuilderV3.addActionsFromSwagger] and - * [org.evomaster.core.problem.graphql.builder.GraphQLActionBuilder.addActionsFromSchema], and - * keeps their contract: fill a cluster keyed by action name, and return what had to be skipped - * rather than raising. + * Turns a parsed AsyncAPI document into action templates, one per message that can be + * published. Same contract as the REST and GraphQL builders: fill the cluster keyed by action + * name, and return the problems found rather than throwing. */ object AsyncApiActionBuilder { diff --git a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiAction.kt b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiAction.kt index 74231320df..e4d24e24ee 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiAction.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiAction.kt @@ -8,22 +8,10 @@ import org.evomaster.core.problem.enterprise.auth.AuthenticationInfo import org.evomaster.core.search.gene.Gene /** - * Publishing one message on one channel: the thing an AsyncAPI search actually does. + * Publishing one message on one channel, modelled on [org.evomaster.core.problem.rpc.RPCCallAction]. * - * It follows the shape of [org.evomaster.core.problem.rpc.RPCCallAction], which is the closest - * analogue in EvoMaster -- a call with no URL, made through the driver's - * `SutController.executeAsyncApiAction`, whose mutable state is the input and whose response is - * read afterwards rather than searched over. - * - * Note what is deliberately *not* a gene: - * - * - the **address**, which is fixed by the contract. There is nothing to search over in where - * a message goes; sending to a channel the service does not read would only waste executions. - * - the **correlation id**, which is stamped fresh at each execution. Searching over it could - * achieve nothing, since the service only echoes it back, and pairing a reply with its - * request needs a value unique to each execution rather than one carried in the genome. - * - the **reply**, which is an observation. It is read at fitness time to decide what was - * covered, exactly as RPC reads its response. + * The genes are the payload and the headers. The channel is fixed by the contract, the + * correlation id is stamped at execution time, and the reply is observed, so none of them is one. */ class AsyncApiAction( diff --git a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividual.kt b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividual.kt index 9b3d5e7102..245711c34c 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividual.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/data/AsyncApiIndividual.kt @@ -122,10 +122,8 @@ class AsyncApiIndividual( ) /* - Every group has to be measured, not just the two this class creates for itself. The - children are copied wholesale, so a size left at its default would not match what is - actually being handed over, and the group bookkeeping rejects that outright -- a copy - would fail for an individual that had picked up, say, a Mongo insertion along the way. + Every group is measured, not just the ones this class creates: the children are copied + wholesale, and a size left at its default would not match them. */ override fun copyContent(): AsyncApiIndividual = AsyncApiIndividual( diff --git a/core/src/test/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiActionBuilderTest.kt b/core/src/test/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiActionBuilderTest.kt index dfe8c20170..b24938c2c4 100644 --- a/core/src/test/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiActionBuilderTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/problem/asyncapi/builder/AsyncApiActionBuilderTest.kt @@ -151,13 +151,9 @@ class AsyncApiActionBuilderTest { val (cluster, _) = build("/asyncapi/sut/scalar.yaml") /* - This is how an operation ends up carrying several messages in practice. No document - in the corpus narrows to more than one with a `messages:` array; what happens is - the opposite -- an operation declares no array at all, and so inherits everything - its channel carries. Here one channel carries five genuinely different payloads and - the operation does not say which, so each becomes an action of its own: they are - different things to send, and picking between them is a structural choice rather - than a mutation. + No corpus document narrows an operation to several messages with a `messages:` + array; what happens is that the operation declares none and inherits everything its + channel carries. Each of the five becomes an action of its own. */ val userEvents = cluster.keys.filter { it.startsWith("subscribeToUserEvents") }.sorted()