├── .gitignore ├── README.md ├── anymocker.jpg ├── gen.js ├── index.js ├── package.json ├── parser.js ├── record.js ├── rule.js └── tools.js /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | ._* 3 | .DS_Store 4 | .git 5 | .hg 6 | .lock-wscript 7 | .svn 8 | .wafpickle-* 9 | CVS 10 | npm-debug.log 11 | logs 12 | *.log 13 | pids 14 | *.pid 15 | *.seed 16 | lib-cov 17 | coverage 18 | .grunt 19 | node_modules 20 | .lock-wscript -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | anymocker 2 | ============ 3 | 4 | A toy mock server based on [Anyproxy](https://github.com/alibaba/anyproxy), install by typing 5 | 6 | ``` 7 | npm install anymocker -g 8 | ``` 9 | 10 | Simply illustrate: 11 | 12 | ![image](https://github.com/fenfenzhong92/anymocker/raw/master/anymocker.jpg) 13 | 14 | 15 | Before all 16 | ------------- 17 | * this is a tool used for fine tuning API json response, based on [Anyproxy](https://github.com/alibaba/anyproxy), so please follow up anyproxy instruction to setup the environment, that's the basis of rules 18 | 19 | * use [jsonpath](https://www.npmjs.com/package/jsonpath) to locate a field in API response 20 | 21 | 22 | Rule detail 23 | ------------- 24 | 25 | #### here are some examples about defining a rule 26 | * replace all API's response(json format), make all `name` field equal to `github`, all `text` field equal to `anymocker`, that's a global rule 27 | 28 | ``` 29 | $ anymocker -s save -m $..name=github $..text=anymocker 30 | ``` 31 | * replace api `xxx/xxx` response(json format), make all `name` field equal to `github`, delete all the `text` field, and then inject a field `title`, valued `devil`, that's a api rule 32 | 33 | ``` 34 | $ anymocker -s save -a xxx/xxx -m $..name=github -d $..text -i $.title=devil 35 | ``` 36 | * when both api and global rules are specified, should notice that for the same field, api will override global, below the api `xxx/xxx` will mock `text` field as `github`, and for other request(global) will mock `text` field as `gitlab` 37 | 38 | ``` 39 | $ anymocker -s save -a xxx/xxx -m $..name=github -d $..text -i $.title=devil -m $..name=gitlab 40 | ``` 41 | 42 | Fuzzy 43 | ------------- 44 | 45 | #### if you don't want to fake data with so much pain, you can just put a `FUZZ` as the value 46 | * only number, string and boolean can be fuzzy 47 | * can randomly return null 48 | * for number, there are chances to amplify and shrink by multiple 49 | * for string, there are chances to get an empty '' 50 | 51 | ``` 52 | $ anymocker -s save -a xxx/xxx -m $..name=FUZZ -d $..text -i $.title=devil -m $..name=gitlab $..text=FUZZ 53 | ``` 54 | 55 | 56 | TODO 57 | -------------- 58 | 59 | #### for multi apis mock, consider: 60 | 61 | if input parameters are like this: 62 | ``` 63 | $ anymocker -s save -a api1 -m x -a api2 -m y -i z 64 | ``` 65 | 66 | you intend to make rules below: 67 | ``` 68 | { 69 | "api": { 70 | "api1": { 71 | "mock": [ 72 | x 73 | ] 74 | }, 75 | "api2": { 76 | "mock": [ 77 | y 78 | ], 79 | "inject": [ 80 | z 81 | ] 82 | } 83 | } 84 | } 85 | ``` 86 | but unfortunately, what you get exactly is: 87 | ``` 88 | { 89 | "api": { 90 | "api1": { 91 | "mock": [ 92 | x 93 | ], 94 | "inject": [ 95 | z 96 | ] 97 | }, 98 | "api2": { 99 | "mock": [ 100 | y 101 | ] 102 | } 103 | } 104 | } 105 | ``` 106 | the latter `-i z` will be squashed as an `api1` rule, that's because anymocker analisis parameters by order. 107 | so please, to set a parameter at a prior place, or you just want to make it easy, try `anymocker -s save -a api1 -m x -i -a api2 -m y -i z` 108 | 109 | -------------------------------------------------------------------------------- /anymocker.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ffzzhong/anymocker/bfdba0195a8f0f4362a063fa0b9e27b4b235f2ec/anymocker.jpg -------------------------------------------------------------------------------- /gen.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var jp = require('jsonpath'), 4 | lo = require('lodash'), 5 | tools = require('./tools'), 6 | mockjs = require('mockjs'); 7 | 8 | var gen_mock = function(mockResData, scale) { 9 | var mock_fields = tools.isEmpty(scale.mock) ? [] : scale.mock, 10 | inject_fields = tools.isEmpty(scale.inject) ? [] : scale.inject, 11 | delete_fields = tools.isEmpty(scale.delete) ? [] : scale.delete 12 | console.log('--------------origin response-----------------') 13 | console.log(mockResData) 14 | console.log(mock_fields); 15 | console.log(inject_fields); 16 | console.log(delete_fields); 17 | for (var mock_field of mock_fields) { 18 | console.log(mock_field); 19 | var seperate_index = mock_field.indexOf('='), 20 | key = mock_field.substring(0, seperate_index), 21 | mock_value = mock_field.substring(seperate_index + 1); 22 | if (mock_value === 'FUZZ') { 23 | var nodes = jp.nodes(mockResData, key) 24 | for (var node of nodes) { 25 | console.log('start fuzzy...'); 26 | mock_value = gen_fuzzy_value(node.value) 27 | console.log('origin value:' + node.value) 28 | console.log('after fuzzy:' + mock_value) 29 | jp.value(mockResData, jp.stringify(node.path), mock_value) 30 | } 31 | } else { 32 | jp.apply(mockResData, key, function(value) { 33 | return mock_value; 34 | }); 35 | } 36 | } 37 | console.log('--------------after mock-----------------') 38 | console.log(mockResData) 39 | for (var inject_field of inject_fields) { 40 | var seperate_index = inject_field.indexOf('='), 41 | key = inject_field.substring(0, seperate_index), 42 | mock_value = inject_field.substring(seperate_index + 1); 43 | jp.value(mockResData, key, mock_value) 44 | } 45 | console.log('--------------after inject-----------------') 46 | console.log(mockResData) 47 | for (var delete_field of delete_fields) { 48 | var paths = jp.paths(mockResData, delete_field) 49 | for (var path of paths) { 50 | path.shift() 51 | lo.unset(mockResData, path) 52 | } 53 | } 54 | console.log('--------------after delete(final)-----------------') 55 | console.log(mockResData) 56 | return mockResData 57 | }; 58 | 59 | var gen_fuzzy_value = function(value) { 60 | var fuzzy_option = mockjs.mock({ 61 | "isNull|1-9": true, 62 | "scale": "@D20", 63 | "zoom|1": true 64 | }) 65 | if (fuzzy_option.isNull) { 66 | return null 67 | } else { 68 | if (/number/.test(Object.prototype.toString.call(value).toLowerCase())) { 69 | if (fuzzy_option.zoom) { 70 | return value * fuzzy_option.scale 71 | } else { 72 | return value / fuzzy_option.scale 73 | } 74 | } 75 | 76 | if (/string/.test(Object.prototype.toString.call(value).toLowerCase())) { 77 | if (fuzzy_option.zoom) { 78 | return mockjs.Random.string(fuzzy_option.zoom) + value + mockjs.Random.string(fuzzy_option.zoom) 79 | } else { 80 | if (fuzzy_option.scale >= value.length) { 81 | return '' 82 | } else { 83 | return value.substring(0, fuzzy_option.scale) 84 | } 85 | } 86 | } 87 | return value 88 | } 89 | }; 90 | 91 | module.exports = { 92 | gen_mock: gen_mock, 93 | gen_fuzzy_value: gen_fuzzy_value 94 | } 95 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var proxy = require('anyproxy'), 6 | parser = require('./parser'); 7 | 8 | 9 | var options = {}; 10 | 11 | if (parser.args.mock === null && parser.args.inject === null && parser.args.delete === null) { 12 | options.port = parser.args.port 13 | console.log('No rules specified, return origin response....') 14 | } else { 15 | options.port = parser.args.port 16 | var rule = require('./rule') 17 | options.rule = rule 18 | } 19 | 20 | new proxy.proxyServer(options) 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "anymocker", 3 | "version": "1.0.3", 4 | "description": "mock server with fuzzy value based on anyproxy", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.intra.douban.com/lingfeng/anymocker.git" 12 | }, 13 | "keywords": [ 14 | "fuzzy", 15 | "mock" 16 | ], 17 | "bin": { 18 | "anymocker": "index.js" 19 | }, 20 | "author": "fenfenzhong", 21 | "license": "ISC", 22 | "dependencies": { 23 | "anyproxy": "3.10.4", 24 | "argparse": "^1.0.9", 25 | "js-beautify": "^1.6.4", 26 | "jsonpath": "^0.2.7", 27 | "lodash": "^4.16.4", 28 | "mockjs": "^1.0.1-beta3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ArgumentParser = require('argparse').ArgumentParser; 4 | 5 | var parser = new ArgumentParser({ 6 | version: '0.0.1', 7 | addHelp: true, 8 | description: 'anymocker usage' 9 | }); 10 | 11 | parser.addArgument( 12 | ['-s', '--save'], { 13 | type: 'string', 14 | defaultValue: './save/', 15 | help: 'file save path' 16 | } 17 | ); 18 | parser.addArgument( 19 | ['-p', '--port'], { 20 | type: 'int', 21 | defaultValue: 8001, 22 | help: 'proxy port' 23 | } 24 | ); 25 | parser.addArgument( 26 | ['-m', '--mock'], { 27 | action: 'append', 28 | type: 'string', 29 | nargs: '*', 30 | help: 'mock value' 31 | } 32 | ); 33 | parser.addArgument( 34 | ['-a', '--api'], { 35 | action: 'append', 36 | type: 'string', 37 | nargs: '*', 38 | help: 'specify url' 39 | } 40 | ); 41 | parser.addArgument( 42 | ['-i', '--inject'], { 43 | action: 'append', 44 | type: 'string', 45 | nargs: '*', 46 | help: 'inject field' 47 | } 48 | ); 49 | parser.addArgument( 50 | ['-d', '--delete'], { 51 | action: 'append', 52 | type: 'string', 53 | nargs: '*', 54 | help: 'delete field' 55 | } 56 | ); 57 | 58 | 59 | var args = parser.parseArgs(), 60 | fields = ['mock', 'inject', 'delete'], 61 | mock_rules = { 62 | global: {}, 63 | api: {} 64 | }; 65 | 66 | console.log(args) 67 | 68 | 69 | // #TODO# 70 | // unsolved parameter order analysis 71 | // 72 | //if input parameters are like this: -a api1 -m x -a api2 -m y -i z 73 | //you intend to make rule below: 74 | // { 75 | // "api": { 76 | // "api1": { 77 | // "mock": [ 78 | // x 79 | // ] 80 | // }, 81 | // "api2": { 82 | // "mock": [ 83 | // y 84 | // ], 85 | // "inject": [ 86 | // z 87 | // ] 88 | // } 89 | // } 90 | // } 91 | //but unfortunately, the latter -i z will be squashed as an api1 rule: 92 | // { 93 | // "api": { 94 | // "api1": { 95 | // "mock": [ 96 | // x 97 | // ], 98 | // "inject": [ 99 | // z 100 | // ] 101 | // }, 102 | // "api2": { 103 | // "mock": [ 104 | // y 105 | // ] 106 | // } 107 | // } 108 | // } 109 | // 110 | //so , please notice that it's better to set a parameter at a prior place from making mistakes 111 | var parse = function() { 112 | if (args.api === null) { 113 | for (var field of fields) { 114 | if (args[field]) { 115 | mock_rules.global = mock_rules.global ? mock_rules.global : {} 116 | if (args[field].length > 1) { 117 | console.log('something wrong happend when specify a rule: ' + field) 118 | process.exit(-1); 119 | } else { 120 | mock_rules.global[field] = args[field][0] 121 | } 122 | } 123 | } 124 | } else { 125 | for (var field of fields) { 126 | if (args[field]) { 127 | if (args[field].length - args.api.length > 1) { 128 | console.log('something wrong happend when specify a rule: ' + field) 129 | process.exit(-1); 130 | } 131 | for (var i in args.api) { 132 | i = parseInt(i) 133 | if (i === args[field].length) { 134 | break; 135 | } 136 | for (var j of args.api[i]) { 137 | mock_rules.api[j] = mock_rules.api[j] ? mock_rules.api[j] : {} 138 | mock_rules.api[j][field] = args[field][i] 139 | } 140 | if (i === args.api.length - 1) { 141 | // reach max leanth of apis, if there are alse args[field] left, then put them as the global rule 142 | if (args[field].length > i + 1) { 143 | mock_rules.global[field] = args[field][i + 1] 144 | } 145 | } 146 | } 147 | } 148 | } 149 | } 150 | console.log(JSON.stringify(args, null, ' ')) 151 | console.log('-------rules after format----------') 152 | console.log(JSON.stringify(mock_rules, null, ' ')) 153 | return mock_rules 154 | }; 155 | 156 | module.exports = { 157 | parse: parse, 158 | args: args 159 | } 160 | -------------------------------------------------------------------------------- /record.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var beautify = require('js-beautify').js_beautify, 4 | path = require('path'), 5 | fs = require('fs'), 6 | parser = require('./parser'), 7 | url = require('url'); 8 | 9 | var api_index = 0; 10 | 11 | var record_data = function(req, serverResData) { 12 | api_index += 1 13 | var fileSavePath = path.resolve(parser.args.save), 14 | reqFile = path.join(fileSavePath, api_index + 'requestData.txt'), 15 | respFile = path.join(fileSavePath, api_index + 'responseData.txt'), 16 | reqData 17 | if (/http/.test(req.url)) { 18 | reqData = url.parse(req.url) 19 | } else { 20 | reqData = url.parse('https://' + req.headers.host + req.url) 21 | } 22 | 23 | fs.appendFile(reqFile, beautify(JSON.stringify(reqData), { 24 | indent_size: 4 25 | }), function(err, data) { 26 | if (err) { 27 | return console.error(err); 28 | } 29 | }); 30 | 31 | fs.appendFile(respFile, beautify(unescape(serverResData.toString().replace(/\\u/g, '%u')), { 32 | indent_size: 4 33 | }), function(err, data) { 34 | if (err) { 35 | return console.error(err); 36 | } 37 | }); 38 | }; 39 | 40 | module.exports = record_data; 41 | -------------------------------------------------------------------------------- /rule.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var tools = require('./tools'), 4 | record_data = require('./record'), 5 | gen = require('./gen'), 6 | parser = require('./parser'), 7 | rule = require('./rule'); 8 | 9 | var mock_rules = parser.parse() 10 | var rule = { 11 | summary: function() { 12 | return 'mock server working...' 13 | }, 14 | 15 | shouldInterceptHttpsReq: function(req) { 16 | // switch https on 17 | return true 18 | }, 19 | 20 | replaceServerResDataAsync: function(req, res, serverResData, callback) { 21 | if (/json/i.test(res.headers['content-type']) && serverResData.toString() != '') { 22 | try { 23 | var mockResData = JSON.parse(serverResData.toString('utf-8')), 24 | global_mock_flag = false; 25 | } catch (e) { 26 | console.log('something wrong when resolving origin response, return directly....'); 27 | callback(serverResData) 28 | } 29 | // conditions must be matched here: 30 | // 1. must be rules, either global rule or api rule, or both of them 31 | // 2. origin response must be json format, and not null 32 | 33 | // apply global rule first if it's not empty(because api rule can override it) 34 | if (!tools.isEmpty(mock_rules.global)) { 35 | console.log('global rule specified, now apply global rule'); 36 | record_data(req, serverResData) 37 | mockResData = gen.gen_mock(mockResData, mock_rules.global) 38 | global_mock_flag = true 39 | } 40 | 41 | if (!tools.isEmpty(mock_rules.api)) { 42 | // when api rule specified 43 | var find_flag = false, 44 | api_index; 45 | for (api_index in mock_rules.api) { 46 | if (req.url.indexOf(api_index) != -1) { 47 | console.log('hit api!') 48 | console.log('origin reuest url:' + req.url) 49 | find_flag = true 50 | break 51 | } 52 | } 53 | if (find_flag) { 54 | console.log('api catched!!! return modified response') 55 | if (!global_mock_flag) { 56 | record_data(req, serverResData) 57 | } 58 | mockResData = gen.gen_mock(mockResData, mock_rules.api[api_index]) 59 | } else { 60 | console.log('api missed... return origin response') 61 | } 62 | } else { 63 | console.log('api unspecified'); 64 | } 65 | callback(JSON.stringify(mockResData)) 66 | } else { 67 | callback(serverResData) 68 | } 69 | } 70 | } 71 | 72 | 73 | module.exports = rule 74 | -------------------------------------------------------------------------------- /tools.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function isEmpty(obj) { 4 | for (var name in obj) { 5 | return false; 6 | } 7 | return true; 8 | }; 9 | 10 | module.exports = { 11 | isEmpty: isEmpty 12 | } 13 | --------------------------------------------------------------------------------