Comparing version 1.4.0 to 2.0.0
@@ -0,1 +1,8 @@ | ||
# 2.0.0 | ||
- Add support for single file mock collections | ||
- Add mock collection conversion utility | ||
## Breaking changes | ||
- Change mock set prefix to `__` | ||
# 1.4.0 | ||
@@ -2,0 +9,0 @@ - Add support for query params matching |
111
lib/mock.js
const path = require('path'); | ||
const fs = require('fs-extra'); | ||
const globby = require('globby'); | ||
const pathToRegexp = require('path-to-regexp'); | ||
const importFresh = require('import-fresh'); | ||
const methodRegExp = /([a-zA-Z+]+?){1}_.+/i; | ||
const paramsRegExp = /\$([^.\s]+)/; | ||
const paramsRegExp = /[$?]([^.\s]+)/; | ||
const setRegExp = /__([\w-]+?)$/; | ||
const defaultType = 'application/octet-stream'; | ||
function getMock(basePath, file) { | ||
class MockContentError extends Error { | ||
constructor(message, error) { | ||
super(message); | ||
this.innerError = error; | ||
} | ||
toString() { | ||
return `${this.message}: ${this.innerError.message}`; | ||
} | ||
} | ||
function getMock(basePath, file, data = undefined) { | ||
let ext = path.extname(file); | ||
const isTemplate = ext.endsWith('_'); | ||
let basename = path.basename(file, ext); | ||
const set = path.extname(basename); | ||
ext = ext ? ext.substring(1, ext.length - (isTemplate ? 1 : 0)).toLowerCase() : ext; | ||
if (set) { | ||
basename = path.basename(basename, set); | ||
if (data !== undefined) { | ||
ext = typeof data === 'function' ? 'js' : ext !== null && typeof data === 'object' ? 'json' : ext; | ||
} | ||
let set = null; | ||
const matchSet = basename.match(setRegExp); | ||
if (matchSet) { | ||
set = matchSet[1]; | ||
basename = path.basename(basename, matchSet[0]); | ||
} | ||
let params = null; | ||
@@ -60,6 +81,7 @@ const matchParams = basename.match(paramsRegExp); | ||
return { | ||
originalFile: file, | ||
file: path.join(basePath, file), | ||
ext, | ||
type: ext || defaultType, | ||
set: set ? set.substring(1) : set, | ||
set, | ||
isTemplate, | ||
@@ -70,3 +92,4 @@ methods, | ||
keys, | ||
params | ||
params, | ||
data | ||
}; | ||
@@ -76,23 +99,77 @@ } | ||
async function getMocks(basePath, ignoreGlobs, globs = ['**/*']) { | ||
if (!basePath) { | ||
globs.push('!node_modules'); | ||
} | ||
// Ensure relative paths for ignore globs | ||
ignoreGlobs = ignoreGlobs.map(glob => `!${path.isAbsolute(glob) ? path.relative(basePath, glob) : glob}`); | ||
let mockFiles = await globby(globs.concat(ignoreGlobs), {cwd: basePath}); | ||
const singleFileMocks = []; | ||
let mockFiles = await getMockFiles(basePath, ignoreGlobs, globs); | ||
const mockCollectionFiles = []; | ||
mockFiles = mockFiles.filter(mock => { | ||
if (mock.endsWith('.mocks.js')) { | ||
singleFileMocks.push(mock); | ||
mockCollectionFiles.push(mock); | ||
return false; | ||
} | ||
return true; | ||
}); | ||
return mockFiles.map(file => getMock(basePath, file)); | ||
return mockFiles.map(file => getMock(basePath, file)).concat(getMocksFromCollections(basePath, mockCollectionFiles)); | ||
} | ||
function getMockFiles(basePath, ignoreGlobs, globs) { | ||
if (!basePath) { | ||
globs.push('!node_modules'); | ||
} | ||
// Ensure relative paths for ignore globs | ||
ignoreGlobs = ignoreGlobs.map(glob => `!${path.isAbsolute(glob) ? path.relative(basePath, glob) : glob}`); | ||
return globby(globs.concat(ignoreGlobs), {cwd: basePath}); | ||
} | ||
function getMocksFromCollections(basePath, mockCollectionFiles) { | ||
return mockCollectionFiles.reduce((mocks, file) => { | ||
try { | ||
basePath = path.isAbsolute(basePath) ? basePath : path.join(process.cwd(), basePath); | ||
const collection = importFresh(path.join(basePath, file)); | ||
const newMocks = Object.entries(collection).map(([route, data]) => getMock(basePath, route, data)); | ||
return mocks.concat(newMocks); | ||
} catch (error) { | ||
console.error(`Error while loading collection "${file}"`, error); | ||
return mocks; | ||
} | ||
}, []); | ||
} | ||
async function getMockContent(mock) { | ||
let content; | ||
if (mock.data !== undefined) { | ||
content = mock.data; | ||
} else if (mock.isTemplate || mock.ext === 'json') { | ||
try { | ||
content = await fs.readFile(mock.file, 'utf-8'); | ||
} catch (error) { | ||
throw new MockContentError(`Error while reading mock file "${mock.file}"`, error); | ||
} | ||
} else if (mock.ext === 'js') { | ||
try { | ||
const filePath = path.isAbsolute(mock.file) ? mock.file : path.join(process.cwd(), mock.file); | ||
content = importFresh(filePath); | ||
} catch (error) { | ||
throw new MockContentError(`Error while evaluating JS for mock "${mock.file}"`); | ||
} | ||
} else { | ||
try { | ||
// Read file as buffer | ||
content = await fs.readFile(mock.file); | ||
} catch (error) { | ||
throw new MockContentError(`Error while reading mock file "${mock.file}"`); | ||
} | ||
} | ||
return content; | ||
} | ||
module.exports = { | ||
MockContentError, | ||
getMock, | ||
getMocks, | ||
getMock | ||
getMockFiles, | ||
getMocksFromCollections, | ||
getMockContent | ||
}; |
@@ -31,3 +31,3 @@ const path = require('path'); | ||
if (options.set) { | ||
file += '.' + options.set; | ||
file += '__' + options.set; | ||
} | ||
@@ -73,3 +73,4 @@ | ||
module.exports = { | ||
isStringContent, | ||
record | ||
}; |
@@ -1,6 +0,3 @@ | ||
const path = require('path'); | ||
const fs = require('fs-extra'); | ||
const importFresh = require('import-fresh'); | ||
const {render} = require('./template'); | ||
const {MockContentError, getMockContent} = require('./mock'); | ||
@@ -15,3 +12,3 @@ function getResponseDetails(response, statusCode) { | ||
if (typeof response === 'object' && hasProperty('statusCode') && hasProperty('body')) { | ||
if (response !== null && typeof response === 'object' && hasProperty('statusCode') && hasProperty('body')) { | ||
details.statusCode = response.statusCode || details.statusCode; | ||
@@ -36,4 +33,4 @@ details.headers = response.headers || details.headers; | ||
async function respondMock(res, mock, data, statusCode = null) { | ||
let result; | ||
async function getMockResponse(mock, data) { | ||
let response = await getMockContent(mock); | ||
@@ -46,41 +43,36 @@ // Response depends of input file type: | ||
if (mock.isTemplate || mock.ext === 'json') { | ||
if (mock.isTemplate) { | ||
try { | ||
result = await fs.readFile(mock.file, 'utf-8'); | ||
response = render(response, data); | ||
} catch (error) { | ||
return internalError(res, `Error while reading mock file "${mock.file}"`, error); | ||
throw new MockContentError(`Error while processing template for mock file "${mock.file}"`, error); | ||
} | ||
} | ||
if (mock.isTemplate) { | ||
try { | ||
result = render(result, data); | ||
} catch (error) { | ||
return internalError(res, `Error while processing template for mock file "${mock.file}"`, error); | ||
} | ||
} | ||
if (mock.ext === 'json') { | ||
try { | ||
result = result ? JSON.parse(result) : undefined; | ||
} catch (error) { | ||
return internalError(res, `Error while parsing JSON for mock "${mock.file}"`, error); | ||
} | ||
} | ||
} else if (mock.ext === 'js') { | ||
if (mock.ext === 'json' && typeof response === 'string') { | ||
try { | ||
const filePath = path.isAbsolute(mock.file) ? mock.file : path.join(process.cwd(), mock.file); | ||
result = importFresh(filePath)(data); | ||
response = response ? JSON.parse(response) : undefined; | ||
} catch (error) { | ||
return internalError(res, `Error while evaluating JS for mock "${mock.file}"`, error); | ||
throw new MockContentError(`Error while parsing JSON for mock "${mock.file}"`, error); | ||
} | ||
} else { | ||
} else if (mock.ext === 'js') { | ||
try { | ||
// Read file as buffer | ||
result = await fs.readFile(mock.file); | ||
response = response(data); | ||
} catch (error) { | ||
return internalError(res, `Error while reading mock file "${mock.file}"`, error); | ||
throw new MockContentError(`Error while evaluating JS for mock "${mock.file}"`); | ||
} | ||
} | ||
const details = getResponseDetails(result, statusCode); | ||
return response; | ||
} | ||
async function respondMock(res, mock, data, statusCode = null) { | ||
let response; | ||
try { | ||
response = await getMockResponse(mock, data); | ||
} catch (error) { | ||
return internalError(res, error.message, error.innerError); | ||
} | ||
const details = getResponseDetails(response, statusCode); | ||
const needType = | ||
@@ -100,3 +92,4 @@ Object.getOwnPropertyNames(details.headers) | ||
module.exports = { | ||
getMockResponse, | ||
respondMock | ||
}; |
{ | ||
"name": "smoke", | ||
"version": "1.4.0", | ||
"version": "2.0.0", | ||
"description": "Simple yet powerful file-based mock server with recording abilities", | ||
"main": "smoke.js", | ||
"bin": { | ||
"smoke": "./bin/smoke" | ||
"smoke": "./bin/smoke", | ||
"smoke-conv": "./bin/smoke-conv" | ||
}, | ||
@@ -31,5 +32,5 @@ "scripts": { | ||
"express": "^4.16.4", | ||
"express-http-proxy": "^1.5.0", | ||
"express-http-proxy": "^1.5.1", | ||
"fs-extra": "^7.0.1", | ||
"globby": "^8.0.2", | ||
"globby": "^9.0.0", | ||
"import-fresh": "^3.0.0", | ||
@@ -41,8 +42,8 @@ "lodash.template": "^4.4.0", | ||
"multer": "^1.4.1", | ||
"path-to-regexp": "^2.4.0" | ||
"path-to-regexp": "^3.0.0" | ||
}, | ||
"devDependencies": { | ||
"jest": "^23.6.0", | ||
"supertest": "^3.3.0", | ||
"xo": "^0.23.0" | ||
"supertest": "^3.4.1", | ||
"xo": "^0.24.0" | ||
}, | ||
@@ -60,2 +61,10 @@ "xo": { | ||
}, | ||
"jest": { | ||
"collectCoverageFrom": [ | ||
"*.js", | ||
"lib/**/*.js" | ||
], | ||
"silent": true, | ||
"verbose": true | ||
}, | ||
"engines": { | ||
@@ -62,0 +71,0 @@ "node": ">=8.0.0" |
@@ -74,3 +74,3 @@ # :dash: smoke | ||
**General format:** `methods_api#route#:routeParam$queryParam=value=.set.extension` | ||
**General format:** `methods_api#route#:routeParam$queryParam=value.__et.extension` | ||
@@ -120,5 +120,5 @@ The path and file name of the mock is used to determinate: | ||
#### Mock set | ||
You can optionally specify a mock set before the file extension by using a `.set-name` suffix after the file name. | ||
You can optionally specify a mock set before the file extension by using a `__set-name` suffix after the file name. | ||
For example `get_api#hello.error.json` will only be used if you start the server with the `error` set enabled: | ||
For example `get_api#hello__error.json` will only be used if you start the server with the `error` set enabled: | ||
`smoke --set error`. | ||
@@ -301,2 +301,34 @@ | ||
### Single file mock collection | ||
You can regroup multiple mocks in a special single file with the extension `.mocks.js`, using this format: | ||
```js | ||
module.exports = { | ||
'<file_name>': '<file_content>' // can be a string, an object (custom response) or a function (JavaScript mock) | ||
}; | ||
``` | ||
See this [example mock collection](test/mocks/collection.mocks.js) to get an idea of all possibilities. | ||
The format of file name is the same as for individual mock files, and will be used to match the request using the same | ||
rules. As for the mock content, the format is also the same as what you would put in single file mock. If a request | ||
matches both a mock file and a mock within a collection with the same specificity, the mock file will always be used | ||
over the collection. | ||
As the format is the same, you can convert a bunch of files to a single file mock collection and conversely. | ||
To convert separate mock files to a collection: | ||
```sh | ||
smoke-conv <glob> <output_file> // Will create <output_file>.mocks.js from all mocks found | ||
``` | ||
To convert a mock collection to separate files: | ||
```sh | ||
smoke-conv <file> <output_folder> // Will extract separate mocks into <output_folder> | ||
``` | ||
Note that only text-based file content will be inserted directly, other file content will be converted to a base64 | ||
string. | ||
:warning: There is a limitation regarding JavaScript mocks: only the exported function will be converted for a given | ||
mock, meaning that if you have non-exported functions, variables or imports they will be lost during the conversion. | ||
## Other mock servers | ||
@@ -303,0 +335,0 @@ |
12
smoke.js
@@ -93,3 +93,4 @@ const path = require('path'); | ||
if (accept && matchMock(mock, method, options.set, query)) { | ||
const score = (mock.methods ? 1 : 0) + (mock.set ? 2 : 0) + (mock.params ? 4 : 0); | ||
const score = | ||
(mock.methods ? 1 : 0) + (mock.set ? 2 : 0) + (mock.params ? 4 : 0) + (mock.data === undefined ? 0.5 : 0); | ||
allMatches.push({match, mock, score}); | ||
@@ -111,2 +112,3 @@ } | ||
} | ||
return proxyResData; | ||
@@ -118,3 +120,3 @@ } | ||
// Search for 404 mocks, matching accept header | ||
const notFoundMocks = await getMocks(options.basePath, ignore, options.notFound); | ||
const notFoundMocks = await getMocks(options.basePath, ignore, [options.notFound]); | ||
const types = notFoundMocks.length > 0 ? notFoundMocks.map(mock => mock.type) : null; | ||
@@ -131,4 +133,5 @@ const accept = types && req.accepts(types); | ||
} else { | ||
const accept = req.accepts(matches.map(match => match.mock.type)); | ||
const {match, mock} = matches.filter(match => accept === match.mock.type).sort((a, b) => b.score - a.score)[0]; | ||
const sortedMatches = matches.sort((a, b) => b.score - a.score); | ||
const accept = req.accepts(sortedMatches.map(match => match.mock.type)); | ||
const {match, mock} = sortedMatches.filter(match => accept === match.mock.type)[0]; | ||
@@ -142,2 +145,3 @@ // Fill in route params | ||
} | ||
next(); | ||
@@ -144,0 +148,0 @@ }; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
340
38442
13
636
5
1
+ Added@types/glob@7.2.0(transitive)
+ Added@types/minimatch@5.1.2(transitive)
+ Added@types/node@22.9.1(transitive)
+ Addeddir-glob@2.2.2(transitive)
+ Addedglobby@9.2.0(transitive)
+ Addedignore@4.0.6(transitive)
+ Addedpath-to-regexp@3.3.0(transitive)
+ Addedpify@4.0.1(transitive)
+ Addedslash@2.0.0(transitive)
+ Addedundici-types@6.19.8(transitive)
- Removedarrify@1.0.1(transitive)
- Removeddir-glob@2.0.0(transitive)
- Removedglobby@8.0.2(transitive)
- Removedignore@3.3.10(transitive)
- Removedpath-to-regexp@2.4.0(transitive)
- Removedslash@1.0.0(transitive)
Updatedexpress-http-proxy@^1.5.1
Updatedglobby@^9.0.0
Updatedpath-to-regexp@^3.0.0