Skip to content

Commit

Permalink
feat: #13 Add support for Asynchronous API mocking and testing
Browse files Browse the repository at this point in the history
Signed-off-by: Laurent Broudoux <[email protected]>
  • Loading branch information
lbroudoux committed Jan 2, 2024
1 parent d4f2f1d commit c7ce728
Show file tree
Hide file tree
Showing 7 changed files with 408 additions and 16 deletions.
34 changes: 33 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@
},
"devDependencies": {
"@types/jest": "^29.5.3",
"@types/ws": "^8.5.10",
"cross-env": "^7.0.3",
"jest": "^29.6.2",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"typescript": "^4.9.5"
"typescript": "^4.9.5",
"ws": "^8.16.0"
},
"files": [
"build"
Expand Down
142 changes: 142 additions & 0 deletions src/microcks-async-minion-container.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* Copyright The Microcks Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { AbstractStartedContainer, GenericContainer, StartedNetwork, StartedTestContainer, Wait, getContainerRuntimeClient } from "testcontainers";
import { MicrocksContainer } from "./microcks-container";

export class MicrocksAsyncMinionContainer extends GenericContainer {
static readonly MICROCKS_ASYNC_MINION_HTTP_PORT = 8081;

private network: StartedNetwork;
private extraProtocols: string = "";

constructor(network: StartedNetwork, image = "quay.io/microcks/microcks-uber-async-minion:nightly") {
super(image);
this.network = network;
}

/**
* Connect the MicrocksAsyncMinionContainer to a Kafka server to allow Kafka messages mocking.
* @param {KafkaConnection} connection Connection details to a Kafka broker.
* @returns this
*/
public withKafkaConnection(connection: KafkaConnection): this {
this.addProtocolIfNeeded('KAFKA');
this.withEnvironment({
ASYNC_PROTOCOLS: this.extraProtocols,
KAFKA_BOOTSTRAP_SERVER: connection.bootstrapServers
});
return this;
}

/**
* Connect the MicrocksAsyncMinionContainer to an Amazon SQS service to allow SQS messages mocking.
* @param {AmazonServiceConnection} connection Connection details to an Amazon SQS service.
* @returns this
*/
public withAmazonSQSConnection(connection: AmazonServiceConnection): this {
this.addProtocolIfNeeded('SQS');
this.withEnvironment({
ASYNC_PROTOCOLS: this.extraProtocols,
AWS_SQS_REGION: connection.region,
AWS_ACCESS_KEY_ID: connection.accessKey,
AWS_SECRET_ACCESS_KEY: connection.secretKey
});
if (connection.endpointOverride != undefined) {
this.withEnvironment({
AWS_SQS_ENDPOINT: connection.endpointOverride
});
}
return this;
}

/**
* Connect the MicrocksAsyncMinionContainer to an Amazon SNS service to allow SQS messages mocking.
* @param {AmazonServiceConnection} connection Connection details to an Amazon SQS service.
* @returns this
*/
public withAmazonSNSConnection(connection: AmazonServiceConnection): this {
this.addProtocolIfNeeded('SNS');
this.withEnvironment({
ASYNC_PROTOCOLS: this.extraProtocols,
AWS_SNS_REGION: connection.region,
AWS_ACCESS_KEY_ID: connection.accessKey,
AWS_SECRET_ACCESS_KEY: connection.secretKey
});
if (connection.endpointOverride != undefined) {
this.withEnvironment({
AWS_SNS_ENDPOINT: connection.endpointOverride
});
}
return this;
}

public override async start(): Promise<StartedMicrocksAsyncMinionContainer> {
this.withNetwork(this.network)
.withNetworkAliases("microcks-async-minion")
.withEnvironment({
MICROCKS_HOST_PORT: "microcks:" + MicrocksContainer.MICROCKS_HTTP_PORT
})
.withExposedPorts(...(this.hasExposedPorts ? this.exposedPorts : [MicrocksAsyncMinionContainer.MICROCKS_ASYNC_MINION_HTTP_PORT]))
.withWaitStrategy(Wait.forLogMessage(/.*Profile prod activated\..*/, 1));

return new StartedMicrocksAsyncMinionContainer(await super.start());
}

private addProtocolIfNeeded(protocol: string): void {
if (this.extraProtocols.indexOf(',' + protocol) == -1) {
this.extraProtocols += ',' + protocol;
}
}
}

export class StartedMicrocksAsyncMinionContainer extends AbstractStartedContainer {

constructor(
startedTestContainer: StartedTestContainer
) {
super(startedTestContainer);
}

/**
* Get the exposed mock endpoints for a WebSocket Service.
* @param {String} service The name of Service/API
* @param {String} version The version of Service/API
* @param {String} operationName The name of operation to get the endpoint for
* @returns A usable endpoint to interact with Microcks mocks.
*/
public getWSMockEndpoint(service: string, version: string, operationName: string): string {
// operationName may start with SUBSCRIBE or PUBLISH.
if (operationName.indexOf(' ') != -1) {
operationName = operationName.split(' ')[1];
}
let endpoint = `ws://${this.getHost()}:${this.getMappedPort(MicrocksAsyncMinionContainer.MICROCKS_ASYNC_MINION_HTTP_PORT)}/api`;
endpoint += `/ws/${service.replace(/\s/g, '+')}/${version.replace(/\s/g, '+')}/${operationName}`;
return endpoint;
}
}


export interface KafkaConnection {
bootstrapServers: string;
}

export interface AmazonServiceConnection {
region: string;
endpointOverride?: string;
accessKey: string;
secretKey: string;
}
20 changes: 14 additions & 6 deletions src/microcks-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ import * as path from "path";
import { readFile, stat } from "fs/promises";
import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers";

const HTTP_PORT = 8080;
const GRPC_PORT = 9090;

export class MicrocksContainer extends GenericContainer {
static readonly MICROCKS_HTTP_PORT = 8080;
static readonly MICROCKS_GRPC_PORT = 9090;

private mainArtifacts: string[] = [];
private secondaryArtifacts: string[] = [];
private secrets: Secret[] = [];
Expand Down Expand Up @@ -61,8 +61,16 @@ export class MicrocksContainer extends GenericContainer {
return this;
}

/**
* Get the image name used for instantiating this container.
* @returns The Docker image name
*/
public getImageName(): string {
return this.imageName.string;
}

public override async start(): Promise<StartedMicrocksContainer> {
this.withExposedPorts(...(this.hasExposedPorts ? this.exposedPorts : [HTTP_PORT, GRPC_PORT]))
this.withExposedPorts(...(this.hasExposedPorts ? this.exposedPorts : [MicrocksContainer.MICROCKS_HTTP_PORT, MicrocksContainer.MICROCKS_GRPC_PORT]))
.withWaitStrategy(Wait.forLogMessage(/.*Started MicrocksApplication.*/, 1));

let startedContainer = new StartedMicrocksContainer(await super.start());
Expand Down Expand Up @@ -157,8 +165,8 @@ export class StartedMicrocksContainer extends AbstractStartedContainer {
startedTestContainer: StartedTestContainer
) {
super(startedTestContainer);
this.httpPort = startedTestContainer.getMappedPort(HTTP_PORT);
this.grpcPort = startedTestContainer.getMappedPort(GRPC_PORT);
this.httpPort = startedTestContainer.getMappedPort(MicrocksContainer.MICROCKS_HTTP_PORT);
this.grpcPort = startedTestContainer.getMappedPort(MicrocksContainer.MICROCKS_GRPC_PORT);
}

/**
Expand Down
42 changes: 42 additions & 0 deletions src/microcks-containers-ensemble.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as path from "path";
import { GenericContainer, Network, Wait } from "testcontainers";
import { MicrocksContainersEnsemble } from "./microcks-containers-ensemble";
import { TestRequest, TestRunnerType } from "./microcks-container";
import { WebSocket } from "ws";

describe("MicrocksContainersEnsemble", () => {
jest.setTimeout(180_000);
Expand Down Expand Up @@ -65,6 +66,7 @@ describe("MicrocksContainersEnsemble", () => {
const ensemble = await new MicrocksContainersEnsemble(network)
.withMainArtifacts([path.resolve(resourcesDir, "apipastries-openapi.yaml")])
.withSecondaryArtifacts([path.resolve(resourcesDir, "apipastries-postman-collection.json")])
.withPostman()
.start();

const badImpl = await new GenericContainer("quay.io/microcks/contract-testing-demo:02")
Expand Down Expand Up @@ -116,4 +118,44 @@ describe("MicrocksContainersEnsemble", () => {
await network.stop();
});
// }

// start and mock async {
it("should start, load artifacts and mock async WebSocket", async () => {
const network = await new Network().start();

// Start ensemble, load artifacts and start other containers.
const ensemble = await new MicrocksContainersEnsemble(network, "quay.io/microcks/microcks-uber:nightly")
.withMainArtifacts([path.resolve(resourcesDir, "pastry-orders-asyncapi.yml")])
.withAsyncFeature("quay.io/microcks/microcks-uber-async-minion:nightly")
.start();

// Initialize messages list and connect to mock endpoint.
let messages: string[] = [];
let wsEndpoint = ensemble.getAsyncMinionContainer()?.getWSMockEndpoint("Pastry orders API", "0.1.0", "SUBSCRIBE pastry/orders");
let expectedMessage = "{\"id\":\"4dab240d-7847-4e25-8ef3-1530687650c8\",\"customerId\":\"fe1088b3-9f30-4dc1-a93d-7b74f0a072b9\",\"status\":\"VALIDATED\",\"productQuantities\":[{\"quantity\":2,\"pastryName\":\"Croissant\"},{\"quantity\":1,\"pastryName\":\"Millefeuille\"}]}";

const ws = new WebSocket(wsEndpoint as string);
ws.on('error', console.error);
ws.on('message', function message(data) {
messages.push(data.toString());
});

// Wait 7 seconds for messages from Async Minion WebSocket to get at least 2 messages.
await delay(7000);

expect(messages.length).toBeGreaterThan(0);
messages.forEach(message => {
expect(message).toBe(expectedMessage);
});

// Now stop the ensemble, the containers and the network.
await ensemble.stop();
await network.stop();
});
// }


function delay(ms: number) {
return new Promise( resolve => setTimeout(resolve, ms) );
}
});
Loading

0 comments on commit c7ce728

Please sign in to comment.