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 bc46d6438c..5314db3c41 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. * @@ -45,12 +47,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 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.