Skip to content

Commit

Permalink
Merge pull request #1 from Athaphian/mocks-generation
Browse files Browse the repository at this point in the history
Mocks generation
  • Loading branch information
Athaphian committed Jun 5, 2018
2 parents 286e815 + eaa9f62 commit 4a18d17
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 56 deletions.
59 changes: 7 additions & 52 deletions modules/jsonFetch.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
const fetch = require('node-fetch'),
fs = require('fs'),
log = require('./logging');
jsonFetchMocks = require('./jsonFetchMocks');

module.exports = (function() {
const DEFAULT_TIMEOUT = 20000;
let useMocks = false,
mocks;

const handleHttpErrors = response => {
if (response.status !== 200) {
Expand All @@ -15,68 +12,26 @@ module.exports = (function() {
}
};

const mocksFile = process.argv.filter(arg => arg.startsWith('mocks=')).map(arg => arg.substr(6))[0];
if (mocksFile) {

// Force logging on, since this might be important, mocks are only used during development anyway
const loggingWasEnabled = log.isEnabled();
log.setEnabled(true);

try {
mocks = JSON.parse(fs.readFileSync(mocksFile, 'utf8'));
useMocks = true;
log.info('[ ] Loaded mocks file "' + mocksFile + '", MOCKS ARE ENABLED.');
} catch (e) {
log.error('[ ] Mocks file "' + mocksFile + '" not found, MOCKS ARE DISABLED.');
}

// Set logging to original state
log.setEnabled(loggingWasEnabled);
}

function getJson(url, headers = {}) {
if (useMocks) {
const mockResponseFile = mocks[url];
if (mockResponseFile) {
if (mockResponseFile === 'ENOTFOUND') {
return new Promise(function(resolve, reject) {
reject({
message: 'request failed because of mock ENOTFOUND',
code: 'ENOTFOUND',
name: 'FetchError'
})
});
}

try {
const mockResponse = JSON.parse(fs.readFileSync(mockResponseFile, 'utf8'));
log.info('[ ] Mock "' + mockResponseFile + '" used to simulate response.');
return new Promise(function(resolve) {
resolve(mockResponse);
});
} catch (e) {
log.error('[ ] Mock "' + mockResponseFile + '" not found. Check your mocks file!');
}
}
}

return fetch(url, {
return jsonFetchMocks.getMock(url, 'GET') || fetch(url, {
'headers': headers,
'timeout': DEFAULT_TIMEOUT
})
.then(handleHttpErrors)
.then(response => response.json());
.then(response => response.json())
.then(jsonFetchMocks.generateMocks(url, 'GET'));
}

function postJson(url, body, headers = {}) {
return fetch(url, {
return jsonFetchMocks.getMock(url, 'POST') || fetch(url, {
'headers': headers,
'timeout': DEFAULT_TIMEOUT,
'body': body,
'method': 'POST'
})
.then(handleHttpErrors)
.then((response) => response.json());
.then((response) => response.json())
.then(jsonFetchMocks.generateMocks(url, 'POST'));
}

return {
Expand Down
121 changes: 121 additions & 0 deletions modules/jsonFetchMocks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
const fs = require('fs'),
log = require('./logging'),
path = require('path');

module.exports = (function() {
let useMocks = false,
mocks,
generateMocks = false,
mocksDirName;

const mocksFile = process.argv.filter(arg => arg.startsWith('mocks=')).map(arg => arg.substr(6))[0];
const generateMocksFile = process.argv.filter(arg => arg.startsWith('generate-mocks=')).map(arg => arg.substr(15))[0];
const reportUnmockedCalls = process.argv.filter(arg => arg.startsWith('report-unmocked=')).map(arg => arg.substr(16)).map(arg => arg === 'true')[0];

const writeObjectToFile = (file, contents) => {
fs.writeFile(file, JSON.stringify(contents), function(err) {
if (err) throw err;
});
};

const saveMocks = () => {
writeObjectToFile(generateMocksFile, mocks);
};

if (mocksFile) {
// Force logging on, since this might be important, mocks are only used during development anyway
const loggingWasEnabled = log.isEnabled();
log.setEnabled(true);

try {
mocks = JSON.parse(fs.readFileSync(mocksFile, 'utf8'));
useMocks = true;
log.info('[ ] Loaded mocks file "' + mocksFile + '", MOCKS ARE ENABLED.');
} catch (e) {
log.error('[ ] Mocks file "' + mocksFile + '" not found, MOCKS ARE DISABLED.');
}

// Set logging to original state
log.setEnabled(loggingWasEnabled);
} else if (generateMocksFile) {
// Force logging on, since this might be important, mocks are only used during development anyway
const loggingWasEnabled = log.isEnabled();
log.setEnabled(true);

try {
if (fs.existsSync(generateMocksFile)) {
mocks = JSON.parse(fs.readFileSync(generateMocksFile, 'utf8'));
log.info('[ ] Mock generation enabled to file "' + generateMocksFile + '", WARNING, EXISTING FILE WILL BE ALTERED.');
} else {
mocks = {};
saveMocks();
log.info('[ ] Mock generation enabled to file "' + generateMocksFile + '".');
}
useMocks = true;
mocksDirName = path.dirname(generateMocksFile);
generateMocks = true;
} catch (e) {
log.error('[ ] Error while accessing mock generation file "' + generateMocksFile + '" MOCK GENERATION IS DISABLED.');
generateMocks = false;
}

// Set logging to original state
log.setEnabled(loggingWasEnabled);
}

const generateMocksFunction = (url, method) => response => {
if (generateMocks) {
try {
const existingMock = mocks[method + '|' + url];
const mockFileName = `${mocksDirName}/${url.replace(/http:\/\/|https:\/\//g, '').replace(/\.|\/|\?/g, '_')}.json`;
if (existingMock) {
log.info('[ ] Mock already exists:', url, mockFileName);
} else {
log.info('[X] Recording mock:', url, mockFileName);
mocks[method + '|' + url] = mockFileName;
writeObjectToFile(mockFileName, response);
saveMocks();
}
} catch (err) {
log.error('[ ] Error while writing mock file,', err);
}
}
return response;
};

const getMock = (url, method) => {
if (useMocks) {
const mockResponseFile = mocks[method + '|'+ url];
if (mockResponseFile) {
if (mockResponseFile === 'ENOTFOUND') {
return new Promise(function(resolve, reject) {
reject({
message: 'request failed because of mock ENOTFOUND',
code: 'ENOTFOUND',
name: 'FetchError'
})
});
}

try {
const mockResponse = JSON.parse(fs.readFileSync(mockResponseFile, 'utf8'));
log.info('[ ] Mock "' + mockResponseFile + '" used to simulate response.');
return new Promise(function(resolve) {
resolve(mockResponse);
});
} catch (e) {
log.error('[ ] Mock "' + mockResponseFile + '" not found. Check your mocks file!');
}
}

if (reportUnmockedCalls) {
log.info('[X] NO MOCK FOUND for url:', url);
}
}
};

return {
'getMock': getMock,
'generateMocks': generateMocksFunction
};
}());
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "express-api-tools",
"version": "1.0.5",
"version": "1.0.6",
"description": "Pragmatic tools to build JSON api endpoints.",
"main": "index.js",
"author": {
Expand Down Expand Up @@ -32,4 +32,4 @@
"api",
"tools"
]
}
}
14 changes: 12 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ jsonFetch.getJson('https://www.google.com', headers).then(body => {

> A simple post variant is also available.
> Mocks can be enabled by starting the server using a command line argument mocks=./someMocksFile.json. The mocks file
#### Mocks
Mocks can be enabled by starting the server using a command line argument: ```mocks=./someMocksFile.json```. The mocks file
contains a simple json object with key/values. The key is the exact url that is fetched by jsonFetch, the value is
a reference to another json file which contains the response that should be returned. If the value is 'ENOTFOUND',
a not found error will be simulated.
Expand All @@ -134,9 +135,18 @@ a not found error will be simulated.
"https://www.google.com": "./mocks/google_mock.json",
"https://www.bing.com": "ENOTFOUND"
}

```

It is possible to enable logging of all calls that are not mocked while mocks are enabled. This can be used to trace
unmocked calls, if needed. This can be done by adding the command line argument: ```report-unmocked=true```.

#### Mock recording
Mocks can also automatically be recorded (to take a snapshot of a specific moment). Add the following command line argument:
```generate-mocks=./some/path/someMocksFile.json```. The mocks file will be appended if it already exists. Otherwise,
it will be created and all recorded mocks will be automatically added. The actual responses will be recorded in separate
json files that will be saved in the same directory as the specified mocks file. If a response is recorded once, subsequent
calls to the same endpoint will return the mocked response.

## Api endpoints
This package also contains a few example api endpoints that can be used in production.

Expand Down

0 comments on commit 4a18d17

Please sign in to comment.