diff --git a/packages/cody/src/lib/hooks/behaviour-test-files/features/step_definitions/__feature__Steps.ts__tmpl__ b/packages/cody/src/lib/hooks/behaviour-test-files/features/step_definitions/__feature__Steps.ts__tmpl__ index 3afaa1fb..b38504a1 100644 --- a/packages/cody/src/lib/hooks/behaviour-test-files/features/step_definitions/__feature__Steps.ts__tmpl__ +++ b/packages/cody/src/lib/hooks/behaviour-test-files/features/step_definitions/__feature__Steps.ts__tmpl__ @@ -2,11 +2,12 @@ import { before, binding, given, then, when } from "cucumber-tsflow"; import {<%= whenEvent.propertyName %>} from "@app/shared/commands/<%= serviceNames.fileName %>/<%= whenEvent.fileName %>"; import { Event } from "@event-engine/messaging/event"; import { getConfiguredMessageBox } from "@server/infrastructure/configuredMessageBox"; -import { getConfiguredEventStore } from "@server/infrastructure/configuredEventStore"; +import { getConfiguredEventStore, PUBLIC_STREAM, WRITE_MODEL_STREAM } from "@server/infrastructure/configuredEventStore"; import {<%= givenEvent.propertyName %>} from "@app/shared/events/<%= serviceNames.fileName %>/<%= aggregate %>/<%= givenEvent.fileName %>"; import {<%= thenEvent.propertyName %>} from "@app/shared/events/<%= serviceNames.fileName %>/<%= aggregate %>/<%= thenEvent.fileName %>"; import expect from "expect"; - +import { setMessageMetadata } from "@event-engine/messaging/message"; +import { AggregateMeta } from "@event-engine/infrastructure/AggregateRepository"; @binding() // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -20,6 +21,9 @@ class <%= feature %>Steps { const listener = (streamName: string, events: Event[]) => { this.events.push(...events); }; + + this.eventStore.createStream(WRITE_MODEL_STREAM); + this.eventStore.createStream(PUBLIC_STREAM); this.eventStore.attachAppendToListener(listener); } @@ -29,23 +33,15 @@ class <%= feature %>Steps { <%- givenPayload %> }; - const event = <%= givenEvent.propertyName %>(payload); - - await this.messageBox.dispatch(event.name, event.payload, event.meta); - } - -/* multiple givens via iterator like above - @given('Car Added To Fleet') - public async givenCarAddedToFleet(): Promise<void> { - const payload = { - 'vehicleId': '6a76bead-46ce-4651-bea0-d8a387b2e9d0', - }; + let event = <%= givenEvent.propertyName %>(payload); - const event = carAddedToFleet(payload); + event = setMessageMetadata(event, AggregateMeta.ID, '<%= expectedIdentifier %>'); + event = setMessageMetadata(event, AggregateMeta.TYPE, '<%= givenAggregateMetaType %>'); + event = setMessageMetadata(event, AggregateMeta.VERSION, 1); + await this.eventStore.appendTo(WRITE_MODEL_STREAM, [event]); await this.messageBox.dispatch(event.name, event.payload, event.meta); } -*/ @when('<%= when %>') public async when<%= whenEvent.className %>(): Promise<void> { diff --git a/packages/cody/src/lib/hooks/on-feature.ts b/packages/cody/src/lib/hooks/on-feature.ts index af99c08a..37745df0 100644 --- a/packages/cody/src/lib/hooks/on-feature.ts +++ b/packages/cody/src/lib/hooks/on-feature.ts @@ -1,13 +1,16 @@ -import { CodyHook, Node, NodeType } from "@proophboard/cody-types"; -import { parseJsonMetadata } from "@proophboard/cody-utils"; -import { Context } from "./context"; -import { getOriginalNode } from "./utils/get-original-node"; -import { names} from "@event-engine/messaging/helpers"; -import { formatFiles, generateFiles } from "@nx/devkit"; -import { CodyResponseException, withErrorCheck } from "./utils/error-handling"; -import { detectService } from "./utils/detect-service"; +import {CodyHook, CodyResponseType, Node, NodeType} from "@proophboard/cody-types"; +import {getSingleTarget, isCodyError, parseJsonMetadata} from "@proophboard/cody-utils"; +import {Context} from "./context"; +import {getOriginalNode} from "./utils/get-original-node"; +import {names} from "@event-engine/messaging/helpers"; +import {formatFiles, generateFiles} from "@nx/devkit"; +import {CodyResponseException, withErrorCheck} from "./utils/error-handling"; +import {detectService} from "./utils/detect-service"; import {flushChanges} from "nx/src/generators/tree"; import {listChangesForCodyResponse} from "./utils/fs-tree"; +import {getNodeFromSyncedNodes} from "@cody-engine/cody/hooks/utils/node-tree"; +import {findAggregateState} from "@cody-engine/cody/hooks/utils/aggregate/find-aggregate-state"; +import {getVoMetadata} from "@cody-engine/cody/hooks/utils/value-object/get-vo-metadata"; const modeKey = "mode"; const modeValueTest = "test-scenario"; @@ -18,10 +21,16 @@ const thenKey = "then"; export const onFeature: CodyHook<Context> = async (feature: Node, ctx: Context) => { try { feature = getOriginalNode(feature, ctx); - const featureMeta : any = feature?.getMetadata() ? parseJsonMetadata<{service?: string}>(feature) : {}; + const featureMeta : any = feature?.getMetadata() ? parseJsonMetadata<{service?: string, mode?: string}>(feature) : {}; const parentContainer = feature.getParent(); const parentContainerMeta : any = parentContainer?.getMetadata() ? parseJsonMetadata<{service?: string}>(parentContainer) : {}; + if (featureMeta[modeKey] != modeValueTest && parentContainerMeta[modeKey] != modeValueTest) { + return { + cody: "Feature code generation is not yet implemented", + } + } + // add all test nodes to a map with their ID as the key, for easy access const validTestNodes = [NodeType.command, NodeType.event]; const testNodesMap = new Map<any, Node>(); @@ -31,67 +40,64 @@ export const onFeature: CodyHook<Context> = async (feature: Node, ctx: Context) } }); - // check if either the feature (the test) or its bounded context (the test container) have their mode set to "test" - if (featureMeta[modeKey] == modeValueTest || parentContainerMeta[modeKey] == modeValueTest) { - - let whenCommand : Node | undefined; - - // find "when" command node - feature.getChildren().forEach(function(elem) { - if (elem.getType() == NodeType.command) { - whenCommand = elem; - } - }); - - if (whenCommand) { - const givenNodes : Array<Node> = []; - const thenNodes : Array<Node> = []; - let currentNode : Node | undefined = whenCommand; - - // everything before the "when" command node is seen as "given" - while (currentNode) { - currentNode = testNodesMap.get(currentNode.getSources().first()?.getId()) || undefined; - - if (currentNode) { - givenNodes.unshift(currentNode); - } - } - - // everything after the "when" command is "then" - currentNode = whenCommand; - while (currentNode) { - currentNode = testNodesMap.get(currentNode.getTargets().first()?.getId()) || undefined; - - if (currentNode) { - thenNodes.push(currentNode); - } - } - - const changesForCodyResponse = await createTestFiles(feature.getName(), featureMeta, givenNodes, whenCommand, thenNodes, ctx); - - // for logging: - const loggedNodes: Array<string> = []; - loggedNodes.push("GIVEN"); - givenNodes.forEach(function(node) { - loggedNodes.push(node.getName()); - }); - loggedNodes.push("WHEN"); - loggedNodes.push(whenCommand.getName()); - loggedNodes.push("THEN"); - thenNodes.forEach(function(node) { - //@ToDo extract and slice expectedIdentifier - loggedNodes.push(node.getName()); - }); - - return { - cody: `Running test called "${feature.getName().trim()}".\nThese are the nodes included in the test: ${loggedNodes.toString()}`, - details: changesForCodyResponse - } + let whenCommand : Node | undefined; + + // find "when" command node + feature.getChildren().forEach(function(elem) { + if (elem.getType() == NodeType.command) { + whenCommand = elem; + } + }); + + if (!whenCommand) { + + return { + cody: "Feature code generation is not yet implemented", + } + } + + const givenNodes : Array<Node> = []; + const thenNodes : Array<Node> = []; + let currentNode : Node | undefined = whenCommand; + + // everything before the "when" command node is seen as "given" + while (currentNode) { + currentNode = testNodesMap.get(currentNode.getSources().first()?.getId()) || undefined; + + if (currentNode) { + givenNodes.unshift(currentNode); + } + } + + // everything after the "when" command is "then" + currentNode = whenCommand; + while (currentNode) { + currentNode = testNodesMap.get(currentNode.getTargets().first()?.getId()) || undefined; + + if (currentNode) { + thenNodes.push(currentNode); } } + const changesForCodyResponse = await createTestFiles(feature.getName(), featureMeta, givenNodes, whenCommand, thenNodes, ctx); + + // for logging: + const loggedNodes: Array<string> = []; + loggedNodes.push("GIVEN"); + givenNodes.forEach(function(node) { + loggedNodes.push(node.getName()); + }); + loggedNodes.push("WHEN"); + loggedNodes.push(whenCommand.getName()); + loggedNodes.push("THEN"); + thenNodes.forEach(function(node) { + //@ToDo extract and slice expectedIdentifier + loggedNodes.push(node.getName()); + }); + return { - cody: "Feature code generation is not yet implemented", + cody: `Running test called "${feature.getName().trim()}".\nThese are the nodes included in the test: ${loggedNodes.toString()}`, + details: changesForCodyResponse } } catch (e) { if(e instanceof CodyResponseException) { @@ -106,7 +112,39 @@ async function createTestFiles(featureName: string, featureMeta: any, givenNodes // if using a service from another board (e.g. Fleet Management), make sure to set this up in the test feature's metadata! const service = withErrorCheck(detectService, [whenCommand, ctx]); - const aggregate = 'car'; + let aggregate: Node | undefined; + for (const [, syncedNode] of ctx.syncedNodes) { + if(syncedNode.getType() === NodeType.command && syncedNode.getName() === whenCommand.getName() + && syncedNode.getTags().contains('pb:connected')) { + const aggregateObj = getSingleTarget(syncedNode, NodeType.aggregate); + + if(!isCodyError(aggregateObj)) { + aggregate = aggregateObj; + break; + } + } + } + + if (!aggregate) { + throw new CodyResponseException({ + cody: 'Could not find aggregate for test generation.', + type: CodyResponseType.Error + }); + } + + const givenNode = givenNodes[0]; + const syncedAggregate = withErrorCheck(getNodeFromSyncedNodes, [aggregate, ctx.syncedNodes]); + const aggregateState = withErrorCheck(findAggregateState, [syncedAggregate, ctx]); + const aggregateStateMeta = withErrorCheck(getVoMetadata, [aggregateState, ctx]); + const aggregateStateNames = names(aggregateState.getName()); + + const thenNode = thenNodes[0]; + const body = '{'+thenNode.getDescription().replaceAll('\'', '"')+'}'; + console.log('Json body: '+ body); + const thenNodeDescriptionObject = JSON.parse(body); + + const aggregateIdentifierProperty = aggregateStateMeta.identifier as keyof typeof thenNodeDescriptionObject; + const givenAggregateMetaType = `${names(service).className}.${names(aggregate.getName()).className}`; // TODO: currently only using the first "when" & "then" nodes const substitutions = { @@ -117,16 +155,16 @@ async function createTestFiles(featureName: string, featureMeta: any, givenNodes "given": featureMeta[givenKey], "when": featureMeta[whenKey], "then": featureMeta[thenKey], - "givenEvent": names(givenNodes[0].getName()), + "givenEvent": names(givenNode.getName()), + "givenAggregateMetaType": givenAggregateMetaType, "whenEvent": names(whenCommand.getName()), "thenEvent": names(thenNodes[0].getName()), - "givenPayload": givenNodes[0].getDescription(), + "givenPayload": givenNode.getDescription(), "whenPayload": whenCommand.getDescription(), "thenPayload": thenNodes[0].getDescription(), - "aggregate": aggregate, - "expectedIdentifier": "6a76bead-46ce-4651-bea0-d8a387b2e9d0" // TODO: read from "then" node payload (convert to json, read & remove "expectedIdentifier", convert back to string) + "aggregate": names(aggregate.getName()).fileName, + "expectedIdentifier": thenNodeDescriptionObject[aggregateIdentifierProperty] } - // console.log(substitutions); // generate test files const {tree} = ctx;