├── logs
├── change.log
└── access.log
├── scripts
└── es5-server.js
├── Procfile
├── .gitignore
├── stubs
├── !truthy!
│ ├── true.json
│ ├── ambiguous.json
│ └── false.json
├── !users!
│ ├── admins.json
│ └── users.json
├── !id!
│ ├── id.json
│ └── newStub.json
└── !index!
│ ├── index.html
│ └── index.xml
├── .babelrc
├── .travis.yml
├── bin
└── mocknode.js
├── dist
├── fonts
│ ├── glyphicons-halflings-regular.eot
│ ├── glyphicons-halflings-regular.ttf
│ ├── glyphicons-halflings-regular.woff
│ ├── glyphicons-halflings-regular.woff2
│ └── glyphicons-halflings-regular.svg
├── index.html
├── css
│ ├── styles.css
│ └── bootstrap-theme.min.css
└── script
│ └── fetch.js
├── src
├── Pages.jsx
├── webpack.config.js
├── index.jsx
├── header.jsx
├── stubs.jsx
├── routing.jsx
├── stubs-dynamic.jsx
├── stubForm.jsx
├── Store.js
├── routingManager.jsx
├── stubForm-dynamic.jsx
└── server.es6
├── package.json
├── config.json
├── README.md
└── server.js
/logs/change.log:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/es5-server.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: npm start
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | tmp
3 |
--------------------------------------------------------------------------------
/stubs/!truthy!/true.json:
--------------------------------------------------------------------------------
1 | {
2 | "return": true
3 | }
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets" : ["es2015", "react"]
3 | }
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "0.12"
--------------------------------------------------------------------------------
/stubs/!truthy!/ambiguous.json:
--------------------------------------------------------------------------------
1 | {
2 | "return": null
3 | }
--------------------------------------------------------------------------------
/stubs/!truthy!/false.json:
--------------------------------------------------------------------------------
1 | {
2 | "return": false
3 | }
--------------------------------------------------------------------------------
/bin/mocknode.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | require("../server.js");
4 |
--------------------------------------------------------------------------------
/dist/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianunay/mock-node/HEAD/dist/fonts/glyphicons-halflings-regular.eot
--------------------------------------------------------------------------------
/dist/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianunay/mock-node/HEAD/dist/fonts/glyphicons-halflings-regular.ttf
--------------------------------------------------------------------------------
/dist/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianunay/mock-node/HEAD/dist/fonts/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/dist/fonts/glyphicons-halflings-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianunay/mock-node/HEAD/dist/fonts/glyphicons-halflings-regular.woff2
--------------------------------------------------------------------------------
/logs/access.log:
--------------------------------------------------------------------------------
1 | {"route":"/id/","query strings":"{}","request body":"{}","ip":"::ffff:127.0.0.1","level":"info","message":"","timestamp":"2016-05-07T17:47:28.028Z"}
2 |
--------------------------------------------------------------------------------
/stubs/!users!/admins.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "user": "Anunay"
4 | },
5 | {
6 | "user": "Inuganti"
7 | },
8 | {
9 | "user": "anu"
10 | },
11 | {
12 | "user": "Inu"
13 | }
14 | ]
--------------------------------------------------------------------------------
/stubs/!users!/users.json:
--------------------------------------------------------------------------------
1 | {"users":[
2 | {
3 | "user": "Anunay"
4 | },
5 | {
6 | "user": "Inuganti"
7 | },
8 | {
9 | "user": "anu"
10 | },
11 | {
12 | "user": "Inu"
13 | }
14 | ]}
--------------------------------------------------------------------------------
/stubs/!id!/id.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "_id": "56asjdhkasjdhkajshdkjashdd"
4 | },
5 | {
6 | "_id": "123127af5583cf16c3c590619"
7 | },
8 | {
9 | "_id": "8ad213af5ffcb565e7b167f71"
10 | },
11 | {
12 | "_id": "9asd9ad7423ed9c6cc6e483c"
13 | },
14 | {
15 | "_id": "56c07af56e4f7d364fc35f8d"
16 | }
17 | ]
--------------------------------------------------------------------------------
/stubs/!index!/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Example file
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | This is an example html file
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/stubs/!id!/newStub.json:
--------------------------------------------------------------------------------
1 | {
2 | "sourcePort": 3000,
3 | "routes": [
4 | {
5 | "route": "/users",
6 | "proxy": "http://localhost:8090"
7 | },
8 | {
9 | "route": "/id",
10 | "proxy": "http://localhost:8090"
11 | },
12 | {
13 | "route": "/user",
14 | "proxy": "http://localhost:8010"
15 | },
16 | {
17 | "route": "/admins",
18 | "stub": "admins"
19 | },
20 | {
21 | "route": "/new/",
22 | "stub": "id"
23 | }
24 | ]
25 | }
--------------------------------------------------------------------------------
/src/Pages.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Store from './Store.js';
3 |
4 | const CHANGE_EVENT = 'PAGE_CHANGE_EVENT';
5 |
6 | class Pages extends React.Component {
7 | constructor(props, context) {
8 | super(props, context);
9 | this.changePage = this.changePage.bind(this);
10 | this.state = {
11 | activePage: 1
12 | }
13 | };
14 | componentDidMount(){
15 | Store.on(CHANGE_EVENT, this.changePage);
16 | }
17 | changePage(activePage){
18 | this.setState({activePage});
19 | }
20 | render(){
21 | return this.props.children[this.state.activePage - 1];
22 | }
23 | }
24 |
25 | module.exports = Pages;
--------------------------------------------------------------------------------
/src/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var path = require('path');
3 |
4 | var BUILD_DIR = path.resolve(__dirname, '../dist/script');
5 | var APP_DIR = path.resolve(__dirname);
6 |
7 | var config = {
8 | entry: APP_DIR + '/index.jsx',
9 | output: {
10 | path: BUILD_DIR,
11 | filename: 'bundle.js'
12 | },
13 | module : {
14 | loaders : [
15 | {
16 | test : /\.jsx?/,
17 | include : APP_DIR,
18 | loader : 'babel'
19 | }
20 | ]
21 | },
22 | resolve: {
23 | root: path.resolve(__dirname),
24 | alias: {
25 | store: './Store'
26 | },
27 | extensions: ['', '.js', '.jsx']
28 | }
29 | };
30 |
31 | module.exports = config;
--------------------------------------------------------------------------------
/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {render} from 'react-dom';
3 | import Header from './header.jsx';
4 | import Routing from './routing.jsx';
5 | import Stubs from './stubs.jsx';
6 | import DynamicStubs from './stubs-dynamic.jsx';
7 | import Pages from './Pages.jsx';
8 |
9 |
10 | class App extends React.Component {
11 | constructor(props, context) {
12 | super(props, context);
13 | };
14 | render(){
15 | return (
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | )
27 | }
28 | }
29 |
30 | render(, document.getElementById('app'));
--------------------------------------------------------------------------------
/stubs/!index!/index.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Gambardella, Matthew
5 | XML Developer's Guide
6 | Computer
7 | 44.95
8 | 2000-10-01
9 | An in-depth look at creating applications
10 | with XML.
11 |
12 |
13 | Ralls, Kim
14 | Midnight Rain
15 | Fantasy
16 | 5.95
17 | 2000-12-16
18 | A former architect battles corporate zombies,
19 | an evil sorceress, and her own childhood to become queen
20 | of the world.
21 |
22 |
--------------------------------------------------------------------------------
/src/header.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Navbar, Nav, NavDropdown, MenuItem} from 'react-bootstrap';
3 |
4 | class Header extends React.Component {
5 | constructor(props, context) {
6 | super(props, context);
7 | };
8 | render(){
9 | return (
10 |
11 |
12 |
13 | Mock Node
14 |
15 |
16 |
22 |
23 | );
24 | }
25 | }
26 |
27 | module.exports = Header;
28 |
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Mock Node configuration
5 |
6 |
7 |
8 |
9 |
10 |
11 |
13 |
14 |
15 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/dist/css/styles.css:
--------------------------------------------------------------------------------
1 | @import url(https://fonts.googleapis.com/css?family=Montserrat:400,700);
2 |
3 | body {
4 | background-color: #f2f2f2;
5 | font-family: 'Montserrat', sans-serif;
6 | font-weight: 400;
7 | font-size: 14px;
8 | color: #555;
9 |
10 | -webkit-font-smoothing: antialiased;
11 | -webkit-overflow-scrolling: touch;
12 | }
13 | /* Titles */
14 | h1, h2, h3, h4, h5, h6 {
15 | font-family: 'Montserrat', sans-serif;
16 | font-weight: 700;
17 | color: #333;
18 | }
19 |
20 | .container {
21 | width: 100%;
22 | }
23 | .navbar-inverse {
24 | background-color: #2f2f2f;
25 | border-radius: 0px;
26 | padding: 20px;
27 | }
28 | .navbar-inverse .navbar-nav>.active>a {
29 | background: none;
30 | box-shadow: none;
31 | }
32 | .route-panel .route {
33 | color: #333;
34 | display: inline-block;
35 | padding-left: 35px;
36 | }
37 | .route-panel a {
38 | display: block;
39 | text-decoration: none;
40 | }
41 | .panel-body a.link {
42 | display: inline-block;
43 | position: absolute;
44 | top: 0;
45 | right: 15px;
46 | }
47 | .route-panel a:hover .route {
48 | text-decoration: underline;
49 | }
50 | .route-panel.danger .label {
51 | padding: .2em 1.8em .3em;
52 | }
53 | .route-panel.warning .label {
54 | padding: .2em 1.3em .3em;
55 | }
56 | .route-panel.info .label {
57 | padding: .2em 1.6em .3em;
58 | }
59 | .contentInput {
60 | position: relative;
61 | }
62 | .contentInput a.link {
63 | position: absolute;
64 | right: 0;
65 | top: 0;
66 | }
67 | .backButton {
68 | display: inline-block;
69 | margin-top: 20px;
70 | font-size: 20px;
71 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mocknode",
3 | "version": "1.2.3",
4 | "description": "A configurable mock server with an intuitive configuration management interface and a http api",
5 | "author": "Anunay Inuganti",
6 | "license": "MIT",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/ianunay/mock-node.git"
10 | },
11 | "main": "server.js",
12 | "engines": {
13 | "node": ">=0.12.0"
14 | },
15 | "scripts": {
16 | "test": "echo \"Warn: tests to be written\"",
17 | "build-interface": "webpack -p --config src/webpack.config.js",
18 | "build-server": "babel src/server.es6 > server.js"
19 | },
20 | "bin": {
21 | "mocknode": "./bin/mocknode.js"
22 | },
23 | "keywords": [
24 | "mock",
25 | "dynamic",
26 | "dynamic mocks",
27 | "webserver",
28 | "reverse proxy",
29 | "proxy",
30 | "nodejs",
31 | "server"
32 | ],
33 | "devDependencies": {
34 | "babel-cli": "^6.7.5",
35 | "babel-core": "^6.5.2",
36 | "babel-loader": "^6.2.4",
37 | "babel-preset-es2015": "^6.6.0",
38 | "babel-preset-react": "^6.5.0",
39 | "react": "^0.14.7",
40 | "react-bootstrap": "^0.28.3",
41 | "react-dom": "^0.14.7",
42 | "valid-url": "^1.0.9",
43 | "webpack": "^1.12.14"
44 | },
45 | "dependencies": {
46 | "async": "^1.5.2",
47 | "body-parser": "^1.15.0",
48 | "cookie-parser": "^1.4.2",
49 | "express": "^4.13.4",
50 | "express-http-proxy": "^0.6.0",
51 | "fs-extra": "^0.30.0",
52 | "minimist": "^1.2.0",
53 | "object-assign": "^4.0.1",
54 | "rimraf": "^2.5.2",
55 | "sandcastle": "^1.3.3",
56 | "tar-fs": "^1.12.0",
57 | "winston": "^2.2.0"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/stubs.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Store from 'store';
3 | import {PageHeader, Tabs, Tab} from 'react-bootstrap';
4 | import StubForm from './stubForm.jsx';
5 |
6 | const config_get_event = 'CONFIG_FETCH_COMPLETE_EVENT',
7 | activate_tab_event = 'STUBS_ACTIVATE_TAB_EVENT';
8 |
9 | class Stubs extends React.Component {
10 | constructor(props, context) {
11 | super(props, context);
12 | this.updateState = this.updateState.bind(this);
13 | this.activateTab = this.activateTab.bind(this);
14 | this.reRender = this.reRender.bind(this);
15 |
16 |
17 | this.state = {
18 | stubs: [],
19 | activeTab: 1
20 | };
21 | };
22 | componentWillMount(){
23 | Store.on(config_get_event, this.updateState);
24 | Store.on(activate_tab_event, this.reRender);
25 | }
26 | componentDidMount(){
27 | this.updateState();
28 | }
29 | componentWillUnmount(){
30 | Store.removeListener(config_get_event, this.updateState);
31 | Store.removeListener(activate_tab_event, this.reRender);
32 | }
33 | updateState(){
34 | let stubs;
35 | for (var i = 0; i < Store.config.routes.length; i++) {
36 | if (Store.config.routes[i].route == Store.routeOfInterest) {
37 | stubs = Store.config.routes[i].stubs;
38 | break;
39 | }
40 | };
41 |
42 | this.setState({stubs});
43 | }
44 | reRender(){
45 | let that = this;
46 | setTimeout(() => {that.activateTab(1)}, 100);
47 | }
48 | activateTab(key){
49 | this.setState({activeTab: key});
50 | }
51 | render(){
52 | var tabs = [];
53 | tabs.push(
54 |
55 | );
56 | this.state.stubs.map((stub, i) => {
57 | tabs.push(
58 |
59 | )
60 | });
61 | return (
62 |
63 |
Back to Routes
64 |
Manage Stubs for {Store.routeOfInterest}
65 |
66 | {tabs}
67 |
68 |
69 | )
70 | }
71 | }
72 |
73 | module.exports = Stubs;
74 |
--------------------------------------------------------------------------------
/src/routing.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Store from 'store';
3 | import {PageHeader, Accordion, Grid, Row, Col, Button, Label, Panel} from 'react-bootstrap';
4 | import RoutingManager from './routingManager.jsx';
5 |
6 | const config_get_event = 'CONFIG_FETCH_COMPLETE_EVENT';
7 |
8 | class Routing extends React.Component {
9 | constructor(props, context) {
10 | super(props, context);
11 | this.updateState = this.updateState.bind(this);
12 | this.addRoute = this.addRoute.bind(this);
13 | this.deleteRoute = this.deleteRoute.bind(this);
14 |
15 | this.state = {
16 | routes: [],
17 | stublist: [],
18 | dynamiclist: []
19 | };
20 | };
21 | componentWillMount(){
22 | Store.getConfig();
23 | Store.on(config_get_event, this.updateState);
24 | }
25 | componentWillUnmount() {
26 | Store.removeListener(config_get_event, this.updateState);
27 | }
28 | addRoute() {
29 | this.setState({count: this.state.routes.push({newRoute: true, stubs:[], dynamicStubs: []})});
30 | }
31 | deleteRoute(i) {
32 | this.state.routes.splice(i, 1);
33 | this.setState({count: this.state.routes.length});
34 | }
35 | updateState(){
36 | this.setState({routes: Store.config.routes, count: Store.config.routes.length});
37 | }
38 | render(){
39 | let PanelArray = [];
40 | this.state.routes.map(function(route, i){
41 | let panelStyle = route.newRoute ? "danger" : (route.handle == "proxy") ? "warning" : (route.handle == "stub") ? "info" : "success";
42 | let header = (
43 |
44 |
47 | {route.route || 'New Route'}
48 |
49 | );
50 | PanelArray.push(
51 |
52 |
54 |
55 | )
56 | }.bind(this));
57 | PanelArray.push();
58 | return (
59 |
60 |
61 |
62 | Manage routes
63 |
64 | {PanelArray}
65 |
66 |
67 |
68 |
69 | )
70 | }
71 | }
72 |
73 | module.exports = Routing;
74 |
--------------------------------------------------------------------------------
/src/stubs-dynamic.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Store from 'store';
3 | import {PageHeader, Tabs, Tab} from 'react-bootstrap';
4 | import StubFormDynamic from './stubForm-dynamic.jsx';
5 |
6 | const config_get_event = 'CONFIG_FETCH_COMPLETE_EVENT',
7 | activate_tab_event = 'DYNAMIC_STUBS_ACTIVATE_TAB_EVENT';
8 |
9 | class Stubs extends React.Component {
10 | constructor(props, context) {
11 | super(props, context);
12 | this.updateState = this.updateState.bind(this);
13 | this.activateTab = this.activateTab.bind(this);
14 | this.reRender = this.reRender.bind(this);
15 |
16 | this.state = {
17 | dynamic: [],
18 | stublist: [],
19 | activeTab: 1
20 | };
21 | };
22 | componentWillMount(){
23 | Store.on(config_get_event, this.updateState);
24 | Store.on(activate_tab_event, this.reRender);
25 | }
26 | componentDidMount(){
27 | this.updateState();
28 | }
29 | componentWillUnmount(){
30 | Store.removeListener(config_get_event, this.updateState);
31 | Store.removeListener(activate_tab_event, this.reRender);
32 | }
33 | reRender(){
34 | let that = this;
35 | setTimeout(() => {that.activateTab(1)}, 100);
36 | }
37 | updateState(){
38 | let dynamic,
39 | stublist;
40 | for (var i = 0; i < Store.config.routes.length; i++) {
41 | if (Store.config.routes[i].route == Store.routeOfInterest) {
42 | dynamic = Store.config.routes[i].dynamicStubs;
43 | stublist = Store.config.routes[i].stubs;
44 | break;
45 | }
46 | };
47 | this.setState({dynamic, stublist});
48 | }
49 | activateTab(key){
50 | this.setState({activeTab: key});
51 | }
52 | render(){
53 | var tabs = [];
54 | tabs.push(
55 |
56 | );
57 | this.state.dynamic.map((stub, i) => {
58 | tabs.push(
59 |
60 |
63 |
64 | )
65 | });
66 | return (
67 |
68 |
Back to Routes
69 |
Manage Dynamic Stubs for {Store.routeOfInterest}
70 |
71 | {tabs}
72 |
73 |
74 | )
75 | }
76 | }
77 |
78 | module.exports = Stubs;
79 |
--------------------------------------------------------------------------------
/src/stubForm.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Store from 'store';
3 | import {Input, Button} from 'react-bootstrap';
4 |
5 | const stub_get_event = 'STUB_GET_COMPLETE_EVENT';
6 |
7 | class StubForm extends React.Component {
8 | constructor(props, context) {
9 | super(props, context);
10 | this.updateState = this.updateState.bind(this);
11 | this.handleChange = this.handleChange.bind(this);
12 | this.validate = this.validate.bind(this);
13 | this.postData = this.postData.bind(this);
14 | this.format = this.format.bind(this);
15 | this.deleteStub = this.deleteStub.bind(this);
16 |
17 | this.state = Object.assign({content: "", oldName: this.props.name}, this.props);
18 | };
19 | componentWillMount(){
20 | Store.on(stub_get_event, this.updateState);
21 | if (this.props.name)
22 | Store.getStub(this.props.name);
23 | }
24 | componentWillUnmount() {
25 | Store.removeListener(stub_get_event, this.updateState);
26 | }
27 | updateState(){
28 | this.setState({content: Store.stubs[this.props.name]});
29 | }
30 | handleChange(elem, event){
31 | let partialState = {};
32 | partialState[elem] = event ? event.target.value : window.event.target.value;
33 | this.setState(partialState);
34 | this.validate(Object.assign(this.state, partialState));
35 | }
36 | validate(state){
37 | this.setState({formValid: state.name && state.content})
38 | }
39 | postData(){
40 | Store.postStubData(this.state);
41 | }
42 | format(){
43 | let content;
44 | try {
45 | content = JSON.stringify(JSON.parse(this.state.content), null, 2);
46 | } catch (e) {}
47 | if (content)
48 | this.setState({content: content})
49 | }
50 | deleteStub(){
51 | if (confirm("Are you sure you want to delete the stub : "+ this.props.name + " ?"))
52 | Store.deleteStub(this.props.name);
53 | }
54 | render(){
55 | let actions = this.state.new
56 | ?
57 | : (
58 |
);
59 | return (
60 |
71 | )
72 | }
73 | }
74 |
75 | module.exports = StubForm;
76 |
--------------------------------------------------------------------------------
/src/Store.js:
--------------------------------------------------------------------------------
1 | let EventEmitter = require('events').EventEmitter,
2 | assign = require('object-assign');
3 |
4 | let Store = assign({}, EventEmitter.prototype, {
5 | config: {},
6 | stubs: {},
7 | routeOfInterest: "",
8 | updatePage: (page) => {
9 | Store.page = page;
10 | Store.emit('PAGE_CHANGE_EVENT', page);
11 | },
12 | getConfig: () => {
13 | fetch('/mocknode/api/config').then((response) => {
14 | return response.json()
15 | }).then((json) => {
16 | Store.config = json;
17 | Store.emit('CONFIG_FETCH_COMPLETE_EVENT');
18 | })
19 | },
20 | changeConfig: (config) => {
21 | let {old_route, route, handle, proxy, stub, stubs, dynamicStub, dynamicStubs} = config;
22 |
23 | fetch('/mocknode/api/modifyroute', {
24 | method: 'post',
25 | headers: {
26 | 'Accept': 'application/json',
27 | 'Content-Type': 'application/json'
28 | },
29 | body: JSON.stringify({
30 | old_route: old_route,
31 | route: route,
32 | handle: handle,
33 | proxy: proxy,
34 | stub: stub,
35 | dynamicStub: dynamicStub
36 | })
37 | }).then((json) => {
38 | Store.getConfig();
39 | })
40 |
41 | },
42 | getStub: (stub) => {
43 | fetch('/mocknode/api/getstub?name='+stub+"&route="+Store.routeOfInterest).then((response) => {
44 | return response.text()
45 | }).then((json) => {
46 | Store.stubs[stub] = json;
47 | Store.emit('STUB_GET_COMPLETE_EVENT');
48 | })
49 | },
50 | postStubData: (state) => {
51 | fetch('/mocknode/api/modifystub', {
52 | method: 'post',
53 | headers: {
54 | 'Accept': 'application/json',
55 | 'Content-Type': 'application/json'
56 | },
57 | body: JSON.stringify({
58 | route: Store.routeOfInterest,
59 | oldname: state.oldName,
60 | name: state.name,
61 | description: state.description,
62 | content: state.content
63 | })
64 | }).then((json) => {
65 | Store.getConfig();
66 | })
67 | },
68 | postDynamicStubData: (state) => {
69 | fetch('/mocknode/api/modifydynamicstub', {
70 | method: 'post',
71 | headers: {
72 | 'Accept': 'application/json',
73 | 'Content-Type': 'application/json'
74 | },
75 | body: JSON.stringify({
76 | route: Store.routeOfInterest,
77 | oldname: state.oldName,
78 | name: state.name,
79 | description: state.description,
80 | defaultStub: state.defaultStub,
81 | conditions: state.conditions
82 | })
83 | }).then((json) => {
84 | Store.getConfig();
85 | })
86 | },
87 | deleteStub: (stub) => {
88 | fetch('/mocknode/api/deletestub?name='+stub+"&route="+Store.routeOfInterest).then((res) => {
89 | Store.getConfig();
90 | Store.emit('STUBS_ACTIVATE_TAB_EVENT', 1);
91 | })
92 | },
93 | deleteDynamicStub: (stub) => {
94 | fetch('/mocknode/api/deletedynamicstub?name='+stub+"&route="+Store.routeOfInterest).then((res) => {
95 | Store.getConfig();
96 | Store.emit('DYNAMIC_STUBS_ACTIVATE_TAB_EVENT', 1);
97 | })
98 | },
99 | deleteRoute: (route) => {
100 | fetch('/mocknode/api/deleteroute?route='+route).then((res) => {
101 | Store.getConfig();
102 | })
103 | }
104 | });
105 |
106 | module.exports = Store;
107 |
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "port": 3000,
3 | "routes": [
4 | {
5 | "route": "/users/",
6 | "handle": "proxy",
7 | "proxy": "http://localhost:8090",
8 | "stub": "users.json",
9 | "dynamicStub": "user conditions",
10 | "stubs": [
11 | {
12 | "name": "admins.json",
13 | "description": "A list of admins, this returns an array."
14 | },
15 | {
16 | "name": "users.json",
17 | "description": "this is the general users"
18 | }
19 | ],
20 | "dynamicStubs": [
21 | {
22 | "name": "user conditions",
23 | "description": "this is an example conditional stubing",
24 | "defaultStub": "id.json",
25 | "conditions": [
26 | {
27 | "eval": "req.body.user == 'admin'",
28 | "stub": "admins.json"
29 | },
30 | {
31 | "eval": "req.query.name == 'anunay'",
32 | "stub": "users.json"
33 | }
34 | ]
35 | }
36 | ]
37 | },
38 | {
39 | "route": "/id/",
40 | "handle": "dynamicStub",
41 | "proxy": "http://localhost:8090",
42 | "stub": "id.json",
43 | "dynamicStub": "random conditions",
44 | "stubs": [
45 | {
46 | "name": "id.json",
47 | "description": "A list of ids"
48 | },
49 | {
50 | "name": "newStub.json",
51 | "description": "dummy stub for example"
52 | }
53 | ],
54 | "dynamicStubs": [
55 | {
56 | "name": "random conditions",
57 | "description": "this is conditional stubing for ids",
58 | "defaultStub": "id.json",
59 | "conditions": [
60 | {
61 | "eval": "req.body.id == 'id'",
62 | "stub": "id.json"
63 | },
64 | {
65 | "eval": "req.body.newStub == 'new'",
66 | "stub": "newStub.json"
67 | }
68 | ]
69 | }
70 | ]
71 | },
72 | {
73 | "route": "/index/",
74 | "handle": "stub",
75 | "proxy": "http://localhost:8090",
76 | "stub": "index.xml",
77 | "dynamicStub": "",
78 | "stubs": [
79 | {
80 | "name": "index.html",
81 | "description": "You can even stub html files"
82 | },
83 | {
84 | "name": "index.xml",
85 | "description": "This is an example xml file"
86 | }
87 | ],
88 | "dynamicStubs": []
89 | },
90 | {
91 | "route": "/truthy/",
92 | "handle": "dynamicStub",
93 | "proxy": "http://localhost:3000",
94 | "stubs": [
95 | {
96 | "name": "true.json",
97 | "description": "returns true :)"
98 | },
99 | {
100 | "name": "false.json",
101 | "description": "returns false :)"
102 | },
103 | {
104 | "name": "ambiguous.json",
105 | "description": "ambiguous"
106 | }
107 | ],
108 | "dynamicStubs": [
109 | {
110 | "name": "dynamic stub",
111 | "description": "its a dynamic stub",
112 | "defaultStub": "ambiguous.json",
113 | "conditions": [
114 | {
115 | "eval": "req.query.return == \"true\"",
116 | "stub": "true.json"
117 | },
118 | {
119 | "eval": "req.query.return == \"false\"",
120 | "stub": "false.json"
121 | }
122 | ]
123 | },
124 | {
125 | "name": "single dynamic stub",
126 | "description": "checking one condition",
127 | "defaultStub": "ambiguous.json",
128 | "conditions": [
129 | {
130 | "eval": "req.query.return == \"true\"",
131 | "stub": "true.json"
132 | }
133 | ]
134 | },
135 | {
136 | "name": "no dynamic",
137 | "description": "something goes here",
138 | "defaultStub": "ambiguous.json",
139 | "conditions": []
140 | }
141 | ],
142 | "dynamicStub": "dynamic stub"
143 | }
144 | ],
145 | "global": {
146 | "delay": 0,
147 | "headers": {
148 | "Access-Control-Allow-Origin": "*",
149 | "X-Powered-By": "mocknode"
150 | }
151 | }
152 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Mock Node
2 | A configurable mock server with an intuitive configuration management interface and a http api.
3 |
4 | [More Information...](https://medium.com/@i.anunay/mocknode-e338f793dba0)
5 |
6 | [](https://travis-ci.org/ianunay/mock-node)
7 |
8 | [](https://www.npmjs.com/package/mocknode)
9 |
10 | Mocknode allows you to mock http endpoints quickly and easily. The simple management interface lets you configure how the server responds to different endpoints:
11 |
12 | 1. Proxy - Proxies the request to an existing service
13 | 2. Stub - A static response which you can manage in the 'manage stubs' link of the route
14 | 3. Dynamic Stub - Responds with a configured stub if the condition defined evaluates to true
15 |
16 | The interface allows every one working on the team to have clear visibility on what routes are being used by an application and all possible responses of the route. Thus making stubs to act as proper documented examples.
17 |
18 | You can use mocknode to write integration tests for your application. Dynamic stubs can be used to define strategies which can be asserted in your test scripts. You can have even more granular control on the integration tests by using the http api exposed by mocknode. Use the API to toggle the handle for a route, change the stub which is being responded, etc.
19 |
20 | ## A running instance on heroku
21 |
22 | [https://mocknode.herokuapp.com/mocknode/](https://mocknode.herokuapp.com/mocknode/)
23 | please go easy on this :)
24 |
25 | ## Installing and Runing the server
26 |
27 | ### Standalone
28 |
29 | 1. clone/download this repository
30 | 2. make sure you have node and npm installed
31 | 3. run npm install to download required packages
32 | 4. node server.js to run the server
33 |
34 | ### npm global install
35 |
36 | 1. npm install -g mocknode
37 | 2. run mocknode to start the server
38 |
39 | ### npm local install
40 |
41 | 1. npm install --save mocknode
42 | 2. add an npm script entry in the package.json "mocknode": "mocknode"
43 | 3. run npm run mocknode to start the server
44 |
45 | and open [https://localhost:3000/mocknode/](https://localhost:3000/mocknode/) in your browser to configure mocknode
46 |
47 |
48 | Mocknode has a downstream dependency on buffertools which executes a node-gyp during install.
49 | A common problem with node-gyp exists [nodejs/node-gyp#809](https://github.com/nodejs/node-gyp/issues/809). If there are node-gyp errors during 'npm install' an easy fix is to remove the gyp installed on your OS.
50 |
51 | ## The Port
52 |
53 | The order of preference for the port on which mocknode runs is:
54 |
55 | env variable of process > port option passed > 3000
56 |
57 | mocknode --port 4000 starts the server on port 4000, unless a PORT env variable exists for the process.
58 |
59 |
60 | ## Mocknode configuration
61 |
62 | Open [https://localhost:3000/mocknode/](https://localhost:3000/mocknode/) in your browser to configure mocknode
63 |
64 | 
65 |
66 | All your changes are saved in the config.json file and the stubs folder. This ensures all your changes are saved if you restart mocknode. You can easily backup all of your configuration by making a copy of these files.
67 |
68 | ## Installation directory
69 |
70 | If you have installed mocknode as a global npm package
71 |
72 | mocknode --location prints the installation path of mocknode.
73 |
74 | ## Logs
75 |
76 | mocknode stores logs in the logs/ folder of the installation directory
77 | 1. access.log - logs all requests to mocknode
78 | 2. change.log - logs all configuration change requests made to mocknode
79 |
80 | ## Export and Import config
81 |
82 | The config.json file and stubs folder hold all the configuration of mocknode.
83 |
84 | mocknode --export creates a mocknode-config.tar file which can be used to setup another instance of mocknode.
85 | mocknode --import [file_path] imports a config.tar file to configure mocknode.
86 |
87 | ## HTTP API
88 |
89 | Mocknode exposes a series of endpoints which can help you integrate it with your code - [ test scripts for example ]
90 |
91 |
92 | | enpoint | method | params |
93 | | :------------------------------ | --------:| :------------: |
94 | | /mocknode/api/config | GET | - |
95 | | /mocknode/api/stubconfig | GET | - |
96 | | /mocknode/api/getstub | GET | name |
97 | | /mocknode/api/modifyroute | POST | route config |
98 | | /mocknode/api/deleteroute | GET | route |
99 | | /mocknode/api/modifystub | POST | stub config |
100 | | /mocknode/api/deletestub | GET | name |
101 | | /mocknode/api/modifydynamicstub | POST | stub config |
102 | | /mocknode/api/deletedynamicstub | GET | name |
103 |
104 |
105 | mocknode interface uses the above endpoints to interact with the server, inspect the network of the browser to better understand the parameters used in each api.
106 |
107 | ### Troubleshooting
108 |
109 | The config files and the stubs folder mentioned above have all the information regarding your configuration.
110 |
111 |
112 | ### License
113 |
114 | MIT
115 |
--------------------------------------------------------------------------------
/src/routingManager.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Store from 'store';
3 | import {Input, ButtonToolbar, ButtonGroup, Button, Grid, Row, Col, Modal} from 'react-bootstrap';
4 | import {isWebUri} from 'valid-url';
5 |
6 | class RoutingManager extends React.Component {
7 | constructor(props, context) {
8 | super(props, context);
9 | this.handleChange = this.handleChange.bind(this);
10 | this.validate = this.validate.bind(this);
11 | this.updateRoute = this.updateRoute.bind(this);
12 | this.deleteRoute = this.deleteRoute.bind(this);
13 | this.manageStubs = this.manageStubs.bind(this);
14 |
15 | this.state = Object.assign(
16 | {formValid: false, old_route: this.props.route},
17 | this.props, {handle: this.props.newRoute ? "proxy" : this.props.handle},
18 | {proxy: this.props.newRoute ? window.location.origin : this.props.proxy}
19 | );
20 | };
21 | componentWillReceiveProps(nextProps){
22 | this.setState({newRoute: nextProps.newRoute, old_route: nextProps.route});
23 | }
24 | validate(state){
25 | let validity = state.route && state.route.charAt(0) == '/' && state.route.charAt(state.route.length - 1) == '/'
26 | && state.route.length > 2 && !/[^a-zA-Z0-9\_\-\/]/.test( state.route ) &&
27 | ((state.handle == "proxy" && state.proxy && isWebUri(state.proxy) && state.proxy.charAt(state.proxy.length - 1) != '/')
28 | || (state.handle == "stub" && state.stub)
29 | || (state.handle == "dynamicStub" && state.dynamicStub));
30 | this.setState({formValid: validity})
31 | }
32 | handleChange(elem, event){
33 | let partialState = {};
34 | partialState[elem] = event ? event.target.value : window.event.target.value;
35 | this.setState(partialState);
36 | this.validate(Object.assign(this.state, partialState));
37 | }
38 | updateRoute(){
39 | Store.changeConfig(this.state);
40 | }
41 | deleteRoute(){
42 | if (confirm("Are you sure you want to delete the route : "+ (this.props.route || "new route") + " ? All the stubs related to this route will be deleted.")) {
43 | this.props.delete();
44 | if (!this.props.newRoute) {
45 | Store.deleteRoute(this.props.route);
46 | }
47 | }
48 | }
49 | manageStubs(page, type) {
50 | Store.routeOfInterest = this.state.old_route;
51 | Store.updatePage(page);
52 | }
53 | render(){
54 | let routeInput,
55 | stublist;
56 | if (this.state.handle == "proxy") {
57 | routeInput = ;
58 | } else if (this.state.handle == "stub") {
59 | let options = this.state.stubs.map((stub, i) => );
60 | routeInput = (
61 |
68 | );
69 | } else if (this.state.handle == "dynamicStub") {
70 | let options = this.state.dynamicStubs.map((stub, i) => );
71 | routeInput = (
72 |
79 | );
80 | }
81 | return (
82 |
83 |
84 |
85 | Route link} placeholder="/route/" onChange={this.handleChange.bind(this, "route")} onBlur={this.handleChange.bind(this, "route")} />
86 |
87 | {routeInput}
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | )
105 | }
106 | }
107 |
108 | module.exports = RoutingManager;
109 |
--------------------------------------------------------------------------------
/src/stubForm-dynamic.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Store from 'store';
3 | import {Input, Badge, Button, Grid, Row, Col, Modal} from 'react-bootstrap';
4 |
5 | class StubForm extends React.Component {
6 | constructor(props, context) {
7 | super(props, context);
8 | this.handleChange = this.handleChange.bind(this);
9 | this.handleConditionChange = this.handleConditionChange.bind(this);
10 | this.validate = this.validate.bind(this);
11 | this.postData = this.postData.bind(this);
12 | this.deleteStub = this.deleteStub.bind(this);
13 | this.addConditions = this.addConditions.bind(this);
14 | this.removeCondition = this.removeCondition.bind(this);
15 |
16 | this.state = Object.assign({oldName: this.props.name, stublist: [], conditions: []}, this.props);
17 | };
18 | componentWillReceiveProps(nextProps){
19 | this.setState({stublist: nextProps.stublist});
20 | }
21 | handleChange(elem, event){
22 | let partialState = {};
23 | partialState[elem] = event ? event.target.value : window.event.target.value;
24 | this.setState(partialState);
25 | this.validate(Object.assign(this.state, partialState));
26 | }
27 | handleConditionChange(elem, index, event){
28 | let newConditions = this.state.conditions.slice();
29 | let partialCondition = newConditions[index];
30 |
31 | partialCondition[elem] = event ? event.target.value : window.event.target.value;
32 | this.setState({conditions: newConditions});
33 | this.validate(Object.assign(this.state, {conditions: newConditions}));
34 | }
35 | postData(){
36 | Store.postDynamicStubData(this.state);
37 | }
38 | validate(state){
39 | let valid = state.name && state.defaultStub;
40 | valid && state.conditions.map(function(condition) {
41 | if (!condition.eval || !condition.stub)
42 | valid = false;
43 | });
44 | this.setState({formValid: valid});
45 | }
46 | deleteStub(){
47 | if (confirm("Are you sure you want to delete the stub : "+ this.props.name + " ?"))
48 | Store.deleteDynamicStub(this.props.name);
49 | }
50 | addConditions(){
51 | let newState = Object.assign({}, this.state);
52 | newState.conditions.push({});
53 | this.setState(newState);
54 | this.validate(newState);
55 | }
56 | removeCondition(index){
57 | let newConditions = this.state.conditions.slice();
58 | newConditions.splice(index, 1);
59 | this.setState({conditions: newConditions});
60 | this.validate(Object.assign({}, this.state, {conditions: newConditions}));
61 | }
62 | render(){
63 | let actions = this.state.new
64 | ?
65 | : (
66 |
);
67 |
68 | let options = this.state.stublist.map((stub, i) => );
69 |
70 | let conditions = this.state.conditions.map((condition, i) => (
71 |
72 | {i+1}
73 |
74 | use req to form a javascript expression this.setState({helpModal: true})}>help on conditions}/>
77 |
78 |
79 | {options}
80 |
81 | Remove Condition
82 |
83 |
84 | )
85 | );
86 |
87 | return (
88 |
89 |
108 |
109 |
110 | Help on conditions
111 |
112 |
113 | The req is a javascript object which you can use to form conditions
114 | If the incoming request has query strings use req.query.key to read the value.
115 | If the incoming request is a post use req.body.key to read the value.
116 | Use req.path == '/path' to match a url which ends with '/path'
117 | List of all properties available in the req object :
118 | baseURL
body
cookies
headers
hostname
ip
ips
method
originalUrl
params
path
protocol
query
route
signedCookies
stale
subdomains
xhr
119 | These properties are being provided by expressjs module
120 | See more about these properties here
121 |
122 |
123 |
124 |
125 |
126 |
127 | )
128 | }
129 | }
130 |
131 | module.exports = StubForm;
132 |
--------------------------------------------------------------------------------
/dist/script/fetch.js:
--------------------------------------------------------------------------------
1 | (function(self) {
2 | 'use strict';
3 |
4 | if (self.fetch) {
5 | return
6 | }
7 |
8 | function normalizeName(name) {
9 | if (typeof name !== 'string') {
10 | name = String(name)
11 | }
12 | if (/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(name)) {
13 | throw new TypeError('Invalid character in header field name')
14 | }
15 | return name.toLowerCase()
16 | }
17 |
18 | function normalizeValue(value) {
19 | if (typeof value !== 'string') {
20 | value = String(value)
21 | }
22 | return value
23 | }
24 |
25 | function Headers(headers) {
26 | this.map = {}
27 |
28 | if (headers instanceof Headers) {
29 | headers.forEach(function(value, name) {
30 | this.append(name, value)
31 | }, this)
32 |
33 | } else if (headers) {
34 | Object.getOwnPropertyNames(headers).forEach(function(name) {
35 | this.append(name, headers[name])
36 | }, this)
37 | }
38 | }
39 |
40 | Headers.prototype.append = function(name, value) {
41 | name = normalizeName(name)
42 | value = normalizeValue(value)
43 | var list = this.map[name]
44 | if (!list) {
45 | list = []
46 | this.map[name] = list
47 | }
48 | list.push(value)
49 | }
50 |
51 | Headers.prototype['delete'] = function(name) {
52 | delete this.map[normalizeName(name)]
53 | }
54 |
55 | Headers.prototype.get = function(name) {
56 | var values = this.map[normalizeName(name)]
57 | return values ? values[0] : null
58 | }
59 |
60 | Headers.prototype.getAll = function(name) {
61 | return this.map[normalizeName(name)] || []
62 | }
63 |
64 | Headers.prototype.has = function(name) {
65 | return this.map.hasOwnProperty(normalizeName(name))
66 | }
67 |
68 | Headers.prototype.set = function(name, value) {
69 | this.map[normalizeName(name)] = [normalizeValue(value)]
70 | }
71 |
72 | Headers.prototype.forEach = function(callback, thisArg) {
73 | Object.getOwnPropertyNames(this.map).forEach(function(name) {
74 | this.map[name].forEach(function(value) {
75 | callback.call(thisArg, value, name, this)
76 | }, this)
77 | }, this)
78 | }
79 |
80 | function consumed(body) {
81 | if (body.bodyUsed) {
82 | return Promise.reject(new TypeError('Already read'))
83 | }
84 | body.bodyUsed = true
85 | }
86 |
87 | function fileReaderReady(reader) {
88 | return new Promise(function(resolve, reject) {
89 | reader.onload = function() {
90 | resolve(reader.result)
91 | }
92 | reader.onerror = function() {
93 | reject(reader.error)
94 | }
95 | })
96 | }
97 |
98 | function readBlobAsArrayBuffer(blob) {
99 | var reader = new FileReader()
100 | reader.readAsArrayBuffer(blob)
101 | return fileReaderReady(reader)
102 | }
103 |
104 | function readBlobAsText(blob) {
105 | var reader = new FileReader()
106 | reader.readAsText(blob)
107 | return fileReaderReady(reader)
108 | }
109 |
110 | var support = {
111 | blob: 'FileReader' in self && 'Blob' in self && (function() {
112 | try {
113 | new Blob();
114 | return true
115 | } catch(e) {
116 | return false
117 | }
118 | })(),
119 | formData: 'FormData' in self,
120 | arrayBuffer: 'ArrayBuffer' in self
121 | }
122 |
123 | function Body() {
124 | this.bodyUsed = false
125 |
126 |
127 | this._initBody = function(body) {
128 | this._bodyInit = body
129 | if (typeof body === 'string') {
130 | this._bodyText = body
131 | } else if (support.blob && Blob.prototype.isPrototypeOf(body)) {
132 | this._bodyBlob = body
133 | } else if (support.formData && FormData.prototype.isPrototypeOf(body)) {
134 | this._bodyFormData = body
135 | } else if (!body) {
136 | this._bodyText = ''
137 | } else if (support.arrayBuffer && ArrayBuffer.prototype.isPrototypeOf(body)) {
138 | // Only support ArrayBuffers for POST method.
139 | // Receiving ArrayBuffers happens via Blobs, instead.
140 | } else {
141 | throw new Error('unsupported BodyInit type')
142 | }
143 |
144 | if (!this.headers.get('content-type')) {
145 | if (typeof body === 'string') {
146 | this.headers.set('content-type', 'text/plain;charset=UTF-8')
147 | } else if (this._bodyBlob && this._bodyBlob.type) {
148 | this.headers.set('content-type', this._bodyBlob.type)
149 | }
150 | }
151 | }
152 |
153 | if (support.blob) {
154 | this.blob = function() {
155 | var rejected = consumed(this)
156 | if (rejected) {
157 | return rejected
158 | }
159 |
160 | if (this._bodyBlob) {
161 | return Promise.resolve(this._bodyBlob)
162 | } else if (this._bodyFormData) {
163 | throw new Error('could not read FormData body as blob')
164 | } else {
165 | return Promise.resolve(new Blob([this._bodyText]))
166 | }
167 | }
168 |
169 | this.arrayBuffer = function() {
170 | return this.blob().then(readBlobAsArrayBuffer)
171 | }
172 |
173 | this.text = function() {
174 | var rejected = consumed(this)
175 | if (rejected) {
176 | return rejected
177 | }
178 |
179 | if (this._bodyBlob) {
180 | return readBlobAsText(this._bodyBlob)
181 | } else if (this._bodyFormData) {
182 | throw new Error('could not read FormData body as text')
183 | } else {
184 | return Promise.resolve(this._bodyText)
185 | }
186 | }
187 | } else {
188 | this.text = function() {
189 | var rejected = consumed(this)
190 | return rejected ? rejected : Promise.resolve(this._bodyText)
191 | }
192 | }
193 |
194 | if (support.formData) {
195 | this.formData = function() {
196 | return this.text().then(decode)
197 | }
198 | }
199 |
200 | this.json = function() {
201 | return this.text().then(JSON.parse)
202 | }
203 |
204 | return this
205 | }
206 |
207 | // HTTP methods whose capitalization should be normalized
208 | var methods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT']
209 |
210 | function normalizeMethod(method) {
211 | var upcased = method.toUpperCase()
212 | return (methods.indexOf(upcased) > -1) ? upcased : method
213 | }
214 |
215 | function Request(input, options) {
216 | options = options || {}
217 | var body = options.body
218 | if (Request.prototype.isPrototypeOf(input)) {
219 | if (input.bodyUsed) {
220 | throw new TypeError('Already read')
221 | }
222 | this.url = input.url
223 | this.credentials = input.credentials
224 | if (!options.headers) {
225 | this.headers = new Headers(input.headers)
226 | }
227 | this.method = input.method
228 | this.mode = input.mode
229 | if (!body) {
230 | body = input._bodyInit
231 | input.bodyUsed = true
232 | }
233 | } else {
234 | this.url = input
235 | }
236 |
237 | this.credentials = options.credentials || this.credentials || 'omit'
238 | if (options.headers || !this.headers) {
239 | this.headers = new Headers(options.headers)
240 | }
241 | this.method = normalizeMethod(options.method || this.method || 'GET')
242 | this.mode = options.mode || this.mode || null
243 | this.referrer = null
244 |
245 | if ((this.method === 'GET' || this.method === 'HEAD') && body) {
246 | throw new TypeError('Body not allowed for GET or HEAD requests')
247 | }
248 | this._initBody(body)
249 | }
250 |
251 | Request.prototype.clone = function() {
252 | return new Request(this)
253 | }
254 |
255 | function decode(body) {
256 | var form = new FormData()
257 | body.trim().split('&').forEach(function(bytes) {
258 | if (bytes) {
259 | var split = bytes.split('=')
260 | var name = split.shift().replace(/\+/g, ' ')
261 | var value = split.join('=').replace(/\+/g, ' ')
262 | form.append(decodeURIComponent(name), decodeURIComponent(value))
263 | }
264 | })
265 | return form
266 | }
267 |
268 | function headers(xhr) {
269 | var head = new Headers()
270 | var pairs = xhr.getAllResponseHeaders().trim().split('\n')
271 | pairs.forEach(function(header) {
272 | var split = header.trim().split(':')
273 | var key = split.shift().trim()
274 | var value = split.join(':').trim()
275 | head.append(key, value)
276 | })
277 | return head
278 | }
279 |
280 | Body.call(Request.prototype)
281 |
282 | function Response(bodyInit, options) {
283 | if (!options) {
284 | options = {}
285 | }
286 |
287 | this.type = 'default'
288 | this.status = options.status
289 | this.ok = this.status >= 200 && this.status < 300
290 | this.statusText = options.statusText
291 | this.headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers)
292 | this.url = options.url || ''
293 | this._initBody(bodyInit)
294 | }
295 |
296 | Body.call(Response.prototype)
297 |
298 | Response.prototype.clone = function() {
299 | return new Response(this._bodyInit, {
300 | status: this.status,
301 | statusText: this.statusText,
302 | headers: new Headers(this.headers),
303 | url: this.url
304 | })
305 | }
306 |
307 | Response.error = function() {
308 | var response = new Response(null, {status: 0, statusText: ''})
309 | response.type = 'error'
310 | return response
311 | }
312 |
313 | var redirectStatuses = [301, 302, 303, 307, 308]
314 |
315 | Response.redirect = function(url, status) {
316 | if (redirectStatuses.indexOf(status) === -1) {
317 | throw new RangeError('Invalid status code')
318 | }
319 |
320 | return new Response(null, {status: status, headers: {location: url}})
321 | }
322 |
323 | self.Headers = Headers;
324 | self.Request = Request;
325 | self.Response = Response;
326 |
327 | self.fetch = function(input, init) {
328 | return new Promise(function(resolve, reject) {
329 | var request
330 | if (Request.prototype.isPrototypeOf(input) && !init) {
331 | request = input
332 | } else {
333 | request = new Request(input, init)
334 | }
335 |
336 | var xhr = new XMLHttpRequest()
337 |
338 | function responseURL() {
339 | if ('responseURL' in xhr) {
340 | return xhr.responseURL
341 | }
342 |
343 | // Avoid security warnings on getResponseHeader when not allowed by CORS
344 | if (/^X-Request-URL:/m.test(xhr.getAllResponseHeaders())) {
345 | return xhr.getResponseHeader('X-Request-URL')
346 | }
347 |
348 | return;
349 | }
350 |
351 | xhr.onload = function() {
352 | var status = (xhr.status === 1223) ? 204 : xhr.status
353 | if (status < 100 || status > 599) {
354 | reject(new TypeError('Network request failed'))
355 | return
356 | }
357 | var options = {
358 | status: status,
359 | statusText: xhr.statusText,
360 | headers: headers(xhr),
361 | url: responseURL()
362 | }
363 | var body = 'response' in xhr ? xhr.response : xhr.responseText;
364 | resolve(new Response(body, options))
365 | }
366 |
367 | xhr.onerror = function() {
368 | reject(new TypeError('Network request failed'))
369 | }
370 |
371 | xhr.open(request.method, request.url, true)
372 |
373 | if (request.credentials === 'include') {
374 | xhr.withCredentials = true
375 | }
376 |
377 | if ('responseType' in xhr && support.blob) {
378 | xhr.responseType = 'blob'
379 | }
380 |
381 | request.headers.forEach(function(value, name) {
382 | xhr.setRequestHeader(name, value)
383 | })
384 |
385 | xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit)
386 | })
387 | }
388 | self.fetch.polyfill = true
389 | })(typeof self !== 'undefined' ? self : this);
--------------------------------------------------------------------------------
/src/server.es6:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const configFile = __dirname + '/config.json',
4 | interfaceFolder = __dirname + '/dist';
5 |
6 | let express = require('express'),
7 | app = express(),
8 | router = express.Router(),
9 | proxy = require('express-http-proxy'),
10 | Url = require('url'),
11 | fs = require('fs'),
12 | path = require('path'),
13 | bodyParser = require('body-parser'),
14 | cookieParser = require('cookie-parser'),
15 | rimraf = require('rimraf'),
16 | winston = require('winston'),
17 | SandCastle = require('sandcastle').SandCastle,
18 | sandcastle = new SandCastle(),
19 | async = require('async'),
20 | config = require(configFile);
21 |
22 | let argv = require('minimist')(process.argv.slice(2));
23 |
24 | if (argv.location)
25 | console.log('mocknode installation directory: ', __dirname);
26 | else if (argv.export) {
27 | let fse = require('fs-extra'),
28 | tmp_path = path.join(__dirname, 'tmp'),
29 | tar = require('tar-fs');
30 |
31 | fse.emptyDirSync(tmp_path);
32 | fse.copySync(__dirname + '/stubs', tmp_path + '/stubs');
33 | fse.copySync(__dirname + '/config.json', tmp_path + '/config.json');
34 | tar.pack(tmp_path).pipe(fs.createWriteStream('mocknode-config.tar'));
35 | console.log('mocknode config has been exported to mocknode-config.tar');
36 | } else if (argv.import) {
37 | let tar = require('tar-fs');
38 | fs.createReadStream(argv.import).pipe(tar.extract(__dirname));
39 | console.log('configuration has been imported');
40 | } else {
41 |
42 | // App starts
43 | const port = process.env.PORT || argv.port || config.port;
44 |
45 | // Support for v0.x
46 | let assign = require('object-assign');
47 |
48 | app.use(bodyParser.urlencoded({extended: false}));
49 | app.use(bodyParser.json());
50 | app.use(cookieParser())
51 | app.use(router);
52 |
53 | let sleep = (milliseconds) => {
54 | var start = new Date().getTime();
55 | for (var i = 0; i < 1e7; i++) {
56 | if ((new Date().getTime() - start) > milliseconds){
57 | break;
58 | }
59 | }
60 | }
61 |
62 | let logger = {
63 | changelog: new (winston.Logger)({
64 | transports: [
65 | new (winston.transports.File)({
66 | name: 'changelog',
67 | filename: path.join(__dirname , 'logs', 'change.log')
68 | })
69 | ]
70 | }),
71 | accesslog: new (winston.Logger)({
72 | transports: [
73 | new (winston.transports.File)({
74 | name: 'accesslog',
75 | filename: path.join(__dirname , 'logs', 'access.log')
76 | })
77 | ]
78 | })
79 | }
80 |
81 | // Global headers and gloabl delay
82 | // currently this is only read from the config file
83 | // TODO: buid and interface for this.
84 | let globalHeaders = (globalConfig) => ((req, res, next) => {
85 | Object.keys(globalConfig.headers).map((header) => {
86 | res.setHeader(header, globalConfig.headers[header]);
87 | });
88 | if (globalConfig.delay)
89 | sleep(globalConfig.delay);
90 | return next();
91 | });
92 |
93 | // Returns a middleware which proxies to the target
94 | // TODO: exception handling for targets which are wrong,
95 | // currently the proxy fails and breaks the server.
96 | let assignNewProxy = (target) => (
97 | proxy(target, {
98 | forwardPath: (req, res) => Url.parse(req.originalUrl).path
99 | })
100 | );
101 |
102 | // Filesystems dont allow '/' in the names of folders / files,
103 | // Converting this character to a '!'
104 | let encodeRoutePath = (route) => route.replace(/\//g, "!");
105 | let decodeRoutePath = (route) => route.replace(/!/g, "/");
106 |
107 |
108 | // Creators and assigners - subtle abstraction : These return middlewares
109 | let createProxyRoute = (route, target) => router.use(route, assignNewProxy(target));
110 |
111 | let createStubRoute = (route, stub) => router.use(route, stubHandler(route, stub));
112 |
113 | let createDynamicStubRoute = (route, dynamicStub) => router.use(route, dynamicStubHandler(route, dynamicStub));
114 |
115 |
116 | let dynamicStubHandler = (_route, _name) => {
117 | let dynamicObj;
118 | configLoop:
119 | for (let i = 0; i < config.routes.length; i++) {
120 | if (config.routes[i].route == _route) {
121 | for (var j = 0; j < config.routes[i].dynamicStubs.length; j++) {
122 | if (config.routes[i].dynamicStubs[j].name == _name) {
123 | dynamicObj = config.routes[i].dynamicStubs[j];
124 | break configLoop;
125 | }
126 | };
127 | };
128 | }
129 | return dynamicStubRequestHandler(_route, dynamicObj);
130 | }
131 |
132 | // Middleware which returns a stub
133 | let stubHandler = (route, stub) => (req, res, next) => res.sendFile(path.join(__dirname, 'stubs', encodeRoutePath(route), stub));
134 |
135 | // Middleware which handles the dynamic stubs conditions
136 | // Uses sandcastle to execute evals on a node sanbox.
137 | // The req object parameters are injected into the execution
138 | // runtime, so the eval expressions can access the req object.
139 | let dynamicStubRequestHandler = (_route, _stub) => {
140 | return (req, res) => {
141 | let returnedStub = _stub.defaultStub,
142 | continueLoop = true,
143 | count = 0;
144 |
145 | if (_stub.conditions.length) {
146 | async.whilst(
147 | () => (count < _stub.conditions.length) && continueLoop,
148 | (callback) => {
149 | let script = sandcastle.createScript(`exports.main = function() {
150 | try {
151 | if (${_stub.conditions[count].eval})
152 | exit('${_stub.conditions[count].stub}')
153 | else
154 | exit(false)
155 | } catch(e) {
156 | exit(false)
157 | }
158 | }`);
159 |
160 | count++;
161 |
162 | script.on('exit', function(err, output) {
163 | if (output) {
164 | continueLoop = false
165 | returnedStub = output
166 | }
167 | callback();
168 | });
169 |
170 | script.run({
171 | req: assign({}, {
172 | baseURL: req.baseURL,
173 | body: req.body,
174 | cookies: req.cookies,
175 | headers: req.headers,
176 | hostname: req.hostname,
177 | ip: req.ip,
178 | ips: req.ips,
179 | method: req.method,
180 | originalUrl: req.originalUrl,
181 | params: req.params,
182 | path: req.path,
183 | protocol: req.protocol,
184 | query: req.query,
185 | route: req.route,
186 | signedCookies: req.signedCookies,
187 | stale: req.stale,
188 | subdomains: req.subdomains,
189 | xhr: req.xhr
190 | })
191 | });
192 | },
193 | (err) => {
194 | res.sendFile(path.join(__dirname, 'stubs', encodeRoutePath(_route), returnedStub));
195 | }
196 | );
197 | }
198 | else {
199 | res.sendFile(path.join(__dirname, 'stubs', encodeRoutePath(_route), returnedStub));
200 | }
201 | }
202 | }
203 |
204 | // Create and update a route
205 | // TODO: RE-Implement this and make it pretty
206 | let updateRoute = (_req) => {
207 | let matchCount = 0;
208 | if (_req.old_route != _req.route) {
209 | if (_req.old_route)
210 | deleteroute(_req.old_route);
211 | delete _req.old_route;
212 | } else {
213 | delete _req.old_route;
214 | for (let layer of router.stack) {
215 | let match = _req.route.match(layer.regexp);
216 | if (match && match[0] == _req.route) {
217 | layer.handle = (_req.handle == "stub")
218 | ? stubHandler(_req.route, _req.stub)
219 | : (_req.handle == "proxy")
220 | ? assignNewProxy(_req.proxy)
221 | : dynamicStubHandler(_req.route, _req.dynamicStub);
222 | for (var i = 0; i < config.routes.length; i++) {
223 | if (config.routes[i].route == _req.route) {
224 | config.routes[i] = assign({}, config.routes[i], _req);
225 | }
226 | }
227 | matchCount++;
228 | }
229 | }
230 | }
231 | if (matchCount == 0) {
232 | if (_req.handle == "stub") {
233 | createStubRoute(_req.route, _req.stub);
234 | } else if (_req.handle == "proxy") {
235 | createProxyRoute(_req.route, _req.proxy);
236 | } else if (_req.handle == "dynamicStub") {
237 | createDynamicStubRoute(_req.route, _req.dynamicStub);
238 | }
239 | config.routes.push(assign({}, _req, {stubs:[], dynamicStubs: []}));
240 | let dir = path.join(__dirname, 'stubs', encodeRoutePath(_req.route));
241 | if (!fs.existsSync(dir))
242 | fs.mkdirSync(dir)
243 | }
244 | fs.writeFile(configFile, JSON.stringify(config, null, 2));
245 | }
246 |
247 | // Create and update for stubs
248 | let updateStubs = (_req) => {
249 | let matchCount = 0;
250 | routeLoop:
251 | for (var i = 0; i < config.routes.length; i++) {
252 | if (config.routes[i].route == _req.route) {
253 | for (var j = 0; j < config.routes[i].stubs.length; j++) {
254 | if(config.routes[i].stubs[j].name == _req.oldname || config.routes[i].stubs[j].name == _req.name) {
255 | config.routes[i].stubs[j].name = _req.name;
256 | config.routes[i].stubs[j].description = _req.description;
257 | fs.writeFileSync(path.join(__dirname, 'stubs', encodeRoutePath(_req.route), _req.name), _req.content);
258 | if (_req.oldname) {
259 | fs.rename(
260 | path.join(__dirname, 'stubs', encodeRoutePath(_req.route), _req.oldname),
261 | path.join(__dirname, 'stubs', encodeRoutePath(_req.route), _req.name)
262 | )
263 | }
264 | matchCount++;
265 | break routeLoop;
266 | }
267 | }
268 | if (matchCount == 0) {
269 | fs.writeFile(path.join(__dirname, 'stubs', encodeRoutePath(_req.route), _req.name), _req.content);
270 | config.routes[i].stubs.push({name: _req.name, description: _req.description});
271 | }
272 | }
273 | }
274 | fs.writeFile(configFile, JSON.stringify(config, null, 2));
275 | }
276 |
277 | // Create and update for dynamic stubs, this is very similar
278 | // to that of stubs but there is no file associated, only the
279 | // config is updated.
280 | let updateDynamicStubs = (_req) => {
281 | let req = assign({}, _req);
282 | let matchCount = 0,
283 | oldname = req.oldname,
284 | route = req.route;
285 | delete req.oldname;
286 | delete req.route;
287 | routeLoop:
288 | for (var i = 0; i < config.routes.length; i++) {
289 | if (config.routes[i].route == route) {
290 | for (var j = 0; j < config.routes[i].dynamicStubs.length; j++) {
291 | if(config.routes[i].dynamicStubs[j].name == oldname || config.routes[i].dynamicStubs[j].name == req.name) {
292 | config.routes[i].dynamicStubs[j] = req;
293 | matchCount++;
294 | break routeLoop;
295 | }
296 | }
297 | if (matchCount == 0) {
298 | config.routes[i].dynamicStubs.push(req);
299 | }
300 | }
301 | }
302 | fs.writeFile(configFile, JSON.stringify(config, null, 2));
303 | }
304 |
305 | // Delete a route:
306 | // 1. Remove it from the router stack
307 | // 2. Remote it from config
308 | // 3. Delete the stubs/route folder
309 | let deleteroute = (_route) => {
310 | let index = router.stack.map((layer) => _route.match(layer.regexp))
311 | .reduce((index, item, i) => !!item ? i : index, 0);
312 | if (index > -1) {
313 | router.stack.splice(index, 1);
314 | rimraf(path.join(__dirname, 'stubs', encodeRoutePath(_route)), () => {});
315 | }
316 | let newRoutes = config.routes.filter((route) => route.route != _route);
317 | config = assign(config, {routes: newRoutes});
318 | fs.writeFile(configFile, JSON.stringify(config, null, 2));
319 | }
320 |
321 | // Delete a stub:
322 | // 1. Delete the file
323 | // 2. Remove the entry from config file
324 | let deletestub = (_route, _stub) => {
325 | fs.unlinkSync(path.join(__dirname, 'stubs', encodeRoutePath(_route), _stub));
326 |
327 | routeLoop:
328 | for (var i = 0; i < config.routes.length; i++) {
329 | if (config.routes[i].route == _route) {
330 | for (var j = 0; j < config.routes[i].stubs.length; j++) {
331 | if(config.routes[i].stubs[j].name == _stub) {
332 | config.routes[i].stubs.splice(j, 1);
333 | break routeLoop;
334 | }
335 | }
336 | }
337 | }
338 | fs.writeFile(configFile, JSON.stringify(config, null, 2));
339 | }
340 |
341 | // Delete a dynamic stub:
342 | // 1. Remove the entry from config file
343 | let deleteDynamicstub = (_route, _stub) => {
344 |
345 | routeLoop:
346 | for (var i = 0; i < config.routes.length; i++) {
347 | if (config.routes[i].route == _route) {
348 | for (var j = 0; j < config.routes[i].dynamicStubs.length; j++) {
349 | if(config.routes[i].dynamicStubs[j].name == _stub) {
350 | config.routes[i].dynamicStubs.splice(j, 1);
351 | break routeLoop;
352 | }
353 | }
354 | }
355 | }
356 | fs.writeFile(configFile, JSON.stringify(config, null, 2));
357 | }
358 |
359 | // Expects that the dynamic stub properties are updated
360 | // checks if the route is using this dynamic stub and updates its layer.handle
361 | let updateDynamicRoutes = (_route, _dynamicStub) => {
362 | for (var i = 0; i < config.routes.length; i++) {
363 | if ( (_route == config.routes[i].route)
364 | && (_dynamicStub == config.routes[i].dynamicStub)
365 | && ("dynamicStub" == config.routes[i].handle) ) {
366 |
367 | for (let layer of router.stack) {
368 | let match = config.routes[i].route.match(layer.regexp);
369 | if (match && match[0] == config.routes[i].route) {
370 | layer.handle = dynamicStubHandler(_route, _dynamicStub);
371 | }
372 | }
373 |
374 | }
375 | }
376 | }
377 |
378 | let logRequest = (_req, _type, _method) => {
379 | logger[_type][_method]({
380 | 'route': _req.path,
381 | 'query_strings': JSON.stringify(_req.query),
382 | 'request_body': JSON.stringify(_req.body),
383 | 'ip': _req.ip
384 | });
385 | }
386 |
387 | // Logs all requests which do not start with '/mocknode/'
388 | router.use('/', (req, res, next) => {
389 | if (/^(?!\/mocknode\/)/.test(req.originalUrl) && /^(?!\/favicon.ico)/.test(req.originalUrl))
390 | logRequest(req, 'accesslog', 'info');
391 | next();
392 | });
393 |
394 | // Logs requests that change the configuration of mocknode
395 | router.use('/mocknode/api', (req, res, next) => {
396 | let logList = [ '/modifyroute', '/deleteroute', '/modifystub', '/deletestub',
397 | '/modifydynamicstub', '/deletedynamicstub'];
398 | if (logList.indexOf(req.path) > -1)
399 | logRequest(req, 'changelog', 'info');
400 | next();
401 | });
402 |
403 |
404 | router.use('/mocknode', express.static(interfaceFolder));
405 | router.use('/mocknode/api/config', (req, res) => res.json(config));
406 |
407 | router.use('/mocknode/api/stubconfig', (req, res) => res.json(stubConfig));
408 |
409 | router.use('/mocknode/api/logs', (req, res) => {
410 | res.sendFile(path.join(__dirname, 'logs', req.query.name));
411 | })
412 |
413 | router.use('/mocknode/api/getstub', (req, res) => {
414 | res.sendFile(path.join(__dirname, 'stubs', encodeRoutePath(req.query.route), req.query.name));
415 | });
416 |
417 | router.use('/mocknode/api/modifyroute', (req, res, next) => {
418 | updateRoute(req.body);
419 | res.send({success: true});
420 | });
421 |
422 | router.use('/mocknode/api/deleteroute', (req, res, next) => {
423 | deleteroute(req.query.route);
424 | res.send({success: true});
425 | });
426 |
427 | router.use('/mocknode/api/modifystub', (req, res) => {
428 | updateStubs(req.body);
429 | res.send({success: true});
430 | });
431 |
432 | router.use('/mocknode/api/deletestub', (req, res) => {
433 | deletestub(req.query.route, req.query.name);
434 | res.send({success: true});
435 | });
436 |
437 | router.use('/mocknode/api/modifydynamicstub', (req, res) => {
438 | updateDynamicStubs(req.body);
439 | updateDynamicRoutes(req.body.route, req.body.name);
440 | res.send({success: true});
441 | });
442 |
443 | router.use('/mocknode/api/deletedynamicstub', (req, res) => {
444 | deleteDynamicstub(req.query.route, req.query.name);
445 | res.send({success: true});
446 | });
447 |
448 | router.use(globalHeaders(config.global));
449 |
450 | config.routes.filter((configObj) => configObj.handle == "proxy")
451 | .map((configObj) => createProxyRoute(configObj.route, configObj.proxy));
452 |
453 | config.routes.filter((configObj) => configObj.handle == "stub")
454 | .map((configObj) => createStubRoute(configObj.route, configObj.stub));
455 |
456 | config.routes.filter((configObj) => configObj.handle == "dynamicStub")
457 | .map((configObj) => createDynamicStubRoute(configObj.route, configObj.dynamicStub));
458 |
459 | app.listen( port );
460 | console.log( "Mocknode started on port: " + port );
461 | console.log( "open 'http://localhost:" + port + "/mocknode' in your browser to configure mocknode" );
462 |
463 | // App ends
464 | }
465 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var configFile = __dirname + '/config.json',
4 | interfaceFolder = __dirname + '/dist';
5 |
6 | var express = require('express'),
7 | app = express(),
8 | router = express.Router(),
9 | proxy = require('express-http-proxy'),
10 | Url = require('url'),
11 | fs = require('fs'),
12 | path = require('path'),
13 | bodyParser = require('body-parser'),
14 | cookieParser = require('cookie-parser'),
15 | rimraf = require('rimraf'),
16 | winston = require('winston'),
17 | SandCastle = require('sandcastle').SandCastle,
18 | sandcastle = new SandCastle(),
19 | async = require('async'),
20 | config = require(configFile);
21 |
22 | var argv = require('minimist')(process.argv.slice(2));
23 |
24 | if (argv.location) console.log('mocknode installation directory: ', __dirname);else if (argv.export) {
25 | var fse = require('fs-extra'),
26 | tmp_path = path.join(__dirname, 'tmp'),
27 | tar = require('tar-fs');
28 |
29 | fse.emptyDirSync(tmp_path);
30 | fse.copySync(__dirname + '/stubs', tmp_path + '/stubs');
31 | fse.copySync(__dirname + '/config.json', tmp_path + '/config.json');
32 | tar.pack(tmp_path).pipe(fs.createWriteStream('mocknode-config.tar'));
33 | console.log('mocknode config has been exported to mocknode-config.tar');
34 | } else if (argv.import) {
35 | var _tar = require('tar-fs');
36 | fs.createReadStream(argv.import).pipe(_tar.extract(__dirname));
37 | console.log('configuration has been imported');
38 | } else {
39 | (function () {
40 |
41 | // App starts
42 | var port = process.env.PORT || argv.port || config.port;
43 |
44 | // Support for v0.x
45 | var assign = require('object-assign');
46 |
47 | app.use(bodyParser.urlencoded({ extended: false }));
48 | app.use(bodyParser.json());
49 | app.use(cookieParser());
50 | app.use(router);
51 |
52 | var sleep = function sleep(milliseconds) {
53 | var start = new Date().getTime();
54 | for (var i = 0; i < 1e7; i++) {
55 | if (new Date().getTime() - start > milliseconds) {
56 | break;
57 | }
58 | }
59 | };
60 |
61 | var logger = {
62 | changelog: new winston.Logger({
63 | transports: [new winston.transports.File({
64 | name: 'changelog',
65 | filename: path.join(__dirname, 'logs', 'change.log')
66 | })]
67 | }),
68 | accesslog: new winston.Logger({
69 | transports: [new winston.transports.File({
70 | name: 'accesslog',
71 | filename: path.join(__dirname, 'logs', 'access.log')
72 | })]
73 | })
74 | };
75 |
76 | // Global headers and gloabl delay
77 | // currently this is only read from the config file
78 | // TODO: buid and interface for this.
79 | var globalHeaders = function globalHeaders(globalConfig) {
80 | return function (req, res, next) {
81 | Object.keys(globalConfig.headers).map(function (header) {
82 | res.setHeader(header, globalConfig.headers[header]);
83 | });
84 | if (globalConfig.delay) sleep(globalConfig.delay);
85 | return next();
86 | };
87 | };
88 |
89 | // Returns a middleware which proxies to the target
90 | // TODO: exception handling for targets which are wrong,
91 | // currently the proxy fails and breaks the server.
92 | var assignNewProxy = function assignNewProxy(target) {
93 | return proxy(target, {
94 | forwardPath: function forwardPath(req, res) {
95 | return Url.parse(req.originalUrl).path;
96 | }
97 | });
98 | };
99 |
100 | // Filesystems dont allow '/' in the names of folders / files,
101 | // Converting this character to a '!'
102 | var encodeRoutePath = function encodeRoutePath(route) {
103 | return route.replace(/\//g, "!");
104 | };
105 | var decodeRoutePath = function decodeRoutePath(route) {
106 | return route.replace(/!/g, "/");
107 | };
108 |
109 | // Creators and assigners - subtle abstraction : These return middlewares
110 | var createProxyRoute = function createProxyRoute(route, target) {
111 | return router.use(route, assignNewProxy(target));
112 | };
113 |
114 | var createStubRoute = function createStubRoute(route, stub) {
115 | return router.use(route, stubHandler(route, stub));
116 | };
117 |
118 | var createDynamicStubRoute = function createDynamicStubRoute(route, dynamicStub) {
119 | return router.use(route, dynamicStubHandler(route, dynamicStub));
120 | };
121 |
122 | var dynamicStubHandler = function dynamicStubHandler(_route, _name) {
123 | var dynamicObj = void 0;
124 | configLoop: for (var i = 0; i < config.routes.length; i++) {
125 | if (config.routes[i].route == _route) {
126 | for (var j = 0; j < config.routes[i].dynamicStubs.length; j++) {
127 | if (config.routes[i].dynamicStubs[j].name == _name) {
128 | dynamicObj = config.routes[i].dynamicStubs[j];
129 | break configLoop;
130 | }
131 | };
132 | };
133 | }
134 | return dynamicStubRequestHandler(_route, dynamicObj);
135 | };
136 |
137 | // Middleware which returns a stub
138 | var stubHandler = function stubHandler(route, stub) {
139 | return function (req, res, next) {
140 | return res.sendFile(path.join(__dirname, 'stubs', encodeRoutePath(route), stub));
141 | };
142 | };
143 |
144 | // Middleware which handles the dynamic stubs conditions
145 | // Uses sandcastle to execute evals on a node sanbox.
146 | // The req object parameters are injected into the execution
147 | // runtime, so the eval expressions can access the req object.
148 | var dynamicStubRequestHandler = function dynamicStubRequestHandler(_route, _stub) {
149 | return function (req, res) {
150 | var returnedStub = _stub.defaultStub,
151 | continueLoop = true,
152 | count = 0;
153 |
154 | if (_stub.conditions.length) {
155 | async.whilst(function () {
156 | return count < _stub.conditions.length && continueLoop;
157 | }, function (callback) {
158 | var script = sandcastle.createScript('exports.main = function() {\n try {\n if (' + _stub.conditions[count].eval + ')\n exit(\'' + _stub.conditions[count].stub + '\')\n else\n exit(false)\n } catch(e) {\n exit(false)\n }\n }');
159 |
160 | count++;
161 |
162 | script.on('exit', function (err, output) {
163 | if (output) {
164 | continueLoop = false;
165 | returnedStub = output;
166 | }
167 | callback();
168 | });
169 |
170 | script.run({
171 | req: assign({}, {
172 | baseURL: req.baseURL,
173 | body: req.body,
174 | cookies: req.cookies,
175 | headers: req.headers,
176 | hostname: req.hostname,
177 | ip: req.ip,
178 | ips: req.ips,
179 | method: req.method,
180 | originalUrl: req.originalUrl,
181 | params: req.params,
182 | path: req.path,
183 | protocol: req.protocol,
184 | query: req.query,
185 | route: req.route,
186 | signedCookies: req.signedCookies,
187 | stale: req.stale,
188 | subdomains: req.subdomains,
189 | xhr: req.xhr
190 | })
191 | });
192 | }, function (err) {
193 | res.sendFile(path.join(__dirname, 'stubs', encodeRoutePath(_route), returnedStub));
194 | });
195 | } else {
196 | res.sendFile(path.join(__dirname, 'stubs', encodeRoutePath(_route), returnedStub));
197 | }
198 | };
199 | };
200 |
201 | // Create and update a route
202 | // TODO: RE-Implement this and make it pretty
203 | var updateRoute = function updateRoute(_req) {
204 | var matchCount = 0;
205 | if (_req.old_route != _req.route) {
206 | if (_req.old_route) deleteroute(_req.old_route);
207 | delete _req.old_route;
208 | } else {
209 | delete _req.old_route;
210 | var _iteratorNormalCompletion = true;
211 | var _didIteratorError = false;
212 | var _iteratorError = undefined;
213 |
214 | try {
215 | for (var _iterator = router.stack[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
216 | var layer = _step.value;
217 |
218 | var match = _req.route.match(layer.regexp);
219 | if (match && match[0] == _req.route) {
220 | layer.handle = _req.handle == "stub" ? stubHandler(_req.route, _req.stub) : _req.handle == "proxy" ? assignNewProxy(_req.proxy) : dynamicStubHandler(_req.route, _req.dynamicStub);
221 | for (var i = 0; i < config.routes.length; i++) {
222 | if (config.routes[i].route == _req.route) {
223 | config.routes[i] = assign({}, config.routes[i], _req);
224 | }
225 | }
226 | matchCount++;
227 | }
228 | }
229 | } catch (err) {
230 | _didIteratorError = true;
231 | _iteratorError = err;
232 | } finally {
233 | try {
234 | if (!_iteratorNormalCompletion && _iterator.return) {
235 | _iterator.return();
236 | }
237 | } finally {
238 | if (_didIteratorError) {
239 | throw _iteratorError;
240 | }
241 | }
242 | }
243 | }
244 | if (matchCount == 0) {
245 | if (_req.handle == "stub") {
246 | createStubRoute(_req.route, _req.stub);
247 | } else if (_req.handle == "proxy") {
248 | createProxyRoute(_req.route, _req.proxy);
249 | } else if (_req.handle == "dynamicStub") {
250 | createDynamicStubRoute(_req.route, _req.dynamicStub);
251 | }
252 | config.routes.push(assign({}, _req, { stubs: [], dynamicStubs: [] }));
253 | var dir = path.join(__dirname, 'stubs', encodeRoutePath(_req.route));
254 | if (!fs.existsSync(dir)) fs.mkdirSync(dir);
255 | }
256 | fs.writeFile(configFile, JSON.stringify(config, null, 2));
257 | };
258 |
259 | // Create and update for stubs
260 | var updateStubs = function updateStubs(_req) {
261 | var matchCount = 0;
262 | routeLoop: for (var i = 0; i < config.routes.length; i++) {
263 | if (config.routes[i].route == _req.route) {
264 | for (var j = 0; j < config.routes[i].stubs.length; j++) {
265 | if (config.routes[i].stubs[j].name == _req.oldname || config.routes[i].stubs[j].name == _req.name) {
266 | config.routes[i].stubs[j].name = _req.name;
267 | config.routes[i].stubs[j].description = _req.description;
268 | fs.writeFileSync(path.join(__dirname, 'stubs', encodeRoutePath(_req.route), _req.name), _req.content);
269 | if (_req.oldname) {
270 | fs.rename(path.join(__dirname, 'stubs', encodeRoutePath(_req.route), _req.oldname), path.join(__dirname, 'stubs', encodeRoutePath(_req.route), _req.name));
271 | }
272 | matchCount++;
273 | break routeLoop;
274 | }
275 | }
276 | if (matchCount == 0) {
277 | fs.writeFile(path.join(__dirname, 'stubs', encodeRoutePath(_req.route), _req.name), _req.content);
278 | config.routes[i].stubs.push({ name: _req.name, description: _req.description });
279 | }
280 | }
281 | }
282 | fs.writeFile(configFile, JSON.stringify(config, null, 2));
283 | };
284 |
285 | // Create and update for dynamic stubs, this is very similar
286 | // to that of stubs but there is no file associated, only the
287 | // config is updated.
288 | var updateDynamicStubs = function updateDynamicStubs(_req) {
289 | var req = assign({}, _req);
290 | var matchCount = 0,
291 | oldname = req.oldname,
292 | route = req.route;
293 | delete req.oldname;
294 | delete req.route;
295 | routeLoop: for (var i = 0; i < config.routes.length; i++) {
296 | if (config.routes[i].route == route) {
297 | for (var j = 0; j < config.routes[i].dynamicStubs.length; j++) {
298 | if (config.routes[i].dynamicStubs[j].name == oldname || config.routes[i].dynamicStubs[j].name == req.name) {
299 | config.routes[i].dynamicStubs[j] = req;
300 | matchCount++;
301 | break routeLoop;
302 | }
303 | }
304 | if (matchCount == 0) {
305 | config.routes[i].dynamicStubs.push(req);
306 | }
307 | }
308 | }
309 | fs.writeFile(configFile, JSON.stringify(config, null, 2));
310 | };
311 |
312 | // Delete a route:
313 | // 1. Remove it from the router stack
314 | // 2. Remote it from config
315 | // 3. Delete the stubs/route folder
316 | var deleteroute = function deleteroute(_route) {
317 | var index = router.stack.map(function (layer) {
318 | return _route.match(layer.regexp);
319 | }).reduce(function (index, item, i) {
320 | return !!item ? i : index;
321 | }, 0);
322 | if (index > -1) {
323 | router.stack.splice(index, 1);
324 | rimraf(path.join(__dirname, 'stubs', encodeRoutePath(_route)), function () {});
325 | }
326 | var newRoutes = config.routes.filter(function (route) {
327 | return route.route != _route;
328 | });
329 | config = assign(config, { routes: newRoutes });
330 | fs.writeFile(configFile, JSON.stringify(config, null, 2));
331 | };
332 |
333 | // Delete a stub:
334 | // 1. Delete the file
335 | // 2. Remove the entry from config file
336 | var deletestub = function deletestub(_route, _stub) {
337 | fs.unlinkSync(path.join(__dirname, 'stubs', encodeRoutePath(_route), _stub));
338 |
339 | routeLoop: for (var i = 0; i < config.routes.length; i++) {
340 | if (config.routes[i].route == _route) {
341 | for (var j = 0; j < config.routes[i].stubs.length; j++) {
342 | if (config.routes[i].stubs[j].name == _stub) {
343 | config.routes[i].stubs.splice(j, 1);
344 | break routeLoop;
345 | }
346 | }
347 | }
348 | }
349 | fs.writeFile(configFile, JSON.stringify(config, null, 2));
350 | };
351 |
352 | // Delete a dynamic stub:
353 | // 1. Remove the entry from config file
354 | var deleteDynamicstub = function deleteDynamicstub(_route, _stub) {
355 |
356 | routeLoop: for (var i = 0; i < config.routes.length; i++) {
357 | if (config.routes[i].route == _route) {
358 | for (var j = 0; j < config.routes[i].dynamicStubs.length; j++) {
359 | if (config.routes[i].dynamicStubs[j].name == _stub) {
360 | config.routes[i].dynamicStubs.splice(j, 1);
361 | break routeLoop;
362 | }
363 | }
364 | }
365 | }
366 | fs.writeFile(configFile, JSON.stringify(config, null, 2));
367 | };
368 |
369 | // Expects that the dynamic stub properties are updated
370 | // checks if the route is using this dynamic stub and updates its layer.handle
371 | var updateDynamicRoutes = function updateDynamicRoutes(_route, _dynamicStub) {
372 | for (var i = 0; i < config.routes.length; i++) {
373 | if (_route == config.routes[i].route && _dynamicStub == config.routes[i].dynamicStub && "dynamicStub" == config.routes[i].handle) {
374 | var _iteratorNormalCompletion2 = true;
375 | var _didIteratorError2 = false;
376 | var _iteratorError2 = undefined;
377 |
378 | try {
379 |
380 | for (var _iterator2 = router.stack[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
381 | var layer = _step2.value;
382 |
383 | var match = config.routes[i].route.match(layer.regexp);
384 | if (match && match[0] == config.routes[i].route) {
385 | layer.handle = dynamicStubHandler(_route, _dynamicStub);
386 | }
387 | }
388 | } catch (err) {
389 | _didIteratorError2 = true;
390 | _iteratorError2 = err;
391 | } finally {
392 | try {
393 | if (!_iteratorNormalCompletion2 && _iterator2.return) {
394 | _iterator2.return();
395 | }
396 | } finally {
397 | if (_didIteratorError2) {
398 | throw _iteratorError2;
399 | }
400 | }
401 | }
402 | }
403 | }
404 | };
405 |
406 | var logRequest = function logRequest(_req, _type, _method) {
407 | logger[_type][_method]({
408 | 'route': _req.path,
409 | 'query_strings': JSON.stringify(_req.query),
410 | 'request_body': JSON.stringify(_req.body),
411 | 'ip': _req.ip
412 | });
413 | };
414 |
415 | // Logs all requests which do not start with '/mocknode/'
416 | router.use('/', function (req, res, next) {
417 | if (/^(?!\/mocknode\/)/.test(req.originalUrl) && /^(?!\/favicon.ico)/.test(req.originalUrl)) logRequest(req, 'accesslog', 'info');
418 | next();
419 | });
420 |
421 | // Logs requests that change the configuration of mocknode
422 | router.use('/mocknode/api', function (req, res, next) {
423 | var logList = ['/modifyroute', '/deleteroute', '/modifystub', '/deletestub', '/modifydynamicstub', '/deletedynamicstub'];
424 | if (logList.indexOf(req.path) > -1) logRequest(req, 'changelog', 'info');
425 | next();
426 | });
427 |
428 | router.use('/mocknode', express.static(interfaceFolder));
429 | router.use('/mocknode/api/config', function (req, res) {
430 | return res.json(config);
431 | });
432 |
433 | router.use('/mocknode/api/stubconfig', function (req, res) {
434 | return res.json(stubConfig);
435 | });
436 |
437 | router.use('/mocknode/api/logs', function (req, res) {
438 | res.sendFile(path.join(__dirname, 'logs', req.query.name));
439 | });
440 |
441 | router.use('/mocknode/api/getstub', function (req, res) {
442 | res.sendFile(path.join(__dirname, 'stubs', encodeRoutePath(req.query.route), req.query.name));
443 | });
444 |
445 | router.use('/mocknode/api/modifyroute', function (req, res, next) {
446 | updateRoute(req.body);
447 | res.send({ success: true });
448 | });
449 |
450 | router.use('/mocknode/api/deleteroute', function (req, res, next) {
451 | deleteroute(req.query.route);
452 | res.send({ success: true });
453 | });
454 |
455 | router.use('/mocknode/api/modifystub', function (req, res) {
456 | updateStubs(req.body);
457 | res.send({ success: true });
458 | });
459 |
460 | router.use('/mocknode/api/deletestub', function (req, res) {
461 | deletestub(req.query.route, req.query.name);
462 | res.send({ success: true });
463 | });
464 |
465 | router.use('/mocknode/api/modifydynamicstub', function (req, res) {
466 | updateDynamicStubs(req.body);
467 | updateDynamicRoutes(req.body.route, req.body.name);
468 | res.send({ success: true });
469 | });
470 |
471 | router.use('/mocknode/api/deletedynamicstub', function (req, res) {
472 | deleteDynamicstub(req.query.route, req.query.name);
473 | res.send({ success: true });
474 | });
475 |
476 | router.use(globalHeaders(config.global));
477 |
478 | config.routes.filter(function (configObj) {
479 | return configObj.handle == "proxy";
480 | }).map(function (configObj) {
481 | return createProxyRoute(configObj.route, configObj.proxy);
482 | });
483 |
484 | config.routes.filter(function (configObj) {
485 | return configObj.handle == "stub";
486 | }).map(function (configObj) {
487 | return createStubRoute(configObj.route, configObj.stub);
488 | });
489 |
490 | config.routes.filter(function (configObj) {
491 | return configObj.handle == "dynamicStub";
492 | }).map(function (configObj) {
493 | return createDynamicStubRoute(configObj.route, configObj.dynamicStub);
494 | });
495 |
496 | app.listen(port);
497 | console.log("Mocknode started on port: " + port);
498 | console.log("open 'http://localhost:" + port + "/mocknode' in your browser to configure mocknode");
499 |
500 | // App ends
501 | })();
502 | }
503 |
504 |
--------------------------------------------------------------------------------
/dist/css/bootstrap-theme.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap v3.3.6 (http://getbootstrap.com)
3 | * Copyright 2011-2015 Twitter, Inc.
4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
5 | */.btn-danger,.btn-default,.btn-info,.btn-primary,.btn-success,.btn-warning{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-success.active,.btn-success:active,.btn-warning.active,.btn-warning:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-danger.disabled,.btn-danger[disabled],.btn-default.disabled,.btn-default[disabled],.btn-info.disabled,.btn-info[disabled],.btn-primary.disabled,.btn-primary[disabled],.btn-success.disabled,.btn-success[disabled],.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-danger,fieldset[disabled] .btn-default,fieldset[disabled] .btn-info,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-success,fieldset[disabled] .btn-warning{-webkit-box-shadow:none;box-shadow:none}.btn-danger .badge,.btn-default .badge,.btn-info .badge,.btn-primary .badge,.btn-success .badge,.btn-warning .badge{text-shadow:none}.btn.active,.btn:active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:focus,.btn-default:hover{background-color:#e0e0e0;background-position:0 -15px}.btn-default.active,.btn-default:active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-o-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#265a88));background-image:linear-gradient(to bottom,#337ab7 0,#265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#245580}.btn-primary:focus,.btn-primary:hover{background-color:#265a88;background-position:0 -15px}.btn-primary.active,.btn-primary:active{background-color:#265a88;border-color:#245580}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#265a88;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:focus,.btn-success:hover{background-color:#419641;background-position:0 -15px}.btn-success.active,.btn-success:active{background-color:#419641;border-color:#3e8f3e}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:focus,.btn-info:hover{background-color:#2aabd2;background-position:0 -15px}.btn-info.active,.btn-info:active{background-color:#2aabd2;border-color:#28a4c9}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:focus,.btn-warning:hover{background-color:#eb9316;background-position:0 -15px}.btn-warning.active,.btn-warning:active{background-color:#eb9316;border-color:#e38d13}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:focus,.btn-danger:hover{background-color:#c12e2a;background-position:0 -15px}.btn-danger.active,.btn-danger:active{background-color:#c12e2a;border-color:#b92c28}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#c12e2a;background-image:none}.img-thumbnail,.thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{background-color:#2e6da4;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:focus .badge,.list-group-item.active:hover .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)}
6 | /*# sourceMappingURL=bootstrap-theme.min.css.map */
--------------------------------------------------------------------------------
/dist/fonts/glyphicons-halflings-regular.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------