specmatic 1.0.0 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.babelrc +8 -12
- package/.github/dependabot.yml +18 -0
- package/.github/workflows/publish.yml +22 -0
- package/.github/workflows/test.yml +21 -0
- package/CONTRIBUTING.MD +17 -41
- package/README.md +152 -22
- package/dist/app.d.js +2 -1
- package/dist/bin/command.line.d.ts +2 -0
- package/dist/bin/command.line.js +36 -0
- package/dist/bin/index.d.ts +2 -0
- package/dist/bin/index.js +4 -5
- package/dist/common/logger.d.ts +3 -0
- package/dist/common/logger.js +62 -0
- package/dist/common/runner.d.ts +4 -0
- package/dist/common/runner.js +53 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +6 -7
- package/dist/core/index.d.ts +24 -0
- package/dist/core/index.js +297 -0
- package/dist/core/shutdownUtils.d.ts +2 -0
- package/dist/core/shutdownUtils.js +42 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +100 -9
- package/dist/kafka/index.d.ts +14 -0
- package/dist/kafka/index.js +158 -0
- package/global.d.ts +1 -0
- package/jest.config.json +5 -0
- package/package.json +50 -23
- package/specmatic.jar +0 -0
- package/src/app.d.ts +1 -1
- package/src/bin/__tests__/command.line.ts +40 -0
- package/src/bin/command.line.ts +35 -0
- package/src/bin/index.ts +2 -2
- package/src/common/__tests__/logger.ts +85 -0
- package/src/common/logger.ts +49 -0
- package/src/common/runner.ts +44 -0
- package/src/config.ts +2 -3
- package/src/core/__tests__/end.points.api.ts +103 -0
- package/src/core/__tests__/print.jar.version.ts +28 -0
- package/src/core/__tests__/set.expectation.ts +54 -0
- package/src/core/__tests__/set.test.results.ts +62 -0
- package/src/core/__tests__/stub.ts +136 -0
- package/src/core/__tests__/test.ts +176 -0
- package/src/core/index.ts +261 -0
- package/src/core/shutdownUtils.ts +21 -0
- package/src/downloadSpecmaticJar.js +30 -0
- package/src/index.ts +19 -2
- package/src/kafka/index.ts +140 -0
- package/test-resources/sample-junit-result-corrupt.xml +175 -0
- package/test-resources/sample-junit-result-generative.xml +389 -0
- package/test-resources/sample-junit-result-multiple.xml +304 -0
- package/test-resources/sample-junit-result-no-testname.xml +179 -0
- package/test-resources/sample-junit-result-single.xml +92 -0
- package/test-resources/sample-junit-result-skipped.xml +198 -0
- package/tsconfig.json +106 -20
- package/.github/workflows/npm-publish.yml +0 -25
- package/.vscode/settings.json +0 -7
- package/dist/bin/core.js +0 -30
- package/dist/lib/index.js +0 -107
- package/src/bin/__tests__/core.ts +0 -13
- package/src/bin/core.ts +0 -22
- package/src/lib/__tests__/index.ts +0 -122
- package/src/lib/index.ts +0 -84
- /package/{mockStub.json → test-resources/sample-mock-stub.json} +0 -0
- /package/{specmatic.json → test-resources/sample-specmatic.json} +0 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { ChildProcess, spawn } from 'child_process';
|
|
3
|
+
import { mock as jestMock, mockReset } from 'jest-mock-extended';
|
|
4
|
+
import { Readable } from 'stream';
|
|
5
|
+
import { copyFileSync, mkdirSync, existsSync } from 'fs';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import express from 'express';
|
|
8
|
+
|
|
9
|
+
import * as specmatic from '../..';
|
|
10
|
+
import { specmaticCoreJarName } from '../../config';
|
|
11
|
+
|
|
12
|
+
jest.mock('child_process');
|
|
13
|
+
|
|
14
|
+
const SPECMATIC_JAR_PATH = path.resolve(__dirname, '..', '..', '..', specmaticCoreJarName);
|
|
15
|
+
const CONTRACT_FILE_PATH = './contracts';
|
|
16
|
+
const HOST = 'localhost';
|
|
17
|
+
const PORT = 8000;
|
|
18
|
+
|
|
19
|
+
const javaProcessMock = jestMock<ChildProcess>();
|
|
20
|
+
const readableMock = jestMock<Readable>();
|
|
21
|
+
javaProcessMock.stdout = readableMock;
|
|
22
|
+
javaProcessMock.stderr = readableMock;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
jest.resetAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('runs the contract tests', async function () {
|
|
29
|
+
spawn.mockReturnValue(javaProcessMock);
|
|
30
|
+
setTimeout(() => {
|
|
31
|
+
copyReportFile();
|
|
32
|
+
javaProcessMock.on.mock.calls[0][1]()
|
|
33
|
+
}, 0);
|
|
34
|
+
|
|
35
|
+
await expect(specmatic.test(HOST, PORT, CONTRACT_FILE_PATH)).resolves.toBeTruthy();
|
|
36
|
+
|
|
37
|
+
expect(spawn.mock.calls[0][1][1]).toBe(`"${path.resolve(SPECMATIC_JAR_PATH)}"`);
|
|
38
|
+
expect(spawn.mock.calls[0][1][2]).toBe(`test ${path.resolve(CONTRACT_FILE_PATH)} --junitReportDir=dist/test-report --host=${HOST} --port=${PORT}`);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('takes additional pass through arguments', async () => {
|
|
42
|
+
spawn.mockReturnValue(javaProcessMock);
|
|
43
|
+
setTimeout(() => {
|
|
44
|
+
copyReportFile();
|
|
45
|
+
javaProcessMock.on.mock.calls[0][1]()
|
|
46
|
+
}, 0);
|
|
47
|
+
|
|
48
|
+
await expect(specmatic.test(HOST, PORT, CONTRACT_FILE_PATH, ['P1', 'P2'])).resolves.toBeTruthy();
|
|
49
|
+
|
|
50
|
+
expect(spawn.mock.calls[0][1][1]).toBe(`"${path.resolve(SPECMATIC_JAR_PATH)}"`);
|
|
51
|
+
expect(spawn.mock.calls[0][1][2]).toBe(`test ${path.resolve(CONTRACT_FILE_PATH)} --junitReportDir=dist/test-report --host=${HOST} --port=${PORT} P1 P2`);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('additional pass through arguments can be string or number', async () => {
|
|
55
|
+
spawn.mockReturnValue(javaProcessMock);
|
|
56
|
+
setTimeout(() => {
|
|
57
|
+
copyReportFile();
|
|
58
|
+
javaProcessMock.on.mock.calls[0][1]()
|
|
59
|
+
}, 0);
|
|
60
|
+
|
|
61
|
+
await expect(specmatic.test(HOST, PORT, CONTRACT_FILE_PATH, ['P1', 123])).resolves.toBeTruthy();
|
|
62
|
+
|
|
63
|
+
expect(spawn.mock.calls[0][1][1]).toBe(`"${path.resolve(SPECMATIC_JAR_PATH)}"`);
|
|
64
|
+
expect(spawn.mock.calls[0][1][2]).toBe(`test ${path.resolve(CONTRACT_FILE_PATH)} --junitReportDir=dist/test-report --host=${HOST} --port=${PORT} P1 123`);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('runs the contract tests with host and port optional', async function () {
|
|
68
|
+
spawn.mockReturnValue(javaProcessMock);
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
copyReportFile();
|
|
71
|
+
javaProcessMock.on.mock.calls[0][1]()
|
|
72
|
+
}, 0);
|
|
73
|
+
|
|
74
|
+
await expect(specmatic.test()).resolves.toBeTruthy();
|
|
75
|
+
|
|
76
|
+
expect(spawn.mock.calls[0][1][1]).toBe(`"${path.resolve(SPECMATIC_JAR_PATH)}"`);
|
|
77
|
+
expect(spawn.mock.calls[0][1][2]).toBe(`test --junitReportDir=dist/test-report`);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('runs the contract tests with contracts path optional', async function () {
|
|
81
|
+
spawn.mockReturnValue(javaProcessMock);
|
|
82
|
+
setTimeout(() => {
|
|
83
|
+
copyReportFile();
|
|
84
|
+
javaProcessMock.on.mock.calls[0][1]()
|
|
85
|
+
}, 0);
|
|
86
|
+
|
|
87
|
+
await expect(specmatic.test(HOST, PORT)).resolves.toBeTruthy();
|
|
88
|
+
|
|
89
|
+
expect(spawn.mock.calls[0][1][1]).toBe(`"${path.resolve(SPECMATIC_JAR_PATH)}"`);
|
|
90
|
+
expect(spawn.mock.calls[0][1][2]).toBe(`test --junitReportDir=dist/test-report --host=${HOST} --port=${PORT}`);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('runs the contract tests and returns a summary', async function () {
|
|
94
|
+
spawn.mockReturnValue(javaProcessMock);
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
copyReportFile();
|
|
97
|
+
javaProcessMock.on.mock.calls[0][1]()
|
|
98
|
+
}, 0);
|
|
99
|
+
|
|
100
|
+
await expect(specmatic.test()).resolves.toStrictEqual({
|
|
101
|
+
total: 5,
|
|
102
|
+
success: 3,
|
|
103
|
+
failure: 2,
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('runs the contract tests and returns a summary with skipped tests count included', async function () {
|
|
108
|
+
spawn.mockReturnValue(javaProcessMock);
|
|
109
|
+
setTimeout(() => {
|
|
110
|
+
copyReportFileWithName('sample-junit-result-skipped.xml');
|
|
111
|
+
javaProcessMock.on.mock.calls[0][1]()
|
|
112
|
+
}, 0);
|
|
113
|
+
|
|
114
|
+
await expect(specmatic.test()).resolves.toStrictEqual({
|
|
115
|
+
total: 3,
|
|
116
|
+
success: 2,
|
|
117
|
+
failure: 1,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('runs the contract tests and get summary when there is just one test', async function () {
|
|
122
|
+
spawn.mockReturnValue(javaProcessMock);
|
|
123
|
+
setTimeout(() => {
|
|
124
|
+
copyReportFileWithName('sample-junit-result-single.xml');
|
|
125
|
+
javaProcessMock.on.mock.calls[0][1]()
|
|
126
|
+
}, 0);
|
|
127
|
+
|
|
128
|
+
await expect(specmatic.test()).resolves.toStrictEqual({
|
|
129
|
+
total: 1,
|
|
130
|
+
success: 1,
|
|
131
|
+
failure: 0,
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('invocation makes sure previous junit report if any is deleted', async function () {
|
|
136
|
+
const spy = jest.spyOn(fs, 'rmSync');
|
|
137
|
+
spawn.mockReturnValue(javaProcessMock);
|
|
138
|
+
setTimeout(() => {
|
|
139
|
+
copyReportFileWithName('sample-junit-result-single.xml');
|
|
140
|
+
javaProcessMock.on.mock.calls[0][1]()
|
|
141
|
+
}, 0);
|
|
142
|
+
|
|
143
|
+
await specmatic.test();
|
|
144
|
+
|
|
145
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
146
|
+
expect(spy).toHaveBeenCalledWith(path.resolve('dist/test-report'), { force: true, recursive: true });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('passes an property indicating api endpoint based on host and port supplied when api coverage is enabled', async () => {
|
|
150
|
+
var app = express();
|
|
151
|
+
app.get('/', () => {});
|
|
152
|
+
|
|
153
|
+
spawn.mockReturnValue(javaProcessMock);
|
|
154
|
+
setTimeout(() => {
|
|
155
|
+
copyReportFile();
|
|
156
|
+
javaProcessMock.on.mock.calls[0][1]()
|
|
157
|
+
}, 0);
|
|
158
|
+
|
|
159
|
+
await expect(specmatic.testWithApiCoverage(app, HOST, PORT)).resolves.toBeTruthy();
|
|
160
|
+
|
|
161
|
+
expect(spawn.mock.calls[0][1][0]).toMatch(/endpointsAPI="http:\/\/.+?:[0-9]+?"/);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
function copyReportFile() {
|
|
165
|
+
copyReportFileWithName('sample-junit-result-multiple.xml');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function copyReportFileWithName(fileName: string) {
|
|
169
|
+
const destDir = path.resolve('dist/test-report');
|
|
170
|
+
if (!existsSync(destDir)) {
|
|
171
|
+
mkdirSync(destDir, { recursive: true });
|
|
172
|
+
}
|
|
173
|
+
const srcPath = path.resolve('test-resources', fileName);
|
|
174
|
+
const destPath = path.resolve(destDir, 'TEST-junit-jupiter.xml');
|
|
175
|
+
copyFileSync(srcPath, destPath);
|
|
176
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { ChildProcess } from 'child_process'
|
|
4
|
+
import { XMLParser } from 'fast-xml-parser'
|
|
5
|
+
import fs from 'fs'
|
|
6
|
+
import logger from '../common/logger'
|
|
7
|
+
import { callCore } from '../common/runner'
|
|
8
|
+
import listExpressEndpoints from 'express-list-endpoints'
|
|
9
|
+
import http from 'http'
|
|
10
|
+
import { AddressInfo } from 'net'
|
|
11
|
+
import { gracefulShutdown } from './shutdownUtils'
|
|
12
|
+
|
|
13
|
+
export class Stub {
|
|
14
|
+
host: string
|
|
15
|
+
port: number
|
|
16
|
+
url: string
|
|
17
|
+
process: ChildProcess
|
|
18
|
+
constructor(host: string, port: number, url: string, process: ChildProcess) {
|
|
19
|
+
this.host = host
|
|
20
|
+
this.port = port
|
|
21
|
+
this.url = url
|
|
22
|
+
this.process = process
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const startStub = (host?: string, port?: number, args?: (string | number)[]): Promise<Stub> => {
|
|
27
|
+
var cmd = `stub`
|
|
28
|
+
if (host) cmd += ` --host=${host}`
|
|
29
|
+
if (port) cmd += ` --port=${port}`
|
|
30
|
+
if (args) cmd += ' ' + args.join(' ')
|
|
31
|
+
|
|
32
|
+
logger.info('Stub: Starting server')
|
|
33
|
+
logger.debug(`Stub: Executing "${cmd}"`)
|
|
34
|
+
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const javaProcess = callCore(
|
|
37
|
+
cmd,
|
|
38
|
+
(err: any) => {
|
|
39
|
+
if (err) {
|
|
40
|
+
logger.error(`Stub: Exited with error ${err}`)
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
(message, error) => {
|
|
44
|
+
if (!error) {
|
|
45
|
+
if (message.indexOf('Stub server is running') > -1) {
|
|
46
|
+
logger.info(`Stub: ${message}`)
|
|
47
|
+
const stubInfo = message.split('on')
|
|
48
|
+
if (stubInfo.length < 2) reject('Cannot determine url from stub output')
|
|
49
|
+
else {
|
|
50
|
+
const url = stubInfo[1].trim()
|
|
51
|
+
const urlInfo = /(.*?):\/\/(.*?):([0-9]+)/.exec(url)
|
|
52
|
+
if ((urlInfo?.length ?? 0) < 4) reject('Cannot determine host and port from stub output')
|
|
53
|
+
else resolve(new Stub(urlInfo![2], parseInt(urlInfo![3]), urlInfo![0], javaProcess))
|
|
54
|
+
}
|
|
55
|
+
} else if (message.indexOf('Address already in use') > -1) {
|
|
56
|
+
logger.error(`Stub: ${message}`)
|
|
57
|
+
reject('Address already in use')
|
|
58
|
+
} else {
|
|
59
|
+
logger.debug(`Stub: ${message}`)
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
logger.error(`Stub: ${message}`)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const stopStub = async (stub: Stub) => {
|
|
70
|
+
logger.debug(`Stub: Stopping server at ${stub.url}`)
|
|
71
|
+
const javaProcess = stub.process
|
|
72
|
+
javaProcess.stdout?.removeAllListeners()
|
|
73
|
+
javaProcess.stderr?.removeAllListeners()
|
|
74
|
+
javaProcess.removeAllListeners('close')
|
|
75
|
+
logger.debug('Trying to stop stub process gracefully ...')
|
|
76
|
+
await gracefulShutdown(javaProcess)
|
|
77
|
+
logger.debug('Completed graceful termination of the stub process')
|
|
78
|
+
logger.info(`Stub: Stopped server at ${stub.url}`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const testWithApiCoverage = async (
|
|
82
|
+
expressApp: any,
|
|
83
|
+
host?: string,
|
|
84
|
+
port?: number,
|
|
85
|
+
contractPath?: string,
|
|
86
|
+
args?: (string | number)[]
|
|
87
|
+
): Promise<{ [k: string]: number } | undefined> => {
|
|
88
|
+
return new Promise(async (resolve, _reject) => {
|
|
89
|
+
let apiCoverageServer = await startApiCoverageServer(expressApp)
|
|
90
|
+
const results = await test(host, port, contractPath, args)
|
|
91
|
+
apiCoverageServer.close(() => {
|
|
92
|
+
resolve(results)
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const test = (host?: string, port?: number, contractPath?: string, args?: (string | number)[]): Promise<{ [k: string]: number } | undefined> => {
|
|
98
|
+
const specsPath = path.resolve(contractPath + '')
|
|
99
|
+
|
|
100
|
+
var cmd = `test`
|
|
101
|
+
if (contractPath) cmd += ` ${specsPath}`
|
|
102
|
+
cmd += ' --junitReportDir=dist/test-report'
|
|
103
|
+
if (host) cmd += ` --host=${host}`
|
|
104
|
+
if (port) cmd += ` --port=${port}`
|
|
105
|
+
if (args) cmd += ' ' + args.join(' ')
|
|
106
|
+
|
|
107
|
+
logger.info('Test: Running')
|
|
108
|
+
logger.debug(`Test: Executing "${cmd}"`)
|
|
109
|
+
|
|
110
|
+
const reportDir = path.resolve('dist/test-report')
|
|
111
|
+
fs.rmSync(reportDir, { recursive: true, force: true })
|
|
112
|
+
|
|
113
|
+
return new Promise((resolve, _reject) => {
|
|
114
|
+
callCore(
|
|
115
|
+
cmd,
|
|
116
|
+
(err: any) => {
|
|
117
|
+
if (err) logger.error(`Test: Failed with error ${err}`)
|
|
118
|
+
var testCases = parseJunitXML()
|
|
119
|
+
const total = testCases.length
|
|
120
|
+
const failure = testCases.filter((testcase: { [id: string]: any }) => testcase['failure'] || testcase['skipped']).length
|
|
121
|
+
const success = total - failure
|
|
122
|
+
var result = { total, success, failure }
|
|
123
|
+
resolve(result)
|
|
124
|
+
},
|
|
125
|
+
(message, error) => {
|
|
126
|
+
if (message.indexOf('API COVERAGE SUMMARY') > -1) {
|
|
127
|
+
console.log(message) //Log always for all log levels
|
|
128
|
+
} else {
|
|
129
|
+
logger[error ? 'error' : 'debug'](`Test: ${message}`)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const showTestResults = (testFn: (name: string, cb: () => void) => void) => {
|
|
137
|
+
var testCases = parseJunitXML()
|
|
138
|
+
testCases.map(function (testcase: { [id: string]: any }) {
|
|
139
|
+
var name = 'No Name'
|
|
140
|
+
if (testcase['system-out']) {
|
|
141
|
+
const nameTempArr = testcase['system-out']
|
|
142
|
+
.trim()
|
|
143
|
+
.replace(/\n/g, '')
|
|
144
|
+
.split(/display-name:.*Scenario: /)
|
|
145
|
+
if (nameTempArr.length > 1) name = nameTempArr[1].trim()
|
|
146
|
+
}
|
|
147
|
+
testFn(name, () => {
|
|
148
|
+
if (testcase.failure || testcase.skipped) throw new Error('Did not pass')
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const setExpectations = (stubPath: string, stubServerBaseUrl?: string): Promise<void> => {
|
|
154
|
+
const stubResponse = require(path.resolve(stubPath))
|
|
155
|
+
stubServerBaseUrl = stubServerBaseUrl || 'http://localhost:9000'
|
|
156
|
+
|
|
157
|
+
logger.info(`Set Expectations: Stub url is ${stubServerBaseUrl}`)
|
|
158
|
+
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
axios
|
|
161
|
+
.post(`${stubServerBaseUrl}/_specmatic/expectations`, stubResponse)
|
|
162
|
+
.then(response => {
|
|
163
|
+
logger.debug(`Set Expectations: ${response.data}`)
|
|
164
|
+
logger.info('Set Expectations: Finished')
|
|
165
|
+
resolve()
|
|
166
|
+
})
|
|
167
|
+
.catch(err => {
|
|
168
|
+
logger.error(`Set Expectations: Failed with error ${err}`)
|
|
169
|
+
reject(`Setting expecation failed with error ${err}`)
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const printJarVersion = () => {
|
|
175
|
+
const cmd = `--version`
|
|
176
|
+
logger.info('Print Jar Version: Running')
|
|
177
|
+
logger.debug(`Print Jar Version: Executing "${cmd}"`)
|
|
178
|
+
|
|
179
|
+
callCore(
|
|
180
|
+
cmd,
|
|
181
|
+
(err: any) => {
|
|
182
|
+
if (err) logger.error(`Print Jar Version: Failed with error ${err}`)
|
|
183
|
+
},
|
|
184
|
+
(message, error) => {
|
|
185
|
+
if (error) logger.error(`Print Jar Version: ${message}`)
|
|
186
|
+
else console.log(`${message}`)
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const startApiCoverageServer = (expressApp: any): Promise<http.Server> => {
|
|
192
|
+
logger.debug(`Registering API endpoint for coverage`)
|
|
193
|
+
let app = http.createServer((_req, res) => {
|
|
194
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
195
|
+
const endPoints = JSON.stringify(extractEndPoints(expressApp))
|
|
196
|
+
logger.debug(`Endpoints: ${endPoints}`)
|
|
197
|
+
res.end(endPoints)
|
|
198
|
+
})
|
|
199
|
+
return new Promise<http.Server>((resolve, reject) => {
|
|
200
|
+
app.on('error', err => {
|
|
201
|
+
logger.error('Error while starting end points server for api coverage', err)
|
|
202
|
+
reject('Error while starting end points server for api coverage')
|
|
203
|
+
})
|
|
204
|
+
app.listen({ host: '127.0.0.1', port: 0 }, () => {
|
|
205
|
+
const address = app.address() as AddressInfo | null
|
|
206
|
+
process.env['endpointsAPI'] = `http://${address?.address}:${address?.port}`
|
|
207
|
+
logger.info(`Endpoints API registered at ${process.env['endpointsAPI']}`)
|
|
208
|
+
resolve(app)
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const parseJunitXML = () => {
|
|
214
|
+
const reportPath = path.resolve('dist/test-report/TEST-junit-jupiter.xml')
|
|
215
|
+
var data = fs.readFileSync(reportPath)
|
|
216
|
+
const parser = new XMLParser()
|
|
217
|
+
var resultXml = parser.parse(data)
|
|
218
|
+
resultXml.testsuite.testcase = Array.isArray(resultXml.testsuite.testcase) ? resultXml.testsuite.testcase : [resultXml.testsuite.testcase]
|
|
219
|
+
return resultXml.testsuite.testcase
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const listEndPoints = (expressApp: any): { [key: string]: string[] } => {
|
|
223
|
+
const details = listExpressEndpoints(expressApp)
|
|
224
|
+
let endPoints: { [key: string]: string[] } = {}
|
|
225
|
+
details.map(apiDetail => {
|
|
226
|
+
endPoints[apiDetail.path] = apiDetail.methods
|
|
227
|
+
})
|
|
228
|
+
delete endPoints['*']
|
|
229
|
+
return endPoints
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function extractEndPoints(expressApp: any) {
|
|
233
|
+
let endPoints = listEndPoints(expressApp)
|
|
234
|
+
let springActuatorPayload = {
|
|
235
|
+
contexts: {
|
|
236
|
+
application: {
|
|
237
|
+
mappings: {
|
|
238
|
+
dispatcherServlets: {
|
|
239
|
+
dispatcherServlet: [],
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
}
|
|
245
|
+
Object.keys(endPoints)
|
|
246
|
+
.sort()
|
|
247
|
+
.map(path => {
|
|
248
|
+
springActuatorPayload.contexts.application.mappings.dispatcherServlets.dispatcherServlet.push({
|
|
249
|
+
details: {
|
|
250
|
+
requestMappingConditions: {
|
|
251
|
+
methods: endPoints[path].sort(),
|
|
252
|
+
patterns: [path.replace(/:([^/]+)/g, '{$1}').replace(/\\/g, '')],
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
} as never)
|
|
256
|
+
})
|
|
257
|
+
return springActuatorPayload
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export { startStub, stopStub, test, testWithApiCoverage, setExpectations, printJarVersion, showTestResults }
|
|
261
|
+
export { startApiCoverageServer }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ChildProcess } from 'child_process';
|
|
2
|
+
import nodeProcess from 'node:process';
|
|
3
|
+
import terminate from 'terminate/promise';
|
|
4
|
+
import logger from '../common/logger'
|
|
5
|
+
const kill = require('tree-kill');
|
|
6
|
+
|
|
7
|
+
export const gracefulShutdown = async function (javaProcess: ChildProcess): Promise<void> {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
logger.info('Sending SIGTERM to stop stub process')
|
|
10
|
+
kill(javaProcess.pid, 'SIGTERM', (err:Error) => {
|
|
11
|
+
if(err) {
|
|
12
|
+
logger.debug('Failed to send SIGTERM to stub process tree:', err);
|
|
13
|
+
resolve();
|
|
14
|
+
} else {
|
|
15
|
+
logger.debug('SIGTERM sent successfully to stub process tree');
|
|
16
|
+
resolve();
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const packageJson = require('../package.json'); // Import the package.json file
|
|
5
|
+
|
|
6
|
+
const specmaticVersion = packageJson.specmaticVersion;
|
|
7
|
+
const jarUrl = `https://github.com/znsio/specmatic/releases/download/${specmaticVersion}/specmatic.jar`;
|
|
8
|
+
const jarFilename = 'specmatic.jar'; // Specify the desired filename for the JAR
|
|
9
|
+
|
|
10
|
+
const downloadPath = path.resolve(__dirname, '..', jarFilename);
|
|
11
|
+
|
|
12
|
+
if (fs.existsSync(downloadPath)) {
|
|
13
|
+
console.log(`Deleting existing jar ...`);
|
|
14
|
+
fs.unlinkSync(downloadPath);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
console.log("Downloading Specmatic jar version: " + specmaticVersion + " ...");
|
|
18
|
+
axios({
|
|
19
|
+
method: 'get',
|
|
20
|
+
url: jarUrl,
|
|
21
|
+
responseType: 'stream',
|
|
22
|
+
})
|
|
23
|
+
.then((response) => {
|
|
24
|
+
response.data.pipe(fs.createWriteStream(downloadPath));
|
|
25
|
+
console.log("Finished downloading Specmatic jar");
|
|
26
|
+
})
|
|
27
|
+
.catch((error) => {
|
|
28
|
+
console.error('Error downloading Specmatic Core JAR file version: ' + specmaticVersion, error);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
export {
|
|
2
2
|
startStub,
|
|
3
|
+
startStub as startHttpStub,
|
|
3
4
|
stopStub,
|
|
5
|
+
stopStub as stopHttpStub,
|
|
4
6
|
test,
|
|
7
|
+
testWithApiCoverage,
|
|
5
8
|
setExpectations,
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
setExpectations as setHttpStubExpectations,
|
|
10
|
+
printJarVersion,
|
|
11
|
+
showTestResults,
|
|
12
|
+
} from './core'
|
|
13
|
+
export {
|
|
14
|
+
startKafkaStub,
|
|
15
|
+
startKafkaStub as startKafkaMock,
|
|
16
|
+
stopKafkaStub,
|
|
17
|
+
stopKafkaStub as stopKafkaMock,
|
|
18
|
+
verifyKafkaStubMessage,
|
|
19
|
+
verifyKafkaStubMessage as verifyKafkaMockMessage,
|
|
20
|
+
verifyKafkaStub,
|
|
21
|
+
verifyKafkaStub as verifyKafkaMock,
|
|
22
|
+
setKafkaStubExpectations,
|
|
23
|
+
setKafkaStubExpectations as setKafkaMockExpectations,
|
|
24
|
+
} from './kafka'
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { callKafka } from '../common/runner';
|
|
2
|
+
import logger from '../common/logger';
|
|
3
|
+
import { ChildProcess } from 'child_process';
|
|
4
|
+
import axios from 'axios';
|
|
5
|
+
import terminate from 'terminate/promise';
|
|
6
|
+
|
|
7
|
+
export class KafkaStub {
|
|
8
|
+
port: number;
|
|
9
|
+
apiPort: number;
|
|
10
|
+
process: ChildProcess;
|
|
11
|
+
constructor(port: number, apiPort: number, process: ChildProcess) {
|
|
12
|
+
this.port = port;
|
|
13
|
+
this.apiPort = apiPort;
|
|
14
|
+
this.process = process;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const startKafkaStub = (port?: number, args?: (string | number)[]): Promise<KafkaStub> => {
|
|
19
|
+
var cmd = ``;
|
|
20
|
+
if (port) cmd += ` --port=${port}`;
|
|
21
|
+
if (args) cmd += ' ' + args.join(' ');
|
|
22
|
+
|
|
23
|
+
logger.info('Kafka Stub: Starting server');
|
|
24
|
+
logger.debug(`Kafka Stub: Executing "${cmd}"`);
|
|
25
|
+
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
let port: number, apiPort: number;
|
|
28
|
+
const javaProcess = callKafka(
|
|
29
|
+
cmd,
|
|
30
|
+
(err: any) => {
|
|
31
|
+
if (err) {
|
|
32
|
+
logger.error(`Kafka Stub: Exited with error ${err}`);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
(message, error) => {
|
|
36
|
+
if (!error) {
|
|
37
|
+
if (message.indexOf('Kafka started on port') > -1) {
|
|
38
|
+
logger.info(`Kafka Stub: ${message}`);
|
|
39
|
+
const stubInfo = message.split('on port');
|
|
40
|
+
if (stubInfo.length < 2) reject('Cannot determine port from kafka stub output');
|
|
41
|
+
else port = parseInt(stubInfo[1].trim());
|
|
42
|
+
} else if (message.indexOf('Starting api server on port') > -1) {
|
|
43
|
+
logger.info(`Kafka Stub: ${message}`);
|
|
44
|
+
const stubInfo = message.split(':');
|
|
45
|
+
if (stubInfo.length < 2) reject('Cannot determine api port from kafka stub output');
|
|
46
|
+
else apiPort = parseInt(stubInfo[1].trim());
|
|
47
|
+
} else if (message.indexOf('Listening on topic') > -1) {
|
|
48
|
+
logger.info(`Kafka Stub: ${message}`);
|
|
49
|
+
if (port && apiPort) resolve(new KafkaStub(port, apiPort, javaProcess));
|
|
50
|
+
else reject('No port or api port information available but kafka stub listening on topic already');
|
|
51
|
+
} else if (message.indexOf('Address already in use') > -1) {
|
|
52
|
+
logger.error(`Kafka Stub: ${message}`);
|
|
53
|
+
reject('Address already in use');
|
|
54
|
+
} else {
|
|
55
|
+
logger.debug(`Kafka Stub: ${message}`);
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
logger.error(`Kafka Stub: ${message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const stopKafkaStub = async (stub: KafkaStub) => {
|
|
66
|
+
logger.debug(`Kafka Stub: Stopping at port=${stub.port}, apiPort=${stub.apiPort}`);
|
|
67
|
+
const javaProcess = stub.process;
|
|
68
|
+
javaProcess.stdout?.removeAllListeners();
|
|
69
|
+
javaProcess.stderr?.removeAllListeners();
|
|
70
|
+
javaProcess.removeAllListeners('close');
|
|
71
|
+
await terminate(javaProcess.pid!);
|
|
72
|
+
logger.info(`Kafka Stub: Stopped at port=${stub.port}, apiPort=${stub.apiPort}`);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const setKafkaStubExpectations = (stub: KafkaStub, expecations: any): Promise<void> => {
|
|
76
|
+
const exectationsUrl = `http://localhost:${stub.apiPort}/_expectations`;
|
|
77
|
+
logger.info(`Kafka Set Expectations: Url is ${exectationsUrl}`);
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
axios
|
|
80
|
+
.post(`${exectationsUrl}`, expecations, {
|
|
81
|
+
headers: {
|
|
82
|
+
Accept: 'application/json',
|
|
83
|
+
'Content-Type': 'application/json',
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
.then(response => {
|
|
87
|
+
logger.debug(`Kafka Set Expectations: Finished ${JSON.stringify(response.data)}`)
|
|
88
|
+
resolve()
|
|
89
|
+
})
|
|
90
|
+
.catch(err => {
|
|
91
|
+
logger.error(`Kafka Set Expectations: Failed with error ${err}`)
|
|
92
|
+
reject(`Set expectation failed with error ${err}`)
|
|
93
|
+
})
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const verifyKafkaStub = (stub: KafkaStub): Promise<Boolean> => {
|
|
98
|
+
const verificationUrl = `http://localhost:${stub.apiPort}/_expectations/verifications`;
|
|
99
|
+
logger.info(`Kafka Verification: Url is ${verificationUrl}`);
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
axios.post(`${verificationUrl}`, {
|
|
102
|
+
headers: {
|
|
103
|
+
Accept: 'application/json',
|
|
104
|
+
'Content-Type': 'application/json',
|
|
105
|
+
},
|
|
106
|
+
})
|
|
107
|
+
.then(response => {
|
|
108
|
+
logger.debug(`Kafka Verification: Finished ${JSON.stringify(response.data)}`);
|
|
109
|
+
if (!response.data.success) logger.info(`Kafka Verification: Errors\n${JSON.stringify(response.data)}`);
|
|
110
|
+
resolve(response.data.success);
|
|
111
|
+
})
|
|
112
|
+
.catch(err => {
|
|
113
|
+
logger.error(`Kafka Verification: Failed with error ${err}`);
|
|
114
|
+
reject(`Kafka verification failed with error ${err}`);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const verifyKafkaStubMessage = (stub: KafkaStub, topic: string, value: string): Promise<Boolean> => {
|
|
120
|
+
const verificationUrl = `http://localhost:${stub.apiPort}/_verifications`;
|
|
121
|
+
logger.info(`Kafka Verify Message: Url is ${verificationUrl}`);
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
axios.post(`${verificationUrl}`, { topic: topic, value: value }, {
|
|
124
|
+
headers: {
|
|
125
|
+
Accept: 'application/json',
|
|
126
|
+
'Content-Type': 'application/json',
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
.then(response => {
|
|
130
|
+
logger.debug(`Kafka Verify Message: Finished ${JSON.stringify(response.data)}`);
|
|
131
|
+
resolve(response.data.received);
|
|
132
|
+
})
|
|
133
|
+
.catch(err => {
|
|
134
|
+
logger.error(`Kafka Verify Message: Failed with error ${err}`);
|
|
135
|
+
reject(`Kafka message verification failed with error ${err}`);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export { startKafkaStub, stopKafkaStub, verifyKafkaStubMessage, verifyKafkaStub, setKafkaStubExpectations };
|