specmatic 0.70.8 → 0.72.1

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/README.md CHANGED
@@ -82,6 +82,9 @@ View test results in any framework so that it shows up in IDE specific test resu
82
82
  `printJarVersion()` <br />
83
83
  method to print the version of specmatic.jar
84
84
 
85
+ `enableApiCoverage(expressAppRef) ` <br />
86
+ enable api coverage for express apps to know which apis and http verbs are covered in contract tests and which not
87
+
85
88
  ### Kafka APIs
86
89
 
87
90
  `startKafkaStub(port?: number, args?: (string | number)[]): Promise<KafkaStub>` <br />
@@ -25,7 +25,11 @@ var callKafka = (args, done, onOutput) => {
25
25
  exports.callKafka = callKafka;
26
26
  function callJar(jarPath, args, done, onOutput) {
27
27
  var _javaProcess$stdout, _javaProcess$stderr;
28
- var javaProcess = (0, _execSh.default)("java -jar ".concat(jarPath, " ").concat(args), {
28
+ var java = 'java';
29
+ if (process.env['endpointsAPI']) {
30
+ java = "".concat(java, " -DendpointsAPI=\"").concat(process.env['endpointsAPI'], "\"");
31
+ }
32
+ var javaProcess = (0, _execSh.default)("".concat(java, " -jar ").concat(jarPath, " ").concat(args), {
29
33
  stdio: 'pipe',
30
34
  stderr: 'pipe'
31
35
  }, done);
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.test = exports.stopStub = exports.startStub = exports.showTestResults = exports.setExpectations = exports.printJarVersion = exports.Stub = void 0;
6
+ exports.test = exports.stopStub = exports.startStub = exports.showTestResults = exports.setExpectations = exports.printJarVersion = exports.enableApiCoverage = exports.Stub = void 0;
7
7
  var _nodeFetch = _interopRequireDefault(require("node-fetch"));
8
8
  var _path = _interopRequireDefault(require("path"));
9
9
  var _fastXmlParser = require("fast-xml-parser");
@@ -11,12 +11,14 @@ var _fs = _interopRequireDefault(require("fs"));
11
11
  var _logger = _interopRequireDefault(require("../common/logger"));
12
12
  var _runner = require("../common/runner");
13
13
  var _promise = _interopRequireDefault(require("terminate/promise"));
14
+ var _expressListEndpoints = _interopRequireDefault(require("express-list-endpoints"));
14
15
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
15
16
  function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
16
17
  function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
17
18
  function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
18
19
  function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
19
20
  function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
21
+ var END_POINTS_API_ROUTE = '_specmatic/endpoints';
20
22
  class Stub {
21
23
  constructor(host, port, url, process) {
22
24
  _defineProperty(this, "host", void 0);
@@ -83,6 +85,10 @@ var stopStub = /*#__PURE__*/function () {
83
85
  }();
84
86
  exports.stopStub = stopStub;
85
87
  var test = (host, port, contractPath, args) => {
88
+ if (process.env['endpointsAPI']) {
89
+ var apiEndPointUrl = host && port ? "http://".concat(host, ":").concat(port, "/").concat(END_POINTS_API_ROUTE) : "http://localhost:8080/".concat(END_POINTS_API_ROUTE);
90
+ process.env['endpointsAPI'] = apiEndPointUrl;
91
+ }
86
92
  var specsPath = _path.default.resolve(contractPath + '');
87
93
  var cmd = "test";
88
94
  if (contractPath) cmd += " ".concat(specsPath);
@@ -164,6 +170,21 @@ var printJarVersion = () => {
164
170
  });
165
171
  };
166
172
  exports.printJarVersion = printJarVersion;
173
+ var listEndPoints = expressApp => {
174
+ var details = (0, _expressListEndpoints.default)(expressApp);
175
+ var endPoints = {};
176
+ details.map(apiDetail => {
177
+ endPoints[apiDetail.path] = apiDetail.methods;
178
+ });
179
+ delete endPoints["/".concat(END_POINTS_API_ROUTE)];
180
+ return endPoints;
181
+ };
182
+ var enableApiCoverage = expressApp => {
183
+ process.env['endpointsAPI'] = END_POINTS_API_ROUTE;
184
+ addEndPointsRoute(expressApp);
185
+ _logger.default.info("Endpoints API registered at ".concat(END_POINTS_API_ROUTE));
186
+ };
187
+ exports.enableApiCoverage = enableApiCoverage;
167
188
  var parseJunitXML = () => {
168
189
  var reportPath = _path.default.resolve('dist/test-report/TEST-junit-jupiter.xml');
169
190
  var data = _fs.default.readFileSync(reportPath);
@@ -171,4 +192,31 @@ var parseJunitXML = () => {
171
192
  var resultXml = parser.parse(data);
172
193
  resultXml.testsuite.testcase = Array.isArray(resultXml.testsuite.testcase) ? resultXml.testsuite.testcase : [resultXml.testsuite.testcase];
173
194
  return resultXml.testsuite.testcase;
195
+ };
196
+ var addEndPointsRoute = expressApp => {
197
+ expressApp.get("/".concat(END_POINTS_API_ROUTE), (_req, res) => {
198
+ var endPoints = listEndPoints(expressApp);
199
+ var springActuatorPayload = {
200
+ contexts: {
201
+ application: {
202
+ mappings: {
203
+ dispatcherServlets: {
204
+ dispatcherServlet: []
205
+ }
206
+ }
207
+ }
208
+ }
209
+ };
210
+ Object.keys(endPoints).sort().map(path => {
211
+ springActuatorPayload.contexts.application.mappings.dispatcherServlets.dispatcherServlet.push({
212
+ details: {
213
+ requestMappingConditions: {
214
+ methods: endPoints[path].sort(),
215
+ patterns: [path]
216
+ }
217
+ }
218
+ });
219
+ });
220
+ res.send(springActuatorPayload);
221
+ });
174
222
  };
package/dist/index.js CHANGED
@@ -3,6 +3,12 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ Object.defineProperty(exports, "enableApiCoverage", {
7
+ enumerable: true,
8
+ get: function get() {
9
+ return _core.enableApiCoverage;
10
+ }
11
+ });
6
12
  Object.defineProperty(exports, "printJarVersion", {
7
13
  enumerable: true,
8
14
  get: function get() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specmatic",
3
- "version": "0.70.8",
3
+ "version": "0.72.1",
4
4
  "description": "Node wrapper for Specmatic",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -35,25 +35,31 @@
35
35
  "logLevel": "trace"
36
36
  },
37
37
  "dependencies": {
38
- "@babel/cli": "^7.21.5",
39
- "@babel/core": "^7.21.8",
40
- "@babel/preset-env": "^7.21.5",
41
- "@babel/preset-typescript": "^7.21.5",
42
- "@types/node": "^20.2.3",
38
+ "@babel/cli": "^7.22.6",
39
+ "@babel/core": "^7.22.6",
40
+ "@babel/preset-env": "^7.22.6",
41
+ "@babel/preset-typescript": "^7.22.5",
42
+ "@types/node": "^20.3.3",
43
43
  "@types/node-fetch": "^2.6.4",
44
44
  "@types/yargs": "^17.0.24",
45
45
  "exec-sh": "^0.4.0",
46
- "fast-xml-parser": "^4.2.2",
46
+ "express-list-endpoints": "^6.0.0",
47
+ "fast-xml-parser": "^4.2.5",
47
48
  "node-fetch": "^2.6.11",
48
49
  "rimraf": "^5.0.1",
49
50
  "terminate": "^2.6.1",
50
- "winston": "^3.8.2",
51
+ "winston": "^3.9.0",
51
52
  "yargs": "^17.7.2"
52
53
  },
53
54
  "devDependencies": {
55
+ "@types/express": "^4.17.17",
56
+ "@types/express-list-endpoints": "^6.0.0",
54
57
  "@types/jest": "^29.5.2",
55
- "jest": "^29.5.0",
58
+ "@types/supertest": "^2.0.12",
59
+ "express": "^4.18.2",
60
+ "jest": "^29.6.0",
56
61
  "jest-extended": "^4.0.0",
57
- "jest-mock-extended": "^3.0.4"
62
+ "jest-mock-extended": "^3.0.4",
63
+ "supertest": "^6.3.3"
58
64
  }
59
65
  }
package/specmatic.jar CHANGED
Binary file
@@ -19,7 +19,11 @@ const callKafka = (args: string, done: (error: any) => void, onOutput: (message:
19
19
  };
20
20
 
21
21
  function callJar(jarPath: string, args: string, done: (error: any) => void, onOutput: (message: string, error: boolean) => void) {
22
- const javaProcess = execSh(`java -jar ${jarPath} ${args}`, { stdio: 'pipe', stderr: 'pipe' }, done);
22
+ let java = 'java';
23
+ if (process.env['endpointsAPI']) {
24
+ java = `${java} -DendpointsAPI="${process.env['endpointsAPI']}"`;
25
+ }
26
+ const javaProcess = execSh(`${java} -jar ${jarPath} ${args}`, { stdio: 'pipe', stderr: 'pipe' }, done);
23
27
  javaProcess.stdout?.on('data', function (data: String) {
24
28
  onOutput(`${data}`, false);
25
29
  });
@@ -0,0 +1,83 @@
1
+ import * as specmatic from '../..';
2
+ import express from 'express';
3
+ import request from 'supertest';
4
+
5
+ test('adds an environment variable indicating api endpoits route is configured', async () => {
6
+ var app = express();
7
+ app.get('/', () => {});
8
+
9
+ specmatic.enableApiCoverage(app);
10
+
11
+ expect(process.env.endpointsAPI).toBe('_specmatic/endpoints');
12
+ });
13
+
14
+ test('gives list of end points for a single route defined on express app', async () => {
15
+ var app = express();
16
+ app.get('/', () => {});
17
+
18
+ specmatic.enableApiCoverage(app);
19
+
20
+ const res = await request(app).get('/_specmatic/endpoints').accept('application/json').expect(200);
21
+ const response = generateResponseObject({ '/': ['GET'] });
22
+ expect(res.body).toStrictEqual(response);
23
+ });
24
+
25
+ test('gives list of end points for multiple routes defined on express app', async () => {
26
+ var app = express();
27
+ app.get('/', () => {});
28
+ app.post('/', () => {});
29
+ app.get('/ping', () => {});
30
+
31
+ specmatic.enableApiCoverage(app);
32
+
33
+ const res = await request(app).get('/_specmatic/endpoints').accept('application/json').expect(200);
34
+ const response = generateResponseObject({ '/': ['GET', 'POST'], '/ping': ['GET'] });
35
+ expect(res.body).toStrictEqual(response);
36
+ });
37
+
38
+ test('gives list of end points for a multiple routes defined on multiple routers', async () => {
39
+ var app = express();
40
+ const userRouter = express.Router();
41
+ userRouter.get('/', function (req, res) {});
42
+ userRouter.post('/', function (req, res) {});
43
+ userRouter.delete('/', function (req, res) {});
44
+ userRouter.put('/', function (req, res) {});
45
+
46
+ const productRouter = express.Router();
47
+ productRouter.get('/', function (req, res) {});
48
+ productRouter.post('/', function (req, res) {});
49
+
50
+ app.use('/user', userRouter);
51
+ app.use('/product', productRouter);
52
+
53
+ specmatic.enableApiCoverage(app);
54
+
55
+ const res = await request(app).get('/_specmatic/endpoints').accept('application/json').expect(200);
56
+ const response = generateResponseObject({ '/product': ['GET', 'POST'], '/user': ['DELETE', 'GET', 'POST', 'PUT'] });
57
+ expect(res.body).toStrictEqual(response);
58
+ });
59
+
60
+ function generateResponseObject(endPoints: { [key: string]: string[] }) {
61
+ const structure = {
62
+ contexts: {
63
+ application: {
64
+ mappings: {
65
+ dispatcherServlets: {
66
+ dispatcherServlet: [],
67
+ },
68
+ },
69
+ },
70
+ },
71
+ };
72
+ Object.keys(endPoints).map(key => {
73
+ structure.contexts.application.mappings.dispatcherServlets.dispatcherServlet.push({
74
+ details: {
75
+ requestMappingConditions: {
76
+ methods: endPoints[key],
77
+ patterns: [key],
78
+ },
79
+ },
80
+ } as never);
81
+ });
82
+ return structure;
83
+ }
@@ -163,6 +163,34 @@ test('invocation makes sure previous junit report if any is deleted', async func
163
163
  expect(spy).toHaveBeenCalledWith(path.resolve('dist/test-report'), { force: true, recursive: true });
164
164
  });
165
165
 
166
+ test('passes an property indicating api endpoint based on host and port supplied when api coverage is enabled', async () => {
167
+ process.env['endpointsAPI'] = '/_specmatic/endpoints';
168
+ execSh.mockReturnValue(javaProcessMock);
169
+ setTimeout(() => {
170
+ copyReportFile();
171
+ execSh.mock.calls[0][2]();
172
+ }, 0);
173
+
174
+ await expect(specmatic.test(HOST, PORT)).resolves.toBeTruthy();
175
+
176
+ expect(execSh).toHaveBeenCalledTimes(1);
177
+ expect(execSh.mock.calls[0][0]).toBe(`java -DendpointsAPI=\"http://${HOST}:${PORT}/_specmatic/endpoints\" -jar ${path.resolve(SPECMATIC_JAR_PATH)} test --junitReportDir=dist/test-report --host=localhost --port=8000`);
178
+ });
179
+
180
+ test('passes an property indicating api endpoint based on defaults if host and port not provided when api coverage is enabled', async () => {
181
+ process.env['endpointsAPI'] = '/_specmatic/endpoints';
182
+ execSh.mockReturnValue(javaProcessMock);
183
+ setTimeout(() => {
184
+ copyReportFile();
185
+ execSh.mock.calls[0][2]();
186
+ }, 0);
187
+
188
+ await expect(specmatic.test(HOST, PORT)).resolves.toBeTruthy();
189
+
190
+ expect(execSh).toHaveBeenCalledTimes(1);
191
+ expect(execSh.mock.calls[0][0]).toBe(`java -DendpointsAPI=\"http://${HOST}:${PORT}/_specmatic/endpoints\" -jar ${path.resolve(SPECMATIC_JAR_PATH)} test --junitReportDir=dist/test-report --host=localhost --port=8000`);
192
+ });
193
+
166
194
  function copyReportFile() {
167
195
  copyReportFileWithName('sample-junit-result-multiple.xml');
168
196
  }
package/src/core/index.ts CHANGED
@@ -6,6 +6,9 @@ import fs from 'fs';
6
6
  import logger from '../common/logger';
7
7
  import { callSpecmatic } from '../common/runner';
8
8
  import terminate from 'terminate/promise';
9
+ import listExpressEndpoints from 'express-list-endpoints';
10
+
11
+ const END_POINTS_API_ROUTE = '_specmatic/endpoints';
9
12
 
10
13
  export class Stub {
11
14
  host: string;
@@ -74,6 +77,11 @@ const stopStub = async (stub: Stub) => {
74
77
  };
75
78
 
76
79
  const test = (host?: string, port?: number, contractPath?: string, args?: (string | number)[]): Promise<{ [k: string]: number } | undefined> => {
80
+ if (process.env['endpointsAPI']) {
81
+ const apiEndPointUrl = host && port ? `http://${host}:${port}/${END_POINTS_API_ROUTE}` : `http://localhost:8080/${END_POINTS_API_ROUTE}`;
82
+ process.env['endpointsAPI'] = apiEndPointUrl;
83
+ }
84
+
77
85
  const specsPath = path.resolve(contractPath + '');
78
86
 
79
87
  var cmd = `test`;
@@ -169,6 +177,22 @@ const printJarVersion = () => {
169
177
  );
170
178
  };
171
179
 
180
+ const listEndPoints = (expressApp: any): { [key: string]: string[] } => {
181
+ const details = listExpressEndpoints(expressApp);
182
+ let endPoints: { [key: string]: string[] } = {};
183
+ details.map(apiDetail => {
184
+ endPoints[apiDetail.path] = apiDetail.methods;
185
+ });
186
+ delete endPoints[`/${END_POINTS_API_ROUTE}`];
187
+ return endPoints;
188
+ };
189
+
190
+ const enableApiCoverage = (expressApp: any) => {
191
+ process.env['endpointsAPI'] = END_POINTS_API_ROUTE;
192
+ addEndPointsRoute(expressApp);
193
+ logger.info(`Endpoints API registered at ${END_POINTS_API_ROUTE}`);
194
+ };
195
+
172
196
  const parseJunitXML = () => {
173
197
  const reportPath = path.resolve('dist/test-report/TEST-junit-jupiter.xml');
174
198
  var data = fs.readFileSync(reportPath);
@@ -178,4 +202,32 @@ const parseJunitXML = () => {
178
202
  return resultXml.testsuite.testcase;
179
203
  };
180
204
 
181
- export { startStub, stopStub, test, setExpectations, printJarVersion, showTestResults };
205
+ const addEndPointsRoute = (expressApp: any) => {
206
+ expressApp.get(`/${END_POINTS_API_ROUTE}`, (_req: any, res: any) => {
207
+ let endPoints = listEndPoints(expressApp);
208
+ let springActuatorPayload = {
209
+ contexts: {
210
+ application: {
211
+ mappings: {
212
+ dispatcherServlets: {
213
+ dispatcherServlet: [],
214
+ },
215
+ },
216
+ },
217
+ },
218
+ };
219
+ Object.keys(endPoints).sort().map(path => {
220
+ springActuatorPayload.contexts.application.mappings.dispatcherServlets.dispatcherServlet.push({
221
+ details: {
222
+ requestMappingConditions: {
223
+ methods: endPoints[path].sort(),
224
+ patterns: [path],
225
+ },
226
+ },
227
+ } as never);
228
+ });
229
+ res.send(springActuatorPayload);
230
+ });
231
+ }
232
+
233
+ export { startStub, stopStub, test, setExpectations, printJarVersion, showTestResults, enableApiCoverage };
package/src/index.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { startStub, stopStub, test, setExpectations, printJarVersion, showTestResults } from './core';
1
+ export { startStub, stopStub, test, setExpectations, printJarVersion, showTestResults, enableApiCoverage } from './core';
2
2
  export { startKafkaStub, stopKafkaStub, verifyKafkaStubMessage, verifyKafkaStub, setKafkaStubExpectations } from './kafka';