Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/presets #42

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 1 addition & 229 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,229 +1 @@
# ng-apimock [![Build Status](https://travis-ci.org/mdasberg/ng-apimock.svg?branch=master)](https://travis-ci.org/mdasberg/ng-apimock) [![npm version](https://img.shields.io/node/v/ng-apimock.svg)](https://github.com/mdasberg/ng-apimock) [![dependency Status](https://img.shields.io/david/mdasberg/ng-apimock.svg)](https://david-dm.org/mdasberg/ng-apimock) [![devDependency Status](https://img.shields.io/david/dev/mdasberg/ng-apimock.svg)](https://david-dm.org/mdasberg/gng-apimock#info=devDependencies) [![npm downloads](https://img.shields.io/npm/dm/ng-apimock.svg?style=flat-square)](https://www.npmjs.com/package/ng-apimock)

> Node plugin that provides the ability to use scenario based api mocking:
- for local development
- for protractor testing

#### Plugins that use ng-apimock
- [grunt-ng-apimock](https://mdasberg.github.io/grunt-ng-apimock) is a plugin that makes ng-apimock available for [Grunt](http://gruntjs.com/)
- [gulp-ng-apimock](https://mdasberg.github.io/gulp-ng-apimock) is a plugin that makes ng-apimock available for [Gulp](http://gulpjs.com/)

## Getting Started

```shell
npm install ng-apimock --save-dev

```

Once the plugin has been installed, you can require it with this line of JavaScript:

```js
var ngApimock = require('ng-apimock')();

```

## The "ngApimock" process mocks

### Overview
In order to use the available mocks, you need to call the run function with this line of JavaScript:

```js
ngApimock.run({
"src": "test/mocks",
"outputDir": "path/to/outputDir",
"done": function() {
// async
}
});

```

The run function will process the mock data provided in the configuration and make it accessible for connect as middleware.

In order to watch for changes, you need to call the watch function with this line of Javascript:

```js
ngApimock.watch("test/mocks");

```

The watch function will watch for changes in the mock directory and update accordingly.

### Howto write mocks
There are a couple of rules to follow.

1. For each api call create a separate file
2. Each file needs to follow the format below.

```js
{
"expression": "your expression here (ie a regex without the leading and trailing '/' or a string)",
"method": "the http method (ie GET, POST, PUT or DELETE)", // supports JSONP as well
"name": "identifiable name for this service call" // if non is provided, expression$$method will be used
"isArray": "indicates if the response data is an array or object",
"responses": {
"some-meaningful-scenario-name": {
"default": true, // if false or not provided this response will not be used as default
"status": 200, // optional - defaults to 200
"headers": {}, // optional - defaults to {}
"data": {}, // optional
"file": "path/to/file.ext" // optional, when provided don't forget the matching content-type header as it will result in a file download instead of data
"statusText": "", // optional
"delay": 2000 // optional - defaults to no delay when provided this delay will only be used for this response
},
"some-other-meaningful-scenario-name": {
"data": {}
}
}
}

```

## Howto use global variables
If for instance, you have date sensitive information in you mocks, mock data is not flexible enough.
You can use global variables for this. By surrounding a value in the response.data with %%theVariableName%%,
you can make your data more flexible, like this:

```json
"responses": {
"some-meaningful-scenario-name": {
"data": {
"today": "%%today%%"
}
}
}
```

For local development you can use the web interface to add, change or delete variables.
For protractor you can use the following commands
```js
ngApimock.setGlobalVariable(name, value); // to add or update
ngApimock.deleteGlobalVariable(name); // to delete
```

### Howto serve selected mocks
To be able to use the selected mocks you need to do two things:

1. Add the connect middleware
2. Add the mocking interface to your connect configuration

#### Add the connect middleware
When running connect you can do add the following middleware block to your configuration


```js
var app = connect();
app.use(require('ng-apimock/lib/utils').ngApimockRequest);
app.use(function middleware2(req, res, next) {
// middleware 2
next();
});
```

#### Add the mocking interface to your connect configuration
When running grunt-contrib-connect you can do add the following staticServe block to your configuration

```js
var app = connect();
app.use('/mocking', require('serve-static')('path/to/the/generated/mocking/index.html'));
app.use(function middleware2(req, res, next) {
// middleware 2
next();
});
```

### Howto use for local development

As you have configured both the [connect middleware](#add-the-connect-middleware) and the [mocking interface](#add-the-mocking-interface-to-your-connect-configuration), everything
should work out of the box. By default all the responses configured as default, will be returned if the expression matches.

If you would like to change the selected scenario, you can go to http://localhost:9000/mocking and use the interface to change the selected scenario or variables

The interface looks like this:

![alt tag](https://raw.githubusercontent.com/mdasberg/ng-apimock/master/img/web-interface-ng-apimock.png)



### Howto use for your protractor tests.
As you are building an [AngularJS](https://angularjs.org/) application you will probably use [Protractor](https://angular.github.io/protractor/#/) for testing your UI.

In order to use ngApimock in your protractor tests, require it in your protractor configuration like this:
```js
exports.config = {
onPrepare: function () {
global.ngApimock = require('.tmp/mocking/protractor.mock.js');
}
};
```
and from that point on you can use it in your tests
```js
describe('Some test', function () {
it('should do something', function() {
ngApimock.selectScenario('name of some api', 'another'); // at runtime you can change a scenario
});
});

```

By default all the scenario's marked as default will be returned if the expression matches. So you only need to add ngApimock.selectScenario in case your test needs
another scenario response to be returned.

NgApimock also works when running multiple tests concurrent, by using the protract session id of the test.
This ensures that changing a scenario in one test, will not effect another test.

### Using Angular 2 or higher with Protractor?
If you are using Angular 2 or higher in combination with Protractor you will need to add the following to you configuration.

**Protractor 4**
```js
exports.config = {
useAllAngular2AppRoots: true
};
```
**Protractor 5 or higher**
```js
exports.config = {
ngApimockOpts: {
angularVersion: 2, // {number} provide major version of Angular
hybrid: false // optional boolean which can be used for testing Angular apps within an AngularJs app.
}
};
```

### Available functions
All these functions are protractor promises, so they can be chained.

#### selectScenario(name, scenarioName, options)
Selects the given scenario (when calling this function without a scenario or with 'passThrough' as scenario name, the call will be passed through to the actual backend)

#### delayResponse(name, delay)
Sets the delay time in milliseconds for the mock so the response will be delayed. The delay set here superseeds the delay defined in the response mock.

#### echoRequest(name, indicator)
Sets the indicator which enables / disables the request logging (only post request should be logged)

#### setAllScenariosToDefault()
Resets all mocks to the default scenarios

#### setAllScenariosToPassThrough
Resets all mocks to use passthroughs

#### setGlobalVariable(key, value)
Adds or updates the global key/value pair

#### setGlobalVariables(variables)
Adds or updates the global key/value pairs ie. {'some':'value', 'another': 'value'}

#### deleteGlobalVariable(key)
Remove the global variable matching the key

### Howto use recording functionality
You can record API calls in NgApimock. This is usefull if you have a live API, and want to create mocks for them.
You turn on Recording in the header Record (checkbox), and start calling the API. Requests are recorded for each mock. You can zoom in up to Request Response information.
The response data can be used in mock files, described earlier.

## Contributing
In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code committing.


Do not use.
50 changes: 50 additions & 0 deletions lib/api/presets/applyPresetHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import * as http from 'http';
import {httpHeaders} from '../../http';
import Registry from '../../registry';
import Handler from '../../handler';
import Preset from '../../../tasks/preset';

/** Abstract Handler for Updating mock state. */
abstract class ApplyPresetHandler implements Handler {

/**
* Handle the preset selection.
* @param registry The registry.
* @param preset The preset identifier.
* @param ngApimockId The ngApimock id.
*/
abstract handlePresetSelection(registry: Registry, preset: Preset, ngApimockId?: string): void;

/**
* @inheritDoc
*
* Handler that takes care of applying the preset
*/
handleRequest(request: http.IncomingMessage, response: http.ServerResponse, next: Function, registry: Registry,
ngApimockId: string): void {
const requestDataChunks: Buffer[] = [];

request.on('data', (rawData: Buffer) => {
requestDataChunks.push(rawData);
});

request.on('end', () => {
const data = JSON.parse(Buffer.concat(requestDataChunks).toString());
try {
const match = registry.presets.find(_preset => _preset.name === data.preset);
if (match) {
this.handlePresetSelection(registry, match, ngApimockId);
} else {
throw new Error('No preset matching identifier [' + data + '] found');
}
response.writeHead(200, httpHeaders.CONTENT_TYPE_APPLICATION_JSON);
response.end();
} catch (e) {
response.writeHead(409, httpHeaders.CONTENT_TYPE_APPLICATION_JSON);
response.end(JSON.stringify(e, ['message']));
}
});
}
}

export default ApplyPresetHandler;
32 changes: 32 additions & 0 deletions lib/api/presets/getPresetsHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as http from 'http';
import {httpHeaders} from '../../http';
import Handler from '../../handler';
import Registry from '../../registry';
import Preset from '../../../tasks/preset';

class GetPresetsHandler implements Handler {

/**
* Gets the selections.
* @param registry The registry.
* @param ngApimockId The ngApimock id.
* @return presets The presets.
*/
getPresets(registry: Registry): Preset[] {
return registry.presets;
};

/**
* @inheritDoc
*
* Handler that takes care of getting all the mocks.
*/
handleRequest(request: http.IncomingMessage, response: http.ServerResponse, next: Function, registry: Registry): void {
const presets = this.getPresets(registry);

response.writeHead(200, httpHeaders.CONTENT_TYPE_APPLICATION_JSON);
response.end(JSON.stringify(presets));
}
}

export default GetPresetsHandler;
27 changes: 27 additions & 0 deletions lib/api/presets/protractor/applyPresetHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import ApplyPresetHandler from '../applyPresetHandler';
import helper from '../../helper';
import Preset from '../../../../tasks/preset';
import ProtractorResetMocksToDefaultsHandler from '../../mocks/protractor/resetMocksToDefaultsHandler';
import ProtractorUpdateMockHandler from '../../mocks/protractor/updateMockHandler';
import Registry from '../../../registry';

/** Handler that takes care of updating the mock configuration for protractor. */
class ProtractorApplyPresetHandler extends ApplyPresetHandler {

private resetDefaultsHandler: ProtractorResetMocksToDefaultsHandler = new ProtractorResetMocksToDefaultsHandler();
private updateMockHandler: ProtractorUpdateMockHandler = new ProtractorUpdateMockHandler();

/** @inheritDoc */
handlePresetSelection(registry: Registry, preset: Preset, ngApimockId: string): void {
helper.protractor.addSessionIfNonExisting(registry, ngApimockId);
this.resetDefaultsHandler.resetToDefaults(registry, ngApimockId);

for (const identifier in preset.scenarios) {
if (preset.scenarios.hasOwnProperty(identifier)) {
this.updateMockHandler.handleScenarioSelection(registry, identifier, preset.scenarios[identifier], ngApimockId)
}
}
}
}

export default ProtractorApplyPresetHandler;
26 changes: 26 additions & 0 deletions lib/api/presets/runtime/applyPresetHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import ApplyPresetHandler from '../applyPresetHandler';
import Preset from '../../../../tasks/preset';
import Registry from '../../../registry';
import RuntimeUpdateMockHandler from '../../mocks/runtime/updateMockHandler';
import RuntimeResetMocksToDefaultsHandler from '../../mocks/runtime/resetMocksToDefaultsHandler';

/** Handler that takes care of updating the mock configuration for protractor. */
class RuntimeApplyPresetHandler extends ApplyPresetHandler {

private resetDefaultsHandler = new RuntimeResetMocksToDefaultsHandler();
private updateMockHandler = new RuntimeUpdateMockHandler();

/** @inheritDoc */
handlePresetSelection(registry: Registry, preset: Preset, ngApimockId: string): void {
this.resetDefaultsHandler.resetToDefaults(registry);


for (const identifier in preset.scenarios) {
if (preset.scenarios.hasOwnProperty(identifier)) {
this.updateMockHandler.handleScenarioSelection(registry, identifier, preset.scenarios[identifier])
}
}
}
}

export default RuntimeApplyPresetHandler;
3 changes: 3 additions & 0 deletions lib/registry.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import Mock from '../tasks/mock';
import Preset from '../tasks/preset';

class Registry {
mocks: Mock[];
presets: Preset[];
defaults: { [key: string]: string };
sessions: {
[key: string]: {
Expand All @@ -20,6 +22,7 @@ class Registry {

constructor() {
this.mocks = [];
this.presets = [];
this.defaults = {};
this.sessions = {};
this.selections = {};
Expand Down
Loading