Skip to content

Commit

Permalink
Decrease hardcoded assumptions for test scenarios
Browse files Browse the repository at this point in the history
  • Loading branch information
bofalke committed Mar 26, 2024
1 parent 0acb9d3 commit cfa6ec7
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}

Expand All @@ -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> {
Expand Down
182 changes: 110 additions & 72 deletions packages/cody/src/lib/hooks/on-feature.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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>();
Expand All @@ -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) {
Expand All @@ -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 = {
Expand All @@ -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;
Expand Down

0 comments on commit cfa6ec7

Please sign in to comment.