Skip to content

Commit

Permalink
Update to specmatic 2.5.0
Browse files Browse the repository at this point in the history
- Fix Stub creation for kafka and core
- Add tests for multi-port stub and kafka stub
  • Loading branch information
StarKhan6368 committed Feb 19, 2025
1 parent 5b8f5e2 commit 3963e78
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 20 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "specmatic",
"version": "0.0.1",
"specmaticVersion": "2.4.0",
"specmaticVersion": "2.5.0",
"description": "Node wrapper for Specmatic",
"main": "dist/index.js",
"scripts": {
Expand Down
104 changes: 96 additions & 8 deletions src/core/__tests__/stub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,97 @@ beforeEach(() => {
jest.resetAllMocks();
});

test('should be able to parse stub port on random assignment', async () => {
spawn.mockReturnValue(javaProcessMock);
setTimeout(() => {
const messageCallback = readableMock.on.mock.calls[0][1];
messageCallback("Free port found: 1234");
messageCallback(`- http://${HOST}:1234 serving endpoints from specs:`);
}, 0);

await expect(specmatic.startStub()).resolves.toStrictEqual(new Stub(HOST, 1234, `http://${HOST}:1234`, javaProcessMock));

expect(spawn.mock.calls[0][1][1]).toBe(`"${path.resolve(SPECMATIC_JAR_PATH)}"`);
expect(spawn.mock.calls[0][1][2]).toBe("stub");
});


test('should stick to random port assignment even if later logs contradict it', async () => {
spawn.mockReturnValue(javaProcessMock);
setTimeout(() => {
const messageCallback = readableMock.on.mock.calls[0][1];
messageCallback("Free port found: 1234");
messageCallback(`- http://${HOST}:9000 serving endpoints from specs:`);
}, 0);

await expect(specmatic.startStub()).resolves.toStrictEqual(new Stub(HOST, 1234, `http://${HOST}:1234`, javaProcessMock));

expect(spawn.mock.calls[0][1][1]).toBe(`"${path.resolve(SPECMATIC_JAR_PATH)}"`);
expect(spawn.mock.calls[0][1][2]).toBe("stub");
});

test('should pick the first port when multi-port stub is being used', async () => {
spawn.mockReturnValue(javaProcessMock);
const messages = [
"Stub server is running on the following URLs:",
"- http://localhost:1234 serving endpoints from specs:",
" 1. ./simpleRandom.yaml",
"",
"!@! http://localhost:9001 serving endpoints from specs #$!",
" 1. ./simple9001.yaml",
"",
"-: http://localhost:9002 serving endpoints from specs",
" 1. ./simple9002.yaml",
" 2. ./simple9002Second.yaml"
];

setTimeout(() => {
const messageCallback = readableMock.on.mock.calls[0][1];
messages.forEach(messageCallback);
}, 0);

await expect(specmatic.startStub()).resolves.toStrictEqual(new Stub(HOST, 1234, `http://${HOST}:1234`, javaProcessMock));

expect(spawn.mock.calls[0][1][1]).toBe(`"${path.resolve(SPECMATIC_JAR_PATH)}"`);
expect(spawn.mock.calls[0][1][2]).toBe("stub");
});

test('test with multi port stub and random port assignment', async () => {
spawn.mockReturnValue(javaProcessMock);
const messages = [
"Free port found: 1234",
"Stub server is running on the following URLs:",
"- http://localhost:9000 serving endpoints from specs:",
" 1. ./simpleRandom.yaml",
"",
"!@! http://localhost:9001 serving endpoints from specs #$!",
" 1. ./simple9001.yaml",
];

setTimeout(() => {
const messageCallback = readableMock.on.mock.calls[0][1];
messages.forEach(messageCallback);
}, 0);

await expect(specmatic.startStub()).resolves.toStrictEqual(new Stub(HOST, 1234, `http://${HOST}:1234`, javaProcessMock));

expect(spawn.mock.calls[0][1][1]).toBe(`"${path.resolve(SPECMATIC_JAR_PATH)}"`);
expect(spawn.mock.calls[0][1][2]).toBe("stub");
});

test('starts the specmatic stub server', async () => {
spawn.mockReturnValue(javaProcessMock);
setTimeout(() => readableMock.on.mock.calls[0][1](`Stub server is running on ${stubUrl}`), 0);
setTimeout(() => readableMock.on.mock.calls[0][1](`- ${stubUrl} serving endpoints from specs:`), 0);

await expect(specmatic.startStub(HOST, PORT)).resolves.toStrictEqual(stub);

expect(spawn.mock.calls[0][1][1]).toBe(`"${path.resolve(SPECMATIC_JAR_PATH)}"`);
expect(spawn.mock.calls[0][1][2]).toBe(`stub --host=${HOST} --port=${PORT}`);
});

test('starts the specmatic stub server with leading and trailing garbage values', async () => {
spawn.mockReturnValue(javaProcessMock);
setTimeout(() => readableMock.on.mock.calls[0][1](`-:!+@+ ${stubUrl} serving endpoints from specs: !@+#@`), 0);

await expect(specmatic.startStub(HOST, PORT)).resolves.toStrictEqual(stub);

Expand All @@ -54,7 +142,7 @@ test('returns host, port and stub url', async () => {
spawn.mockReturnValue(javaProcessMock);
const randomPort = 62269;
const stubUrl = `http://${HOST}:${randomPort}`;
setTimeout(() => readableMock.on.mock.calls[0][1](`Stub server is running on ${stubUrl}. Ctrl + C to stop.`), 0);
setTimeout(() => readableMock.on.mock.calls[0][1](`- ${stubUrl} serving endpoints from specs:`), 0);

const stub = new Stub(HOST, randomPort, stubUrl, javaProcessMock);

Expand All @@ -66,7 +154,7 @@ test('returns host, port and stub url', async () => {

test('fails if stub url is not available in start up message', async () => {
spawn.mockReturnValue(javaProcessMock);
setTimeout(() => readableMock.on.mock.calls[0][1](`Stub server is running`), 0);
setTimeout(() => readableMock.on.mock.calls[0][1]("serving endpoints from specs:"), 0);

await expect(specmatic.startStub(HOST, PORT)).toReject();

Expand All @@ -77,7 +165,7 @@ test('fails if stub url is not available in start up message', async () => {
test('fails if host info is not available in start up message', async () => {
spawn.mockReturnValue(javaProcessMock);
const stubUrl = `http://`;
setTimeout(() => readableMock.on.mock.calls[0][1](`Stub server is running on ${stubUrl}`), 0);
setTimeout(() => readableMock.on.mock.calls[0][1](`- ${stubUrl} serving endpoints from specs:`), 0);

await expect(specmatic.startStub(HOST, PORT)).toReject();

Expand All @@ -88,7 +176,7 @@ test('fails if host info is not available in start up message', async () => {
test('fails if port info is not available in start up message', async () => {
spawn.mockReturnValue(javaProcessMock);
const stubUrl = `http://${HOST}`;
setTimeout(() => readableMock.on.mock.calls[0][1](`Stub server is running on ${stubUrl}`), 0);
setTimeout(() => readableMock.on.mock.calls[0][1](`- ${stubUrl} serving endpoints from specs:`), 0);

await expect(specmatic.startStub(HOST, PORT)).toReject();

Expand All @@ -98,7 +186,7 @@ test('fails if port info is not available in start up message', async () => {

test('host and port are optional', async () => {
spawn.mockReturnValue(javaProcessMock);
setTimeout(() => readableMock.on.mock.calls[0][1](`Stub server is running on ${stubUrl}`), 0);
setTimeout(() => readableMock.on.mock.calls[0][1](`- ${stubUrl} serving endpoints from specs:`), 0);

await expect(specmatic.startStub()).resolves.toStrictEqual(stub);

Expand All @@ -108,7 +196,7 @@ test('host and port are optional', async () => {

test('takes additional pass through arguments', async () => {
spawn.mockReturnValue(javaProcessMock);
setTimeout(() => readableMock.on.mock.calls[0][1](`Stub server is running on ${stubUrl}`), 0);
setTimeout(() => readableMock.on.mock.calls[0][1](`- ${stubUrl} serving endpoints from specs:`), 0);

await expect(specmatic.startStub(HOST, PORT, ['p1', 'p2'])).resolves.toStrictEqual(stub);

Expand All @@ -118,7 +206,7 @@ test('takes additional pass through arguments', async () => {

test('additional pass through arguments can be string or number', async () => {
spawn.mockReturnValue(javaProcessMock);
setTimeout(() => readableMock.on.mock.calls[0][1](`Stub server is running on ${stubUrl}`), 0);
setTimeout(() => readableMock.on.mock.calls[0][1](`- ${stubUrl} serving endpoints from specs:`), 0);

await expect(specmatic.startStub(HOST, PORT, ['p1', 123])).resolves.toStrictEqual(stub);

Expand Down
59 changes: 51 additions & 8 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import http from 'http'
import { AddressInfo } from 'net'
import { gracefulShutdown } from './shutdownUtils'

const freePortMessage = 'Free port found'
const stubServingMessage = 'serving endpoints from specs'

export class Stub {
host: string
port: number
Expand All @@ -31,6 +34,7 @@ const startStub = (host?: string, port?: number, args?: (string | number)[]): Pr

logger.info('Stub: Starting server')
logger.debug(`Stub: Executing "${cmd}"`)
let parsedPort: string|null = null

return new Promise((resolve, reject) => {
const javaProcess = callCore(
Expand All @@ -42,16 +46,18 @@ const startStub = (host?: string, port?: number, args?: (string | number)[]): Pr
},
(message, error) => {
if (!error) {
if (message.indexOf('Stub server is running') > -1) {
if (message.indexOf(freePortMessage) > -1) {
logger.info(`Stub: ${message}`)
parsedPort = message.split(`${freePortMessage}:`)[1].trim()
}
else if (message.indexOf(stubServingMessage) > -1) {
logger.info(`Stub: ${message}`)
const stubInfo = message.split('on')
const stubInfo = message.split(stubServingMessage)
if (stubInfo.length < 2) reject('Cannot determine url from stub output')
else {
const url = stubInfo[1].trim()
const urlInfo = /(.*?):\/\/(.*?):([0-9]+)/.exec(url)
if ((urlInfo?.length ?? 0) < 4) reject('Cannot determine host and port from stub output')
else resolve(new Stub(urlInfo![2], parseInt(urlInfo![3]), urlInfo![0], javaProcess))
}
else try {
const stub = parseStubOutput(stubInfo, parsedPort, javaProcess);
resolve(stub);
} catch (e) { reject(extractErrorMessage(e)); }
} else if (message.indexOf('Address already in use') > -1) {
logger.error(`Stub: ${message}`)
reject('Address already in use')
Expand All @@ -66,6 +72,43 @@ const startStub = (host?: string, port?: number, args?: (string | number)[]): Pr
})
}

function extractErrorMessage(error: any): string {
if (typeof error === 'string') {
return error;
}

if (error instanceof Error) {
return error.message;
}

return String(error);
}

function parseStubOutput(
stubInfo: string[],
parsedPort: string | null,
javaProcess: ChildProcess
): Stub {
const url = stubInfo[0].trim();
const urlInfo = /\b(.*?):\/\/(.*?):([0-9]+)/.exec(url);

if (urlInfo === null || (urlInfo?.length ?? 0) < 4) {
throw new Error('Cannot determine host and port from stub output');
}

const regexPort = urlInfo[3];
const finalPort = parsedPort ?? regexPort;
let finalUrl: string;

if (parsedPort && parsedPort !== regexPort) {
finalUrl = `${urlInfo[1]}://${urlInfo[2]}:${parsedPort}`;
} else {
finalUrl = urlInfo[0];
}

return new Stub(urlInfo[2], Number.parseInt(finalPort), finalUrl, javaProcess);
}

const stopStub = async (stub: Stub) => {
logger.debug(`Stub: Stopping server at ${stub.url}`)
const javaProcess = stub.process
Expand Down
62 changes: 62 additions & 0 deletions src/kafka/__tests__/kafkaStub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import path from 'path';
import { specmaticKafkaJarName } from '../../config';
import { ChildProcess, spawn } from 'child_process';
import { Readable } from 'stream';
import { mock as jestMock } from 'jest-mock-extended';
import * as specmatic from '../..';
import { KafkaStub } from '..';

jest.mock('child_process');
jest.mock('terminate');

const SPECMATIC_JAR_PATH = path.resolve(__dirname, '..', '..', '..', '..','specmatic-beta', 'kafka', specmaticKafkaJarName);
const javaProcessMock = jestMock<ChildProcess>();
const readableMock = jestMock<Readable>();
javaProcessMock.stdout = readableMock;
javaProcessMock.stderr = readableMock;

beforeEach(() => {
jest.resetAllMocks();
});

test('starts the specmatic kafka stub server', async () => {
spawn.mockReturnValue(javaProcessMock);

setTimeout(() => {
const messageCallback = readableMock.on.mock.calls[0][1];
messageCallback("[Specmatic::Mock] Starting api server on port:29092");
messageCallback("[Specmatic::Mock] Kafka started on localhost:9092");
messageCallback("[Specmatic::Mock] Listening on topics: (product-queries)")
}, 0);

await expect(specmatic.startKafkaStub()).resolves.toStrictEqual(
new KafkaStub(9092, 29092, javaProcessMock)
);

expect(spawn.mock.calls[0][1][1]).toBe(`"${path.resolve(SPECMATIC_JAR_PATH)}"`);
expect(spawn.mock.calls[0][1][2]).toBe("");
});

test('takes additional pass through arguments', async () => {
spawn.mockReturnValue(javaProcessMock);

setTimeout(() => {
const messageCallback = readableMock.on.mock.calls[0][1];
messageCallback("[Specmatic::Mock] Starting api server on port:29092");
messageCallback("[Specmatic::Mock] Kafka started on localhost:1234");
messageCallback("[Specmatic::Mock] Listening on topics: (product-queries)")
}, 0);

await expect(specmatic.startKafkaStub(1234, ['p1', 'p2'])).resolves.toStrictEqual(
new KafkaStub(1234, 29092, javaProcessMock)
);

expect(spawn.mock.calls[0][1][1]).toBe(`"${path.resolve(SPECMATIC_JAR_PATH)}"`);
expect(spawn.mock.calls[0][1][2]).toBe(" --port=1234 p1 p2");
});

test('stopStub method stops any running stub server', async () => {
specmatic.stopKafkaMock(new KafkaStub(1234, 29092, javaProcessMock));
expect(readableMock.removeAllListeners).toHaveBeenCalledTimes(2);
expect(javaProcessMock.removeAllListeners).toHaveBeenCalledTimes(1);
});
6 changes: 3 additions & 3 deletions src/kafka/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ const startKafkaStub = (port?: number, args?: (string | number)[]): Promise<Kafk
if (!error) {
if (message.indexOf('Kafka started on') > -1) {
logger.info(`Kafka Stub: ${message}`);
const stubInfo = message.split('on ')[1];
const stubInfo = message.split('Kafka started on')[1];
if (stubInfo.length < 2) reject('Cannot determine port from kafka stub output');
else port = parseInt(stubInfo.split(':')[1].trim());
} else if (message.indexOf('Starting api server on port') > -1) {
logger.info(`Kafka Stub: ${message}`);
const stubInfo = message.split(':');
const stubInfo = message.split('Starting api server on port')[1];
if (stubInfo.length < 2) reject('Cannot determine api port from kafka stub output');
else apiPort = parseInt(stubInfo[1].trim());
else apiPort = parseInt(stubInfo.split(':')[1].trim());
} else if (message.indexOf('Listening on topic') > -1) {
logger.info(`Kafka Stub: ${message}`);
if (port && apiPort) resolve(new KafkaStub(port, apiPort, javaProcess));
Expand Down

0 comments on commit 3963e78

Please sign in to comment.