├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .npmrc ├── .nycrc ├── .prettierrc ├── .travis.yml ├── LICENSE.txt ├── README.md ├── build ├── clean.ts └── copy.ts ├── package.json ├── src ├── bin │ ├── har-convert.ts │ ├── raml-mocker.ts │ ├── raml-runner.ts │ └── server.ts ├── har-convert │ ├── filter-path.ts │ ├── index.ts │ ├── template │ │ ├── api.ejs │ │ └── test.spec.ejs │ ├── to-raml.ts │ ├── to-spec.ts │ └── xhr.ts ├── http-client.ts ├── index.ts ├── models │ ├── $ref.ts │ ├── body.ts │ ├── config.ts │ ├── harTypes.ts │ ├── output-request.ts │ ├── parameter.ts │ ├── response.ts │ ├── rest-api.ts │ ├── runner.ts │ └── schema.ts ├── output.ts ├── read-raml │ ├── constant.ts │ ├── definition-schema.ts │ ├── index.ts │ └── utils.ts ├── runner │ ├── index.ts │ ├── runner-util.ts │ └── validate-warning.ts ├── server.ts ├── util │ ├── config-util.ts │ ├── fs.ts │ └── index.ts └── validate.ts ├── test ├── filter-path.spec.ts ├── har-convert │ ├── api.raml │ ├── har-convert.spec.ts │ └── localhost.har ├── output.spec.ts ├── parameter.spec.ts ├── read-raml │ ├── definition-schema.spec.ts │ ├── read-raml.spec.ts │ └── utils.spec.ts ├── runner │ └── runner-util.spec.ts ├── schama.spec.ts ├── to-raml.spec.ts ├── to-spec.spec.ts ├── tsconfig.json └── util │ ├── config-util.spec.ts │ └── util.spec.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xbl/raml-mocker/ebb6a179321933b601bb5712e1fe3331f05540a2/.eslintignore -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | 👋 Hi! This file was autogenerated by tslint-to-eslint-config. 3 | https://github.com/typescript-eslint/tslint-to-eslint-config 4 | 5 | It represents the closest reasonable ESLint configuration to this 6 | project's original TSLint configuration. 7 | 8 | We recommend eventually switching this configuration to extend from 9 | the recommended rulesets in typescript-eslint. 10 | https://github.com/typescript-eslint/tslint-to-eslint-config/blob/master/docs/FAQs.md 11 | 12 | Happy linting! 💖 13 | */ 14 | module.exports = { 15 | "env": { 16 | "es6": true, 17 | "node": true 18 | }, 19 | "extends": [ 20 | "plugin:@typescript-eslint/recommended", 21 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 22 | "prettier", 23 | "prettier/@typescript-eslint" 24 | ], 25 | "parser": "@typescript-eslint/parser", 26 | "parserOptions": { 27 | "project": ["tsconfig.json", "./test/tsconfig.json"], 28 | "sourceType": "module" 29 | }, 30 | "plugins": [ 31 | "@typescript-eslint", 32 | "@typescript-eslint/tslint" 33 | ], 34 | "rules": { 35 | "@typescript-eslint/adjacent-overload-signatures": "warn", 36 | "@typescript-eslint/array-type": [ 37 | "warn", 38 | { 39 | "default": "array-simple" 40 | } 41 | ], 42 | "@typescript-eslint/await-thenable": "error", 43 | "@typescript-eslint/ban-ts-comment": "error", 44 | "@typescript-eslint/ban-types": [ 45 | "warn", 46 | { 47 | "types": { 48 | "Object": { 49 | "message": "Avoid using the `Object` type. Did you mean `object`?" 50 | }, 51 | "Function": { 52 | "message": "Avoid using the `Function` type. Prefer a specific function type, like `() => void`." 53 | }, 54 | "Boolean": { 55 | "message": "Avoid using the `Boolean` type. Did you mean `boolean`?" 56 | }, 57 | "Number": { 58 | "message": "Avoid using the `Number` type. Did you mean `number`?" 59 | }, 60 | "String": { 61 | "message": "Avoid using the `String` type. Did you mean `string`?" 62 | }, 63 | "Symbol": { 64 | "message": "Avoid using the `Symbol` type. Did you mean `symbol`?" 65 | } 66 | } 67 | } 68 | ], 69 | "@typescript-eslint/consistent-type-assertions": "warn", 70 | "@typescript-eslint/consistent-type-definitions": "warn", 71 | "@typescript-eslint/dot-notation": "warn", 72 | "@typescript-eslint/explicit-member-accessibility": [ 73 | "off", 74 | { 75 | "accessibility": "explicit" 76 | } 77 | ], 78 | "@typescript-eslint/explicit-module-boundary-types": "warn", 79 | "@typescript-eslint/indent": [ 80 | "warn", 81 | 2, 82 | { 83 | "FunctionDeclaration": { 84 | "parameters": "first" 85 | }, 86 | "FunctionExpression": { 87 | "parameters": "first" 88 | } 89 | } 90 | ], 91 | "@typescript-eslint/member-delimiter-style": [ 92 | "warn", 93 | { 94 | "multiline": { 95 | "delimiter": "semi", 96 | "requireLast": true 97 | }, 98 | "singleline": { 99 | "delimiter": "semi", 100 | "requireLast": false 101 | } 102 | } 103 | ], 104 | "@typescript-eslint/member-ordering": "warn", 105 | "@typescript-eslint/naming-convention": "off", 106 | "@typescript-eslint/no-array-constructor": "error", 107 | "@typescript-eslint/no-empty-function": "warn", 108 | "@typescript-eslint/no-empty-interface": "warn", 109 | "@typescript-eslint/no-explicit-any": "off", 110 | "@typescript-eslint/no-extra-non-null-assertion": "error", 111 | "@typescript-eslint/no-extra-semi": "error", 112 | "@typescript-eslint/no-floating-promises": "error", 113 | "@typescript-eslint/no-for-in-array": "error", 114 | "@typescript-eslint/no-implied-eval": "error", 115 | "@typescript-eslint/no-inferrable-types": "error", 116 | "@typescript-eslint/no-misused-new": "warn", 117 | "@typescript-eslint/no-misused-promises": "error", 118 | "@typescript-eslint/no-namespace": "warn", 119 | "@typescript-eslint/no-non-null-asserted-optional-chain": "error", 120 | "@typescript-eslint/no-non-null-assertion": "warn", 121 | "@typescript-eslint/no-parameter-properties": "off", 122 | "@typescript-eslint/no-this-alias": "error", 123 | "@typescript-eslint/no-unnecessary-type-assertion": "error", 124 | "@typescript-eslint/no-unsafe-assignment": "warn", 125 | "@typescript-eslint/no-unsafe-call": "warn", 126 | "@typescript-eslint/no-unsafe-member-access": "warn", 127 | "@typescript-eslint/no-unsafe-return": "error", 128 | "@typescript-eslint/no-unused-expressions": "warn", 129 | "@typescript-eslint/no-unused-vars": "warn", 130 | "@typescript-eslint/no-use-before-define": "off", 131 | "@typescript-eslint/no-var-requires": "warn", 132 | "@typescript-eslint/prefer-as-const": "error", 133 | "@typescript-eslint/prefer-for-of": "warn", 134 | "@typescript-eslint/prefer-function-type": "warn", 135 | "@typescript-eslint/prefer-namespace-keyword": "warn", 136 | "@typescript-eslint/prefer-regexp-exec": "error", 137 | "@typescript-eslint/quotes": [ 138 | "warn", 139 | "single" 140 | ], 141 | "@typescript-eslint/require-await": "error", 142 | "@typescript-eslint/restrict-plus-operands": "error", 143 | "@typescript-eslint/restrict-template-expressions": "off", 144 | "@typescript-eslint/semi": [ 145 | "warn", 146 | "always" 147 | ], 148 | "@typescript-eslint/triple-slash-reference": [ 149 | "warn", 150 | { 151 | "path": "always", 152 | "types": "prefer-import", 153 | "lib": "always" 154 | } 155 | ], 156 | "@typescript-eslint/type-annotation-spacing": "warn", 157 | "@typescript-eslint/unbound-method": "error", 158 | "@typescript-eslint/unified-signatures": "warn", 159 | "arrow-body-style": "warn", 160 | "arrow-parens": [ 161 | "warn", 162 | "always" 163 | ], 164 | "brace-style": [ 165 | "warn", 166 | "1tbs" 167 | ], 168 | "comma-dangle": [ 169 | "warn", 170 | "always-multiline" 171 | ], 172 | "complexity": "off", 173 | "constructor-super": "warn", 174 | "curly": "warn", 175 | "eol-last": "warn", 176 | "eqeqeq": [ 177 | "warn", 178 | "smart" 179 | ], 180 | "guard-for-in": "warn", 181 | "id-blacklist": [ 182 | "warn", 183 | "any", 184 | "Number", 185 | "number", 186 | "String", 187 | "string", 188 | "Boolean", 189 | "boolean", 190 | "Undefined", 191 | "undefined" 192 | ], 193 | "id-match": "warn", 194 | "import/order": "off", 195 | "max-classes-per-file": [ 196 | "warn", 197 | 1 198 | ], 199 | "max-len": [ 200 | "warn", 201 | { 202 | "code": 120 203 | } 204 | ], 205 | "new-parens": "warn", 206 | "no-array-constructor": "off", 207 | "no-bitwise": "warn", 208 | "no-caller": "warn", 209 | "no-cond-assign": "warn", 210 | "no-console": "warn", 211 | "no-debugger": "warn", 212 | "no-empty": "warn", 213 | "no-empty-function": "off", 214 | "no-eval": "warn", 215 | "no-fallthrough": "off", 216 | "no-invalid-this": "off", 217 | "no-new-wrappers": "warn", 218 | "no-shadow": [ 219 | "warn", 220 | { 221 | "hoist": "all" 222 | } 223 | ], 224 | "no-throw-literal": "warn", 225 | "no-trailing-spaces": "warn", 226 | "no-undef-init": "warn", 227 | "no-underscore-dangle": "warn", 228 | "no-unsafe-finally": "warn", 229 | "no-unused-labels": "warn", 230 | "no-unused-vars": "off", 231 | "no-var": "warn", 232 | "object-shorthand": "warn", 233 | "one-var": [ 234 | "warn", 235 | "never" 236 | ], 237 | "prefer-const": "warn", 238 | "quote-props": [ 239 | "warn", 240 | "consistent-as-needed" 241 | ], 242 | "radix": "warn", 243 | "require-await": "off", 244 | "space-before-function-paren": [ 245 | "warn", 246 | { 247 | "anonymous": "never", 248 | "asyncArrow": "always", 249 | "named": "never" 250 | } 251 | ], 252 | "spaced-comment": [ 253 | "warn", 254 | "always", 255 | { 256 | "markers": [ 257 | "/" 258 | ] 259 | } 260 | ], 261 | "use-isnan": "warn", 262 | "valid-typeof": "off", 263 | "@typescript-eslint/tslint/config": [ 264 | "error", 265 | { 266 | "rules": { 267 | "import-spacing": true, 268 | "whitespace": [ 269 | true, 270 | "check-branch", 271 | "check-decl", 272 | "check-operator", 273 | "check-separator", 274 | "check-type", 275 | "check-typecast" 276 | ] 277 | } 278 | } 279 | ] 280 | } 281 | }; 282 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | *.lock 3 | *.log 4 | package-lock.json 5 | .nyc_output 6 | coverage 7 | dist 8 | docs 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | coverage 3 | src/ 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @xbl:registry=https://registry.npmjs.org 2 | registry=https://registry.npm.taobao.org 3 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@istanbuljs/nyc-config-typescript", 3 | "all": true, 4 | "include": [ 5 | "src/**/*.ts" 6 | ], 7 | "exclude": [ 8 | "**/*.d.ts", 9 | "src/bin/**/*.ts" 10 | ], 11 | "extension": [ 12 | ".ts" 13 | ], 14 | "reporter": [ 15 | "html", 16 | "text-summary", 17 | "json" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: coverage 2 | language: node_js 3 | node_js: 4 | - "12.18.3" 5 | env: 6 | - CODECOV_TOKEN="718a1aed-7418-4bb6-8047-d2dddc6a2006" 7 | install: 8 | - yarn 9 | - yarn global add codecov 10 | scripts: 11 | - yarn test 12 | after_success: 13 | - codecov -f coverage/*.json 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Raml-mocker 4 | 5 | ![version](https://img.shields.io/npm/v/@xbl/raml-mocker.svg) 6 | ![node (scoped)](https://img.shields.io/node/v/@xbl/raml-mocker.svg?style=flat) 7 | [![Build Status](https://travis-ci.org/xbl/raml-mocker.svg?branch=master)](https://travis-ci.org/xbl/raml-mocker) 8 | [![codecov](https://codecov.io/gh/xbl/raml-mocker/branch/master/graph/badge.svg)](https://codecov.io/gh/xbl/raml-mocker) 9 | 10 | Raml-mocker 是基于 [Raml](https://raml.org/) 的 mock server,Raml 是 RESTfull API 描述语言,同时支持自定义指令。raml-mocker 可以根据 raml 描述文档读取到 API 中的 uri 及 response 中的 example 继而生成 mock server。 11 | 12 | 在 2.0 版本中增加了 API 接口的测试,所以 Raml-mocker 不仅仅是 mock server,还是一个不错的 API 接口测试工具。 13 | 14 | ## 开始 15 | 16 | #### 初始化项目 17 | ```shell 18 | git clone https://github.com/xbl/raml-mocker-starter.git raml-api 19 | cd raml-api 20 | git remote rm origin 21 | ``` 22 | #### 安装 23 | ```shell 24 | yarn 25 | # or 26 | npm install 27 | ``` 28 | ##### 启动 mock server 29 | ```shell 30 | yarn start 31 | # or 32 | npm start 33 | ``` 34 | ![console](https://tva1.sinaimg.cn/large/007S8ZIlly1ggjjwpwgwgj30o005c74v.jpg) 35 | 36 | 命令行会输出 Mock Server 的地址,不熟悉 Nodejs 的同学可以参考[这里](https://github.com/xbl/raml-mocker/wiki/%E4%BD%BF%E7%94%A8Docker%E5%90%AF%E5%8A%A8),使用 Docker 启动。 37 | 38 | **注意:此地址是 API 接口的 host,需要请求接口完整路径才能返回正确数据。** 39 | 40 | #### 验证一下 41 | 42 | ```shell 43 | curl -i http://localhost:3000/api/v1/articles 44 | # or 45 | curl -i http://localhost:3000/api/v1/articles/bbb 46 | ``` 47 | 48 | 或者使用 Postman: 49 | 50 | ![Postman](https://tva1.sinaimg.cn/large/007S8ZIlly1ggjjvm0uvnj30u00ujdo9.jpg) 51 | 52 | 53 | 54 | ### 生成 API 可视化文档 55 | 56 | ```shell 57 | yarn run build 58 | # or 59 | npm run build 60 | ``` 61 | 62 | 会在工程下面生成一个 api.html 文件,双击打开即可看到一个 html 文档,如图: 63 | 64 | ![API 文档](https://tva1.sinaimg.cn/large/007S8ZIlly1ggjk7bsx7pj31px0u0wt3.jpg) 65 | 66 | 此功能使用了[raml2html](https://www.npmjs.com/package/raml2html)。 67 | 68 | 69 | 70 | ## 配置 .raml-config.json 71 | 72 | ```json 73 | { 74 | "controller": "./controller", 75 | "raml": "./raml", 76 | "main": "api.raml", 77 | "port": 3000, 78 | "plugins": [] 79 | } 80 | ``` 81 | 82 | * controller: controller 目录路径,在高级篇中会有更详细说明 83 | * raml: raml 文件目录 84 | * main: raml 目录下的入口文件 85 | * port: mock server 服务端口号 86 | * plugins: 插件(*可能会有变动*) 87 | 88 | 89 | 90 | 91 | 92 | 93 | ## 入门篇:Mock Server 94 | 95 | 在 ./raml/api 目录下创建 books 文件夹: 96 | 97 | ![目录结构](https://tva1.sinaimg.cn/large/007S8ZIlly1ggjk9ocmy8j30ea0x4q54.jpg) 98 | 99 | 在 books 文件夹中创建 books.raml 文件 100 | 101 | ```yaml 102 | get: 103 | description: 图书列表 104 | responses: 105 | 200: 106 | body: 107 | application/json: 108 | type: object 109 | example: !include ./books_200.json 110 | ``` 111 | 112 | 在 books 文件夹中创建 books_200.json 文件 113 | 114 | ```json 115 | { 116 | "code": 200, 117 | "data": [ 118 | { 119 | "id": 1, 120 | "title": "books title", 121 | "description": "books desccription1" 122 | }, 123 | { 124 | "id": 2, 125 | "title": "books title", 126 | "description": "books desccription2" 127 | } 128 | ] 129 | } 130 | ``` 131 | 132 | 修改 ./raml/api.raml 133 | 134 | ```yaml 135 | #%RAML 1.0 136 | --- 137 | title: hello demo API 138 | baseUri: / 139 | version: v1 140 | mediaType: application/json 141 | 142 | # 安全设置 143 | securitySchemes: !include ./securitySchemes.raml 144 | 145 | # 自定义资源 types,类似于资源模板,指定 type 可以减少代码,还可以覆盖模板 146 | resourceTypes: !include ./resourceTypes.raml 147 | 148 | # 相当于数据类型,可用于自动化测试作为验证条件 149 | types: !include ./types.raml 150 | 151 | # 152 | /api/v1: 153 | /articles: !include ./api/articles/articles.raml 154 | /products: !include ./api/products/products.raml 155 | /login: !include ./api/users/login.raml 156 | # 添加 157 | /books: !include ./api/books/books.raml 158 | 159 | ``` 160 | 161 | ![](https://tva1.sinaimg.cn/large/007S8ZIlly1ggjkeq974rj31j20u0q83.jpg) 162 | 163 | 请求时是将 /api/v1/books 与 host 拼接出来的 URL,/api/v1 可在文档 api.raml 中修改。 164 | 165 | ```shell 166 | curl -X GET http://localhost:3000/api/v1/books 167 | ``` 168 | 169 | 或者使用Postman: 170 | 171 | ![Postman](https://tva1.sinaimg.cn/large/007S8ZIlly1ggjkg5ree3j31560u0q6q.jpg) 172 | 173 | 174 | 175 | ## 高级篇:动态 Server 176 | 177 | 在 raml 文档中添加 `(controller)` 指令,即可添加动态的 Server,如: 178 | 179 | ```yaml 180 | /books: 181 | type: 182 | resourceList: 183 | get: 184 | description: 获取用户的书籍 185 | (controller): user#getBook 186 | responses: 187 | 200: 188 | body: 189 | type: song[] 190 | example: !include ./books_200.json 191 | ``` 192 | 193 | 在文档中 `(controller)` 表示 controller 目录下 user.js 中 getBook 函数。 194 | 195 | controller/user.js 196 | 197 | ```javascript 198 | exports.getBook = (req, res, webApi) => { 199 | console.log(webApi); 200 | res.send('Hello World!'); 201 | } 202 | ``` 203 | 204 | Raml-mocker 是在 [expressjs](http://expressjs.com/) 基础上进行开发,req、res 可以参考 express 文档。 205 | 206 | 207 | 如此,raml-mocker 提供了更多可扩展空间,我们甚至可以在 controller 中实现一定的逻辑判断。 208 | 209 | ## API 自动化测试 210 | 211 | 在 1.1.0 中增加 API 测试,通过在 raml 文件中添加 response 数据格式描述,raml-runner 会发送请求,来验证 response 的数据格式是否符合预期。 212 | 213 | ![runner](https://tva1.sinaimg.cn/large/007S8ZIlly1ggjkka6ktxj30zs0u0dkg.jpg) 214 | 215 | 216 | 1. 在 types 文件中编写商品 Type,描述了返回数据的类型,以及对象中字段验证: 217 | 218 | ```yaml 219 | Product: 220 | type: object 221 | properties: 222 | productId: 223 | type: string 224 | minLength: 4 225 | maxLength: 36 226 | required: true 227 | productName: string 228 | description: string 229 | price: number 230 | ``` 231 | 232 | 2. 在 API Raml 中添加 type 字段: 233 | 234 | ``` yaml 235 | get: 236 | description: 商品列表 237 | queryParameters: 238 | isStar: 239 | description: 是否精选 240 | type: boolean 241 | required: false 242 | example: true 243 | responses: 244 | 200: 245 | body: 246 | # 这里描述的商品数组 247 | type: Product[] 248 | example: !include ./products_200.json 249 | 250 | /{productId}: 251 | get: 252 | description: 商品详情 253 | (controller): product#getProductDetail 254 | (uriParameters): 255 | productId: 256 | description: productId 257 | example: aaaa 258 | responses: 259 | 200: 260 | body: 261 | # type 这里描述的商品 262 | type: Product 263 | example: !include ./product_200.json 264 | 265 | ``` 266 | 267 | 3. 启动 Mock Server,并运行测试 268 | 269 | ```shell 270 | # 启动 Mock Server 271 | npm start 272 | 273 | # 运行 API 测试 274 | npm test 275 | ``` 276 | 277 | 278 | 279 | ### 设置不同环境 280 | 281 | 运行测试时默认会测试 Mock Server的 response,设置不同的环境方式如下: 282 | 283 | 编辑 .raml-config.json 文件 284 | 285 | ```json 286 | { 287 | "controller": "./controller", 288 | "raml": "./raml", 289 | "main": "api.raml", 290 | "port": 3000, 291 | "runner": { 292 | "local": "http://localhost:3000", 293 | "dev": "http://abc.com:3001" 294 | } 295 | } 296 | ``` 297 | 298 | 在 runner 添加不同的环境对应的 HOST,通过 SET `NODE_ENV` 来更改运行不同环境的测试。 299 | 300 | ```shell 301 | cross-env NODE_ENV=dev raml-runner 302 | 303 | # 为了方便已经在模板项目中添加了 npm script,可自由更改 304 | npm run test:dev 305 | ``` 306 | 307 | 308 | 309 | ### 前置条件 310 | 311 | 以上只能满足不需要登录的 API 测试,登录的接口则需要 **优先** 执行,然后再执行其他接口,此处为了简单增加了`(runner)` 指令: 312 | 313 | ```yaml 314 | /login 315 | post: 316 | description: 登录 317 | body: 318 | username: 319 | description: 用户名 320 | type: string 321 | required: true 322 | example: abc 323 | password: 324 | description: 密码 325 | type: string 326 | required: true 327 | example: abc 328 | (runner): 329 | # 注意:这里的相对路径是相对于工程目录,而不是当前文件。 330 | after: ./runner/afterLogin.js 331 | responses: 332 | 200: 333 | body: 334 | type: string 335 | example: fdafda232432fdaxfda25dfa 336 | 337 | ``` 338 | 339 | 解析 raml 文件会优先执行带有 `(runner)` 指令的接口,并在执行完成之后调用 `after` 对应的 js 文件。 340 | 341 | afterLogin.js 342 | 343 | ```javascript 344 | module.exports = (axios, response) => { 345 | axios.defaults.headers.common['Authorization'] = response.data; 346 | } 347 | 348 | ``` 349 | 350 | 测试发请求使用的 [axios](https://www.npmjs.com/package/axios) 模块,所以这里会在函数参数中添加 axios 实例,以及执行 login 接口的 response 对象。通常,设置 Header 就可以满足登录所需要的大部分场景。 351 | 352 | afterLogin.js 可返回 `Promise` 对象: 353 | 354 | ``` js 355 | module.exports = (axios, response) => { 356 | return new Promise((resolve, reject) => { 357 | axios.defaults.headers.common['Authorization'] = response.data; 358 | setTimeout(() => { 359 | console.log('不仅设置了header,还吃了个饭,洗了个澡...'); 360 | resolve() 361 | }, 3000); 362 | }); 363 | } 364 | ``` 365 | 366 | 367 | 368 | ## API 场景测试 369 | 370 | 在 2.0 中增加了 API 的场景测试,在目录中增加了 `test` 文件夹。 371 | 372 | 1. 在 raml 中增加 description 373 | 374 | ```yaml 375 | get: 376 | # 请保证 description 唯一 377 | description: 商品列表 378 | queryParameters: 379 | isStar: 380 | description: 是否精选 381 | type: boolean 382 | required: false 383 | example: true 384 | isOk: 385 | description: 是否精选2 386 | type: boolean 387 | required: false 388 | example: true 389 | responses: 390 | 200: 391 | body: 392 | type: Product[] 393 | example: !include ./products_200.json 394 | ``` 395 | 396 | **注意:** description 的字符串会在 loadApi 时使用,所以请保证唯一。 397 | 398 | 2. 在 test 目录新增 article.spec.js 399 | 400 | ```javascript 401 | const assert = require('assert'); 402 | const { loadApi } = require('@xbl/raml-mocker'); 403 | 404 | it('从文章列表到文章详情', async () => { 405 | // 根据 `文章列表` 的 description 找到 raml 描述的 API 406 | const getList = loadApi('文章列表'); 407 | const { status, data: list } = await getList(); 408 | const articleId = list[0].articleId; 409 | 410 | assert.equal(status, 200); 411 | assert.equal(articleId, 'A00001'); 412 | 413 | const getDetail = loadApi('文章详情'); 414 | const { data: detail } = await getDetail({ id: articleId }); 415 | assert.equal(detail.title, '提升家里整体格调的小物件'); 416 | }); 417 | ``` 418 | 419 | 测试框架集成了 [Mocha](https://mochajs.org/),断言使用 Nodejs 自带的 [Assert](https://nodejs.org/dist/latest-v10.x/docs/api/assert.html#assert_assert) 模块,开发者可以选择自己喜欢的断言库。 420 | 421 | 运行测试: 422 | 423 | ```shell 424 | npm run test:api 425 | ``` 426 | 427 | ![运行测试](https://tva1.sinaimg.cn/large/007S8ZIlly1ggjklircrmj30xm0cc0us.jpg) 428 | 429 | 430 | 431 | ### API 432 | 433 | ```javascript 434 | loadApi(description: string): Function; 435 | // loadApi 接收一个字符串参数,返回一个函数 436 | 437 | anonymousFn (uriParameters, queryParameter, body): Promise 438 | 439 | /** 440 | * uriParameters: { 441 | * id: 1 442 | * ... 443 | * } 444 | * 445 | * queryParameter: { 446 | * pageSize: 20 447 | * ... 448 | * } 449 | * 450 | * body 是 POST 的数据 451 | */ 452 | ``` 453 | 454 | AioseResponse 文档可参考[这里](https://www.npmjs.com/package/axios#response-schema)。 455 | 456 | 457 | 458 | ## 旧有项目如何使用 raml-mocker 459 | 460 | #### HTTP Archive (HAR) 反向工程 461 | 462 | 2.0 新增的功能,帮助开发者和测试同学可以在旧有项目中快速使用 raml-mocker,并生成测试代码片段。请看[视频](http://v.youku.com/v_show/id_XNDA3NzYzOTM2MA==.html?spm=a2h3j.8428770.3416059.1)。 463 | 464 | [![视频](http://img.alicdn.com/tfs/TB1ZbM.lOqAXuNjy1XdXXaYcVXa-160-90.png)](http://v.youku.com/v_show/id_XNDA3NzYzOTM2MA==.html?spm=a2h3j.8428770.3416059.1) 465 | 466 | ### 通过 har 文件生成 raml 467 | 468 | 以 npm 为例: 469 | 470 | ``` shell 471 | har-convert -f ./www.npmjs.com.har -o ./raml/api.raml -filter www.npmjs.com 472 | ``` 473 | 474 | ### 通过 har 文件生成测试片段 475 | 476 | ```shell 477 | har-convert -f ./www.npmjs.com.har -o ./test/search.spec.js 478 | ``` 479 | 480 | 481 | 482 | 可通过录制特定场景的请求可生成该场景的测试片段。 483 | 484 | 关于 har 可参考[这里](https://github.com/xbl/raml-mocker/wiki/HTTP-Archive-(HAR)--%E8%AF%B4%E6%98%8E)。 485 | 486 | ## Road Map 487 | 488 | - [x] API 自动化测试 489 | - [x] API 场景测试 490 | - [x] 自动化增加前置条件,如:登录 491 | - [x] 读取 HTTP Archive (HAR) format 反向工程 492 | - [ ] 多场景的契约测试 493 | - [ ] Mock Server 增加请求参数验证 494 | - [ ] baseUriParameters 495 | - [ ] 上传文件的处理 496 | 497 | 498 | 499 | ## 使用遇到问题? 500 | 501 | 使用中遇到任何问题,请给[告诉我](https://github.com/xbl/raml-mocker/issues)好吗? 502 | -------------------------------------------------------------------------------- /build/clean.ts: -------------------------------------------------------------------------------- 1 | import * as shell from 'shelljs'; 2 | 3 | shell.rm('-R', 'dist', 'coverage', '.nyc_output'); 4 | -------------------------------------------------------------------------------- /build/copy.ts: -------------------------------------------------------------------------------- 1 | import * as shell from 'shelljs'; 2 | 3 | // Copy all the view templates 4 | const template = 'har-convert/template'; 5 | shell.cp('-R', `src/${template}`, `dist/${template}`); 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xbl/raml-mocker", 3 | "version": "2.0.4", 4 | "engines": { 5 | "node": ">=8.0.0" 6 | }, 7 | "scripts": { 8 | "copy-assets": "ts-node build/copy", 9 | "clean": "ts-node build/clean", 10 | "dev": "tsc --watch", 11 | "build": "npm run clean && tsc && npm run relative-path && npm run chmod && npm run copy-assets", 12 | "lint": "eslint -c .eslintrc.js --ext .ts 'src/**/*.ts' ./test/**/*.ts", 13 | "lint:fix": "eslint -c .eslintrc.js --ext .ts 'src/**/*.ts' ./test/**/*.ts --fix", 14 | "chmod": "chmod +x ./dist/bin/*", 15 | "test": "nyc ava --fail-fast -v", 16 | "test:watch": "ava --watch --fail-fast -v", 17 | "prepublish": "npm run build", 18 | "publish": "npm publish --access=public", 19 | "publish:next": "npm publish --access=public --tag next", 20 | "relative-path": "tscpaths -p tsconfig.json -s ./src -o ./dist" 21 | }, 22 | "main": "dist/index.js", 23 | "bin": { 24 | "raml-mocker": "./dist/bin/raml-mocker.js", 25 | "raml-runner": "./dist/bin/raml-runner.js", 26 | "har-convert": "./dist/bin/har-convert.js" 27 | }, 28 | "ava": { 29 | "extensions": [ 30 | "ts" 31 | ], 32 | "require": [ 33 | "ts-node/register", 34 | "tsconfig-paths/register" 35 | ] 36 | }, 37 | "dependencies": { 38 | "ajv": "^6.9.1", 39 | "axios": "^0.18.0", 40 | "chalk": "^2.4.1", 41 | "chokidar": "^2.0.4", 42 | "express": "^4.16.3", 43 | "raml-1-parser": "^1.1.67", 44 | "tslint": "^6.1.3" 45 | }, 46 | "devDependencies": { 47 | "@istanbuljs/nyc-config-typescript": "^1.0.1", 48 | "@types/node": "^11.9.3", 49 | "@typescript-eslint/eslint-plugin": "^4.0.1", 50 | "@typescript-eslint/eslint-plugin-tslint": "^4.0.1", 51 | "@typescript-eslint/parser": "^4.0.1", 52 | "ava": "^3.12.1", 53 | "body-parser": "^1.19.0", 54 | "eslint": "^7.8.1", 55 | "eslint-config-prettier": "^6.11.0", 56 | "eslint-plugin-import": "^2.22.0", 57 | "lodash": "^4.17.13", 58 | "nyc": "^15.1.0", 59 | "path-to-regexp": "^3.0.0", 60 | "prettier": "^1.15.1", 61 | "shelljs": "^0.8.3", 62 | "sinon": "^7.3.0", 63 | "source-map-support": "^0.5.19", 64 | "ts-node": "^9.0.0", 65 | "tsconfig-paths": "^3.9.0", 66 | "tscpaths": "^0.0.7", 67 | "typescript": "^4.0.2" 68 | }, 69 | "repository": { 70 | "type": "git", 71 | "url": "git+https://github.com/xbl/raml-mocker.git" 72 | }, 73 | "keywords": [ 74 | "raml", 75 | "mock server" 76 | ], 77 | "author": "xbl", 78 | "license": "ISC", 79 | "bugs": { 80 | "url": "https://github.com/xbl/raml-mocker/issues" 81 | }, 82 | "homepage": "https://github.com/xbl/raml-mocker#readme", 83 | "description": "raml mock server" 84 | } 85 | -------------------------------------------------------------------------------- /src/bin/har-convert.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from '@/util/fs'; 4 | import { read, save } from '../har-convert'; 5 | 6 | const argsMap = {}; 7 | const args = process.argv.splice(2); 8 | args.forEach((val, index) => { 9 | if (index % 2 === 0) { 10 | argsMap[val] = args[index + 1]; 11 | } 12 | }); 13 | 14 | const harPath: string = argsMap['-f'] as string; 15 | const target = argsMap['-o'] as string; 16 | const filter = argsMap['-filter'] as string; 17 | 18 | const convert = async () => { 19 | if (!harPath || !target) { 20 | // eslint-disable no-console 21 | console.log(` 22 | har 转 raml: 23 | har-convert -f ./[har 文件].har -o ./raml/[目标].raml -filter /api/v1 24 | 25 | har 转 *.spec.js: 26 | har-convert -f ./[har 文件].har -o ./test/[目标].spec.js 27 | 28 | Options: 29 | 30 | -f 入口文件 31 | -o 输出文件 32 | -filter 只过滤带有过滤条件的请求(可选),如: -filter /api/v1 33 | `); 34 | return; 35 | } 36 | const har = await fs.readFile(harPath, 'utf-8'); 37 | const restAPIArr = read(har, filter); 38 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 39 | save(restAPIArr, target); 40 | }; 41 | 42 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 43 | convert(); 44 | -------------------------------------------------------------------------------- /src/bin/raml-mocker.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import path from 'path'; 4 | import chokidar from 'chokidar'; 5 | import { fork, ChildProcess } from 'child_process'; 6 | import { loadConfig } from '@/util/config-util'; 7 | 8 | const start = async () => { 9 | const config = await loadConfig(); 10 | let server: ChildProcess = null; 11 | const startServer = () => { 12 | server = fork(path.join(__dirname, './server')); 13 | server.send(config); 14 | }; 15 | const restartServer = () => { 16 | // eslint-disable no-console 17 | console.log('restart...'); 18 | server.kill('SIGHUP'); 19 | startServer(); 20 | }; 21 | 22 | startServer(); 23 | 24 | const watcher = chokidar.watch([config.raml, config.controller]); 25 | watcher 26 | .on('change', restartServer) 27 | .on('unlinkDir', restartServer) 28 | .on('unlink', restartServer); 29 | }; 30 | 31 | void start(); 32 | -------------------------------------------------------------------------------- /src/bin/raml-runner.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { getHost } from '@/util'; 4 | import { loadConfig } from '@/util/config-util'; 5 | import Runner from '@/runner'; 6 | import Output from '@/output'; 7 | 8 | const start = async () => { 9 | const config = await loadConfig(); 10 | const host = getHost(config); 11 | const output = new Output(host); 12 | process.on('beforeExit', () => { 13 | output.print(); 14 | }); 15 | 16 | const runner = new Runner(config, output); 17 | void runner.start(); 18 | }; 19 | 20 | void start(); 21 | -------------------------------------------------------------------------------- /src/bin/server.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import http from 'http'; 4 | import mocker from '../server'; 5 | import Config from '@/models/config'; 6 | 7 | process.on('message', (config: Config) => { 8 | mocker.setConfig(config); 9 | const port = config.port || 3000; 10 | // eslint-disable no-console 11 | http 12 | .createServer(mocker.app) 13 | .listen(port, () => 14 | console.log(`raml mock server http://localhost:${port}!`), 15 | ); 16 | }); 17 | -------------------------------------------------------------------------------- /src/har-convert/filter-path.ts: -------------------------------------------------------------------------------- 1 | import { HarEntry } from '@/models/harTypes'; 2 | 3 | export default (entries: HarEntry[], condition: string): any[] => 4 | entries 5 | .filter(({ request }) => { 6 | const { url } = request; 7 | return url.includes(condition); 8 | }); 9 | -------------------------------------------------------------------------------- /src/har-convert/index.ts: -------------------------------------------------------------------------------- 1 | import urlUtil from 'url'; 2 | import fs from '@/util/fs'; 3 | import xhrFilter from './xhr'; 4 | import toRaml from './to-raml'; 5 | import toSpec from './to-spec'; 6 | import { extname, join } from 'path'; 7 | import filterPath from './filter-path'; 8 | import RestAPI from '@/models/rest-api'; 9 | import { loadApi } from 'raml-1-parser'; 10 | import { getRestApiArr } from '@/read-raml'; 11 | import Parameter from '@/models/parameter'; 12 | import { mergeRestApi } from '@/util'; 13 | import { loadConfig } from '@/util/config-util'; 14 | import { Api } from 'raml-1-parser/dist/parser/artifacts/raml10parserapi'; 15 | import { HarEntry, HarTyped } from '@/models/harTypes'; 16 | 17 | 18 | const filterEmpty = (obj: any): RestAPI => JSON.parse(JSON.stringify(obj)) as RestAPI; 19 | 20 | const toParameter = (queryStrings: any[]): Parameter[] => 21 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 22 | queryStrings.map(({name, value}) => ({ name, example: decodeURIComponent(value)})); 23 | 24 | const toRestAPI = (entries: any[]): RestAPI[] => entries.map((entry: HarEntry) => { 25 | const { request, response } = entry; 26 | const { url, method, queryString, postData } = request; 27 | const newUrl = urlUtil.parse(url); 28 | 29 | const { 30 | status, 31 | content: { mimeType, text }, 32 | } = response; 33 | 34 | return filterEmpty({ 35 | url: newUrl.pathname, 36 | description: `${method.toLowerCase()}${ newUrl.pathname.replace(/\//g, '_') }`, 37 | method, 38 | queryParameters: toParameter(queryString), 39 | body: postData, 40 | responses: [ 41 | { 42 | code: status, 43 | body: { mimeType, text }, 44 | }, 45 | ], 46 | }); 47 | }); 48 | 49 | export const mergeRestApiToSpec = async (newRestAPIArr: RestAPI[]): Promise => { 50 | const config = await loadConfig(); 51 | const apiJSON = await loadApi(join(config.raml, config.main)) as Api; 52 | const restApiArr = mergeRestApi(newRestAPIArr, getRestApiArr(apiJSON)); 53 | return toSpec(restApiArr); 54 | }; 55 | 56 | export const read = (har: string, filter?: string): RestAPI[] => { 57 | const json: HarTyped = JSON.parse(har) as HarTyped; 58 | let entries = xhrFilter(json.log.entries); 59 | if (filter) { 60 | entries = filterPath(entries, filter); 61 | } 62 | return toRestAPI(entries); 63 | }; 64 | 65 | export const save = async (restAPIArr: RestAPI[], target: string): Promise => { 66 | const ext = extname(target); 67 | let str = ''; 68 | if (isRamlFile(ext)) { 69 | str = await toRaml(restAPIArr); 70 | } 71 | if (isScriptFile(ext)) { 72 | str = await mergeRestApiToSpec(restAPIArr); 73 | } 74 | return fs.appendFile(target, str); 75 | }; 76 | 77 | const isScriptFile = (ext: string) => ['.js', '.ts'].includes(ext); 78 | 79 | const isRamlFile = (ext: string) => ext === '.raml'; 80 | -------------------------------------------------------------------------------- /src/har-convert/template/api.ejs: -------------------------------------------------------------------------------- 1 | <% restAPIArr.forEach(restAPI => { %> 2 | <%= restAPI.url %>: 3 | <%= restAPI.method.toLocaleLowerCase() %>: 4 | description: <%= restAPI.description %><% 5 | if (!_.isEmpty(restAPI.queryParameters)) { 6 | %> 7 | queryParameters:<% 8 | _.forEach(restAPI.queryParameters, (param) => {%> 9 | <%= param.name %>: 10 | example: <%= param.example %><% 11 | }); 12 | } 13 | if (!_.isEmpty(restAPI.body)) { 14 | %> 15 | body: 16 | example: | 17 | <%= restAPI.body.text %><% 18 | } 19 | %> 20 | responses:<% 21 | restAPI.responses.forEach(response => { 22 | %> 23 | <%= response.code %>: 24 | body: 25 | example: | 26 | <%= indentString(response.body.text, 12) %><% 27 | }); 28 | %> 29 | <% }) %> 30 | -------------------------------------------------------------------------------- /src/har-convert/template/test.spec.ejs: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { loadApi } = require('@xbl/raml-mocker'); 3 | 4 | it('Case Name', async () => {<% 5 | restAPIArr.forEach((restAPI, index) => { 6 | const methodName = `${restAPI.method.toLowerCase()}Fn${index}`; 7 | const uriParam = JSON.stringify(restAPI.uriParameters) || '{}'; 8 | let bodyData = '{}'; 9 | if (restAPI.body) { 10 | if (isJSONType(restAPI.body.mimeType)) { 11 | bodyData = restAPI.body.text; 12 | } else { 13 | bodyData = `'${restAPI.body.text}'`; 14 | } 15 | } 16 | const queryParameters = Parameter.toJSON(restAPI.queryParameters); 17 | const params = [uriParam, JSON.stringify(queryParameters), bodyData].join(','); 18 | %> 19 | const <%= methodName %> = loadApi('<%= restAPI.description %>'); 20 | const { status: status<%= index%>, data: data<%= index%> } = await <%= methodName %>(<%= params %>); 21 | 22 | assert.equal(status<%= index%>, 200); 23 | // TODO: assert 24 | <% }) %> 25 | }); 26 | -------------------------------------------------------------------------------- /src/har-convert/to-raml.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-return */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 3 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 4 | 5 | import { template } from 'lodash'; 6 | import fs from '@/util/fs'; 7 | import RestAPI from '@/models/rest-api'; 8 | import { indentString, isJSONType } from '@/util'; 9 | 10 | const filter = (restAPIs: RestAPI[]) => { 11 | const map = {}; 12 | return restAPIs.filter((restAPI) => { 13 | const key = `${restAPI.method}_${restAPI.url}`; 14 | return map[key] ? false : (map[key] = true); 15 | }); 16 | }; 17 | 18 | const toRaml = async (restAPIs: RestAPI[]): Promise => { 19 | const str = await fs.readFile(`${__dirname}/template/api.ejs`, 'utf-8'); 20 | const compiled = template(str, { imports : { indentString, isJSONType }}); 21 | return compiled({ restAPIArr: filter(restAPIs) }).trim(); 22 | }; 23 | 24 | export default toRaml; 25 | -------------------------------------------------------------------------------- /src/har-convert/to-spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-return */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 3 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 4 | import { template } from 'lodash'; 5 | import { isJSONType } from '@/util'; 6 | import fs from '@/util/fs'; 7 | import RestAPI from '@/models/rest-api'; 8 | import Parameter from '@/models/parameter'; 9 | 10 | const SPEC_TEMPLATE = `${__dirname}/template/test.spec.ejs`; 11 | 12 | const toSpec = async (restAPIArr: RestAPI[]): Promise => { 13 | const str = await fs.readFile(SPEC_TEMPLATE, 'utf-8'); 14 | const compiled = template(str, { imports : { isJSONType, Parameter }}); 15 | return compiled({ restAPIArr}).trim(); 16 | }; 17 | 18 | export default toSpec; 19 | -------------------------------------------------------------------------------- /src/har-convert/xhr.ts: -------------------------------------------------------------------------------- 1 | import { isJSONType } from '../util'; 2 | import { HarEntry } from '@/models/harTypes'; 3 | 4 | export default (entries: HarEntry[]): any[] => 5 | entries 6 | .filter(({ response }) => { 7 | const { mimeType } = response.content; 8 | return isJSONType(mimeType); 9 | }); 10 | -------------------------------------------------------------------------------- /src/http-client.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import axios from 'axios'; 3 | import { isEmpty } from 'lodash'; 4 | import RestAPI from './models/rest-api'; 5 | import { replaceUriParameters } from './util'; 6 | 7 | export default class HttpClient { 8 | 9 | constructor(host: string) { 10 | axios.defaults.baseURL = host; 11 | } 12 | 13 | send = async (restApi: RestAPI, uriParameters, queryParameter = {}, body = {}) => { 14 | let requestPath = restApi.url; 15 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call 16 | if (!isEmpty(uriParameters)) { 17 | replaceUriParameters(requestPath, (match, expression) => { 18 | requestPath = requestPath.replace(match, uriParameters[expression]); 19 | }); 20 | } 21 | 22 | const response = await axios(requestPath, { 23 | method: restApi.method, 24 | data: body, 25 | params: queryParameter, 26 | }); 27 | 28 | const { runner } = restApi; 29 | if (runner) { 30 | const { after } = runner; 31 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 32 | const afterModule = require(path.resolve(after)); 33 | if (typeof afterModule === 'function') { 34 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call 35 | await afterModule(axios, response); 36 | } 37 | } 38 | return response; 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import HttpClient from '@/http-client'; 3 | import { getRestApiArr } from '@/read-raml'; 4 | import { getHost } from '@/util'; 5 | import { loadConfig } from '@/util/config-util'; 6 | import { loadApi as loadRamlApi } from 'raml-1-parser'; 7 | import { Api } from 'raml-1-parser/dist/parser/artifacts/raml10parserapi'; 8 | import RestAPI from './models/rest-api'; 9 | 10 | let restApiArr: RestAPI[]; 11 | let httpClient: HttpClient; 12 | 13 | export const initProject = async () => { 14 | const config = await loadConfig(); 15 | const host = getHost(config); 16 | httpClient = new HttpClient(host); 17 | 18 | const apiJSON = await loadRamlApi(join(config.raml, config.main)) as Api; 19 | restApiArr = getRestApiArr(apiJSON); 20 | }; 21 | 22 | export const loadApi = (description: string) => { 23 | if (!description) { 24 | throw Error('Please set API description!'); 25 | } 26 | if (!restApiArr) { 27 | throw Error('Can\'t find API'); 28 | } 29 | const api: RestAPI = restApiArr 30 | .filter((restApi) => restApi.description === description) 31 | .pop(); 32 | if (!api) { 33 | throw Error(`Can't find API by '${description}'!`); 34 | } 35 | 36 | const execute = async (uriParameters, queryParameter, body) => { 37 | try { 38 | return await httpClient.send(api, uriParameters, queryParameter, body); 39 | } catch (error) { 40 | throw new Error(`description: '${description}' ${(error as Error).message}`); 41 | } 42 | }; 43 | return execute; 44 | }; 45 | -------------------------------------------------------------------------------- /src/models/$ref.ts: -------------------------------------------------------------------------------- 1 | export default class $Ref { 2 | type?: any[]; 3 | $ref?: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/models/body.ts: -------------------------------------------------------------------------------- 1 | export default class Body { 2 | mimeType: string; 3 | text: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/models/config.ts: -------------------------------------------------------------------------------- 1 | export default class Config​​ { 2 | controller: string; 3 | raml: string; 4 | main: string; 5 | port: 3000; 6 | runner: object; 7 | plugins?: string[]; 8 | } 9 | -------------------------------------------------------------------------------- /src/models/harTypes.ts: -------------------------------------------------------------------------------- 1 | import Body from '@/models/body'; 2 | 3 | export interface HarEntry { 4 | request: { url: string; method: string; queryString: any[]; postData?: Body }; 5 | response: { status: number; content: { mimeType: string; text: string}; redirectURL?: string}; 6 | } 7 | export interface HarTyped { 8 | log: {entries: HarEntry[]}; 9 | } 10 | -------------------------------------------------------------------------------- /src/models/output-request.ts: -------------------------------------------------------------------------------- 1 | export default class OutputRequest { 2 | path: string; 3 | method: string; 4 | beginTime: any; 5 | 6 | constructor(obj: {path?: string; method?: string}) { 7 | this.path = obj.path; 8 | this.method = obj.method; 9 | this.beginTime = Date.now(); 10 | } 11 | 12 | setRealPath(realPath: string) { 13 | if (realPath) { 14 | this.path = realPath; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/models/parameter.ts: -------------------------------------------------------------------------------- 1 | export default class Parameter { 2 | name: string; 3 | type?: string; 4 | description?: string; 5 | example?: unknown; 6 | required?: boolean; 7 | 8 | static build(jsonObject: unknown): Parameter[] { 9 | return Object.keys(jsonObject).map((key) => ({ name: key, example: jsonObject[key] as unknown })); 10 | } 11 | 12 | static toJSON(params: Parameter[]): Record { 13 | const jsonObj = {}; 14 | params.forEach((param) => { 15 | jsonObj[param.name] = param.example; 16 | }); 17 | return jsonObj; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/models/response.ts: -------------------------------------------------------------------------------- 1 | import Body from './body'; 2 | import Schema from './schema'; 3 | 4 | export default class Response { 5 | code: number; 6 | body?: Body; 7 | redirectURL?: string; 8 | schema?: Schema; 9 | } 10 | -------------------------------------------------------------------------------- /src/models/rest-api.ts: -------------------------------------------------------------------------------- 1 | import Body from './body'; 2 | import Response from './response'; 3 | import Runner from './runner'; 4 | import Parameter from './parameter'; 5 | 6 | export default class RestAPI { 7 | url: string; 8 | method: string; 9 | description?: string; 10 | controller?: string; 11 | runner?: Runner; 12 | uriParameters?: object; 13 | queryParameters?: Parameter[]; 14 | body?: Body; 15 | responses?: Response[]; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/models/runner.ts: -------------------------------------------------------------------------------- 1 | export default class Runner { 2 | after?: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/models/schema.ts: -------------------------------------------------------------------------------- 1 | export default class Schema { 2 | $id?: string; 3 | items?: any[]; 4 | $ref?: string; 5 | additionalItems?: any; 6 | type?: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/output.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import chalk from 'chalk'; 3 | import OutputRequest from '@/models/output-request'; 4 | 5 | const typeMap = { 6 | 0: { 7 | color: 'red', 8 | icon: '✖', 9 | }, 10 | 1: { 11 | color: 'green', 12 | icon: '✔', 13 | }, 14 | 2: { 15 | color: 'yellow', 16 | icon: '!', 17 | }, 18 | }; 19 | 20 | export default class Output { 21 | public static WARNING = 2; 22 | public static ERROR = 0; 23 | public static SUCCESS = 1; 24 | 25 | host: string; 26 | successCount = 0; 27 | failCount = 0; 28 | 29 | 30 | // eslint-disable-next-line @typescript-eslint/prefer-regexp-exec 31 | static processMessage = (message: string) => message.match(/(^.*)[\n]*([\w\W]*)/); 32 | 33 | constructor(host: string) { 34 | this.host = host; 35 | console.log(`HOST: ${this.host}`); 36 | } 37 | 38 | 39 | push = (type: number, outputRequest: OutputRequest, message = '') => { 40 | const { path​​, method, beginTime} = outputRequest; 41 | const [, title, validInfo] = Output.processMessage(message); 42 | const { color, icon } = typeMap[type]; 43 | if (type === Output.ERROR) { 44 | this.failCount++; 45 | } else { 46 | this.successCount++; 47 | } 48 | const takeTime = (Date.now() - beginTime).toString(); 49 | console.log( 50 | chalk`{${color} ${icon} 请求:[${method.toUpperCase()}]} {underline ${ 51 | path 52 | }} {gray ${takeTime}ms}`, 53 | ); 54 | console.log(chalk`{${color} ${title}}`); 55 | console.log(validInfo); 56 | }; 57 | 58 | print = () => { 59 | console.log(chalk`{green ${this.successCount.toString()} tests passed}`); 60 | if (this.failCount > 0) { 61 | console.log(chalk`{red ${this.failCount.toString()} tests failed} `); 62 | process.exit(1); 63 | } 64 | }; 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/read-raml/constant.ts: -------------------------------------------------------------------------------- 1 | export const ANY_TYPE = 'any'; 2 | export const BASE_TYPE = [ 3 | 'string', 4 | 'number', 5 | 'boolean', 6 | 'array', 7 | 'object', 8 | 'integer', 9 | 'null', 10 | ANY_TYPE, 11 | ]; 12 | -------------------------------------------------------------------------------- /src/read-raml/definition-schema.ts: -------------------------------------------------------------------------------- 1 | import $Ref from '../models/$ref'; 2 | import Schema from '../models/schema'; 3 | import { setProps } from './utils'; 4 | import { BASE_TYPE } from './constant'; 5 | import { Api } from 'raml-1-parser/dist/parser/artifacts/raml10parserapi'; 6 | 7 | 8 | export const getDefinitionSchema = (apiJSON: Api): Schema => { 9 | const $id = '/definitionSchema'; 10 | const definitionSchema = { 11 | $id, 12 | definitions: {}, 13 | }; 14 | const clazzArr = apiJSON.types(); 15 | clazzArr.forEach((clazz) => { 16 | const clazzName = clazz.name(); 17 | const jsonObj = clazz.toJSON({ serializeMetadata: false }); 18 | const { properties } = jsonObj[clazzName]; 19 | 20 | if (!properties) { 21 | return; 22 | } 23 | 24 | const requiredArr = []; 25 | const schemaProperties = {}; 26 | Object.keys(properties).forEach((key) => { 27 | const { 28 | items, 29 | required, 30 | name, 31 | type, 32 | maxLength, 33 | minLength, 34 | pattern, 35 | } = properties[key]; 36 | 37 | const property = { 38 | type: type.map(String), 39 | }; 40 | setProps(property, 'maxLength', maxLength); 41 | setProps(property, 'minLength', minLength); 42 | setProps(property, 'pattern', pattern); 43 | if (required) { 44 | requiredArr.push(name); 45 | } 46 | schemaProperties[name] = property; 47 | 48 | if (!BASE_TYPE.includes(type[0])) { 49 | schemaProperties[name] = { $ref: `${$id}#/definitions/${type[0]}` }; 50 | return; 51 | } 52 | 53 | if (items) { 54 | let $ref: $Ref = { type: items }; 55 | if (!BASE_TYPE.includes(items)) { 56 | $ref = { $ref: `${$id}#/definitions/${items}` }; 57 | } 58 | schemaProperties[name] = { 59 | items: [$ref], 60 | additionalItems: $ref, 61 | }; 62 | } 63 | }); 64 | 65 | const schemaPro = { 66 | type: 'object', 67 | properties: schemaProperties, 68 | required: requiredArr, 69 | }; 70 | 71 | definitionSchema.definitions[clazzName] = schemaPro; 72 | }); 73 | return definitionSchema; 74 | }; 75 | -------------------------------------------------------------------------------- /src/read-raml/index.ts: -------------------------------------------------------------------------------- 1 | import { isRedirectCode } from '../util'; 2 | import RestAPI from '../models/rest-api'; 3 | import Response from '../models/response'; 4 | import Schema from '../models/schema'; 5 | import { setProps, getPathname } from './utils'; 6 | import { BASE_TYPE, ANY_TYPE } from './constant'; 7 | import Body from '../models/body'; 8 | import Parameter from '../models/parameter'; 9 | import { TypeDeclaration, Api, Response as RamlResponse, 10 | Resource, Method } from 'raml-1-parser/dist/parser/artifacts/raml10parserapi'; 11 | import { isEmpty } from 'lodash'; 12 | 13 | const getSchemaByType = (type): Schema => { 14 | if (!type) { 15 | return undefined; 16 | } 17 | const newType = type.replace('[]', ''); 18 | if (newType === ANY_TYPE) { 19 | return undefined; 20 | } 21 | if (BASE_TYPE.includes(newType)) { 22 | return { type: newType }; 23 | } 24 | const $ref = { $ref: `/definitionSchema#/definitions/${newType}` }; 25 | let schema: Schema = $ref; 26 | if (type.includes('[]')) { 27 | schema = { 28 | items: [$ref], 29 | additionalItems: $ref, 30 | }; 31 | } 32 | return schema; 33 | }; 34 | 35 | const getQueryParameters = (queryParameters): Parameter[] => { 36 | if (!Array.isArray(queryParameters)) { 37 | return; 38 | } 39 | const newParams: Parameter[] = []; 40 | queryParameters.forEach((param) => { 41 | if (!param.example()) { 42 | return; 43 | } 44 | const value = param.example().value(); 45 | if (!value) { 46 | return; 47 | } 48 | // TODO: 文档中说返回的是字符串,结果返回的是数组: 49 | // https://raml-org.github.io/raml-js-parser-2/interfaces/_src_raml1_artifacts_raml08parserapi_.parameter.html#type 50 | let type = param.type(); 51 | if (Array.isArray(param.type())) { 52 | type = param.type().pop(); 53 | } 54 | newParams.push({name: param.name(), example: value, required: param.required(), type }); 55 | }); 56 | return newParams; 57 | }; 58 | 59 | const getPostBody = ([body]: TypeDeclaration[]): Body => { 60 | if (!body || !body.example()) { 61 | return; 62 | } 63 | const value = body.example().value(); 64 | if (!value) { 65 | return; 66 | } 67 | return { 68 | mimeType: body.name(), 69 | text: value, 70 | }; 71 | }; 72 | 73 | export const getAnnotationByName = (name, method): unknown => { 74 | let annotationObj; 75 | method.annotations().forEach((annotation) => { 76 | const json = annotation.toJSON(); 77 | if (json.name !== name) { 78 | return; 79 | } 80 | annotationObj = json.structuredValue; 81 | }); 82 | return annotationObj; 83 | }; 84 | 85 | const getUriParameters = (resource, method) => { 86 | const uriParameters = {}; 87 | const params = getAnnotationByName('uriParameters', method); 88 | if (!isEmpty(params)) { 89 | Object.keys(params).forEach((key) => { 90 | const param = params[key]; 91 | if (!param) { 92 | return; 93 | } 94 | const example = String(param.example); 95 | if (param && example) { 96 | uriParameters[key] = example; 97 | } 98 | }); 99 | } 100 | 101 | // has bug: https://github.com/raml-org/raml-js-parser-2/issues/829 102 | resource.allUriParameters().forEach((parameter) => { 103 | const name = parameter.name(); 104 | const example = parameter.example(); 105 | let value = ''; 106 | if (!example) { 107 | return; 108 | } 109 | value = example.value(); 110 | uriParameters[name] = value; 111 | }); 112 | 113 | return uriParameters; 114 | }; 115 | 116 | const getHeaderLocation = (response: RamlResponse) => { 117 | let redirectURL: string; 118 | response.headers().forEach((typeDeclaration) => { 119 | if (typeDeclaration.name().toLowerCase() === 'location') { 120 | redirectURL = typeDeclaration.type()[0]; 121 | } 122 | }); 123 | return redirectURL; 124 | }; 125 | 126 | const getResponseByBody = (code, body: TypeDeclaration): Response => { 127 | const example = body.example(); 128 | if (!example) { 129 | return; 130 | } 131 | const mimeType = body.name(); 132 | const type = body.type().pop(); 133 | const restApiResp: Response = { 134 | code, 135 | body: { 136 | text: example.value(), 137 | mimeType, 138 | }, 139 | }; 140 | const schema = getSchemaByType(type); 141 | setProps(restApiResp, 'schema', schema); 142 | return restApiResp; 143 | }; 144 | 145 | const getRestApiByMethod = (url: string, method: Method, resource: Resource): RestAPI => { 146 | const restApi: RestAPI = { url, method: method.method() }; 147 | const description = method.description() && method.description().value(); 148 | setProps(restApi, 'description', description); 149 | 150 | const controller = getAnnotationByName('controller', method); 151 | setProps(restApi, 'controller', controller); 152 | 153 | const runner = getAnnotationByName('runner', method); 154 | setProps(restApi, 'runner', runner); 155 | 156 | restApi.uriParameters = getUriParameters(resource, method); 157 | 158 | restApi.queryParameters = getQueryParameters(method.queryParameters()); 159 | const postBody = getPostBody(method.body()); 160 | setProps(restApi, 'body', postBody); 161 | 162 | restApi.responses = []; 163 | method.responses().forEach((response) => { 164 | const code = parseInt(response.code().value(), 10); 165 | // 30x 166 | if (isRedirectCode(code)) { 167 | const redirectURL = getHeaderLocation(response); 168 | restApi.responses.push({ code, redirectURL }); 169 | return; 170 | } 171 | 172 | restApi.responses = restApi.responses.concat(response.body() 173 | .map((body) => getResponseByBody(code, body)) 174 | .filter((webApiResp: Response) => webApiResp)); 175 | }); 176 | return restApi; 177 | }; 178 | 179 | export const getRestApiArr = (apiJSON: Api): RestAPI[] => { 180 | let restApiArr: RestAPI[] = []; 181 | apiJSON.allResources().forEach((resource: Resource) => { 182 | const url = getPathname(resource.absoluteUri()); 183 | restApiArr = restApiArr.concat(resource.methods() 184 | .map((method: Method) => getRestApiByMethod(url, method, resource))); 185 | }); 186 | return restApiArr; 187 | }; 188 | -------------------------------------------------------------------------------- /src/read-raml/utils.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'url'; 2 | 3 | export const setProps = (obj, property, value) => { 4 | if (value) { 5 | obj[property] = value; 6 | } 7 | }; 8 | 9 | export const getPathname = (url) => decodeURIComponent(parse(url).pathname); 10 | -------------------------------------------------------------------------------- /src/runner/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { join } from 'path'; 4 | import { isEmpty } from 'lodash'; 5 | import { loadApi as loadRamlApi } from 'raml-1-parser'; 6 | import { Api } from 'raml-1-parser/dist/parser/artifacts/raml10parserapi'; 7 | 8 | import Output from '@/output'; 9 | import { getHost } from '@/util'; 10 | import HttpClient from '@/http-client'; 11 | import Config from '@/models/config'; 12 | import RestAPI from '@/models/rest-api'; 13 | import Parameter from '@/models/parameter'; 14 | import OutputRequest from '@/models/output-request'; 15 | import SchemaValidate from '@/validate'; 16 | import { getRestApiArr } from '@/read-raml'; 17 | import { getDefinitionSchema } from '@/read-raml/definition-schema'; 18 | import { getResponseByStatusCode, sortByRunner, splitByParameter } from './runner-util'; 19 | import ValidateWarning from './validate-warning'; 20 | import { AxiosResponse } from 'axios'; 21 | import Schema from '@/models/schema'; 22 | 23 | const splitRestApiArr = (apiJSON: Api): RestAPI[] => { 24 | const result: RestAPI[] = []; 25 | getRestApiArr(apiJSON).forEach((restApi: RestAPI) => { 26 | result.push(...splitByParameter(restApi)); 27 | }); 28 | return result; 29 | }; 30 | 31 | const doRequest = (httpClient: HttpClient, webApi: RestAPI): Promise> => { 32 | const body = webApi.body ? webApi.body.text : {}; 33 | return httpClient.send( 34 | webApi, 35 | webApi.uriParameters, 36 | Parameter.toJSON(webApi.queryParameters), 37 | body, 38 | ); 39 | }; 40 | 41 | export default class Runner { 42 | config: Config; 43 | output: Output; 44 | definitionSchema: Schema; 45 | httpClient: HttpClient; 46 | restApiArr: RestAPI[]; 47 | schemaValidate: SchemaValidate; 48 | 49 | constructor(config: Config, output: Output) { 50 | this.config = config; 51 | this.httpClient = new HttpClient(getHost(this.config)); 52 | this.output = output; 53 | } 54 | 55 | async start() { 56 | const apiJSON = await loadRamlApi(join(this.config.raml, this.config.main)) as Api; 57 | this.restApiArr = sortByRunner(splitRestApiArr(apiJSON)); 58 | if (isEmpty(this.restApiArr)) { 59 | return ; 60 | } 61 | this.definitionSchema = getDefinitionSchema(apiJSON); 62 | this.schemaValidate = new SchemaValidate(this.definitionSchema); 63 | void this.runByRunner(); 64 | } 65 | 66 | validateResponse = (webApi, response) => { 67 | const { data, status } = response; 68 | if (!webApi.responses.length) { 69 | throw new ValidateWarning('No set responses'); 70 | } 71 | const resp = getResponseByStatusCode(status, webApi.responses); 72 | if (!resp) { 73 | throw new Error(`Can\'t find responses by status ${status}`); 74 | } 75 | this.schemaValidate.execute(resp.schema, data); 76 | }; 77 | 78 | logError = (err, outputRequest: OutputRequest) => { 79 | if (err instanceof ValidateWarning) { 80 | this.output.push(Output.WARNING, outputRequest, err.message); 81 | return ; 82 | } 83 | this.output.push(Output.ERROR, outputRequest, err.message || err); 84 | }; 85 | 86 | send = async (webApi: RestAPI) => { 87 | const outputRequest: OutputRequest = 88 | new OutputRequest({ path: webApi.url, method: webApi.method }); 89 | try { 90 | const response = await doRequest(this.httpClient, webApi); 91 | outputRequest.setRealPath(response.request.path); 92 | this.validateResponse(webApi, response); 93 | this.output.push(Output.SUCCESS, outputRequest); 94 | } catch (err) { 95 | this.logError(err, outputRequest); 96 | } 97 | }; 98 | 99 | // eslint-disable-next-line @typescript-eslint/require-await 100 | runByRunner = async (): Promise => { 101 | for (const webApi of this.restApiArr) { 102 | if (webApi.runner) { 103 | await this.send(webApi); 104 | return; 105 | } 106 | void this.send(webApi); 107 | } 108 | }; 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/runner/runner-util.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-return */ 2 | import { isEmpty } from 'lodash'; 3 | import RestAPI from '../models/rest-api'; 4 | import Response from '../models/response'; 5 | import Parameter from '../models/parameter'; 6 | 7 | export const getResponseByStatusCode = (code: number, responses: Response[]): Response => { 8 | if (isEmpty(responses)) { 9 | return; 10 | } 11 | return responses.find((resp) => resp.code === code); 12 | }; 13 | 14 | export const sortByRunner = (restApiArr: RestAPI[]): RestAPI[] => { 15 | if (isEmpty(restApiArr)) { 16 | return; 17 | } 18 | return restApiArr.sort((restApi) => (restApi.runner && restApi.runner.after ? -1 : 1)); 19 | }; 20 | 21 | const getGroup = (data: any[], index = 0, group = []): any[] => { 22 | const tempArr = []; 23 | tempArr.push([data[index]]); 24 | group.forEach((_, i) => { 25 | tempArr.push([...group[i], data[index]]); 26 | }); 27 | group.push(...tempArr); 28 | 29 | if (index + 1 >= data.length) { 30 | return group; 31 | } 32 | return getGroup(data, index + 1, group); 33 | }; 34 | 35 | export const splitByParameter = (restApi: RestAPI): RestAPI[] => { 36 | const queryParameters = restApi.queryParameters; 37 | if (isEmpty(queryParameters)) { 38 | return [restApi]; 39 | } 40 | const baseRestApi = {...restApi}; 41 | baseRestApi.queryParameters = []; 42 | const restApiArr = [baseRestApi]; 43 | const newParams = getGroup(queryParameters); 44 | newParams.forEach((element: Parameter[]) => { 45 | const newRestApi: RestAPI = { ...restApi}; 46 | newRestApi.queryParameters = element; 47 | restApiArr.push(newRestApi); 48 | }); 49 | return restApiArr; 50 | }; 51 | -------------------------------------------------------------------------------- /src/runner/validate-warning.ts: -------------------------------------------------------------------------------- 1 | export default class ValidateWarning extends Error {} 2 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import express from 'express'; 3 | import bodyParser from 'body-parser'; 4 | import { getRestApiArr } from './read-raml'; 5 | import { isRedirectCode, toExpressUri } from './util'; 6 | import RestAPI from './models/rest-api'; 7 | import { loadApiSync } from 'raml-1-parser'; 8 | import { Api } from 'raml-1-parser/dist/parser/artifacts/raml10parserapi'; 9 | 10 | const app = express(); 11 | 12 | app.use(bodyParser.urlencoded({ extended: false })); 13 | app.use(bodyParser.json()); 14 | app.use(bodyParser.text({type: '*/*'})); 15 | 16 | app.use((req, res, next) => { 17 | // eslint-disable no-console 18 | console.log('PATH: %s, METHOD: %s', req.path, req.method); 19 | next(); 20 | }); 21 | 22 | const handler = (req, res, config, restApi: RestAPI) => { 23 | if (restApi.controller) { 24 | const [controller, methodName] = restApi.controller.split('#'); 25 | const moduleCtrl = require(`${config.controller}/${controller}`); 26 | const fn = moduleCtrl[methodName]; 27 | if (typeof fn === 'function') { 28 | fn.call(app, req, res, restApi); 29 | } 30 | return; 31 | } 32 | 33 | const response = restApi.responses[0]; 34 | if (!response) { 35 | res.status(404).send('no set response or example'); 36 | return; 37 | } 38 | 39 | if (isRedirectCode(response.code)) { 40 | res.redirect(response.redirectURL); 41 | return; 42 | } 43 | 44 | const { body } = response; 45 | if (body.mimeType) { 46 | res.type(body.mimeType); 47 | } 48 | res.status(response.code); 49 | if (Array.isArray(config.plugins)) { 50 | config.plugins.forEach((plugin) => { 51 | // eslint-disable-next-line 52 | const text = require(plugin)(body); 53 | res.send(text); 54 | }); 55 | return; 56 | } 57 | res.send(body.text); 58 | }; 59 | 60 | const setConfig = (config) => { 61 | const apiJSON = loadApiSync(join(config.raml, config.main)) as Api; 62 | 63 | const restApiArr = getRestApiArr(apiJSON); 64 | restApiArr.forEach((restApi) => { 65 | app[restApi.method](toExpressUri(restApi.url), (req, res) => { 66 | handler(req, res, config, restApi); 67 | }); 68 | }); 69 | }; 70 | 71 | export default { 72 | setConfig, 73 | app, 74 | }; 75 | -------------------------------------------------------------------------------- /src/util/config-util.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import fs from '@/util/fs'; 3 | import { resolve } from 'path'; 4 | import Config from '@/models/config'; 5 | 6 | const readConfigFile = async (configFile: string): Promise => { 7 | const currentPath = process.cwd(); 8 | try { 9 | return await fs.readFile(resolve(currentPath, `./${configFile}`), 'utf8'); 10 | } catch (error) { 11 | throw new Error(`在当前目录 ${currentPath} 没有找到${configFile}配置文件`); 12 | } 13 | }; 14 | 15 | const parseStrToConfig = (configFile: string, str: string): Config => { 16 | try { 17 | return JSON.parse(str) as Config; 18 | } catch (error) { 19 | throw new Error(`解析${configFile}配置文件出错,不是正确的 JSON 格式。`); 20 | } 21 | }; 22 | 23 | const processConfigPath = (config: Config): Config => { 24 | config.raml = resolve(config.raml); 25 | config.controller = resolve(config.controller); 26 | if (Array.isArray(config.plugins)) { 27 | config.plugins = config.plugins.map((plugin) => resolve(plugin)); 28 | } 29 | return config; 30 | }; 31 | 32 | export const loadConfig = async (): Promise => { 33 | try { 34 | const configFile = '.raml-config.json'; 35 | const str = await readConfigFile(configFile); 36 | const config: Config = parseStrToConfig(configFile, str); 37 | return processConfigPath(config); 38 | } catch (error) { 39 | if (error && error instanceof Error) { 40 | // eslint-disable-next-line no-console 41 | console.log(chalk`{red ${error.message}}`); 42 | } 43 | process.exit(1); 44 | return Promise.reject(error); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /src/util/fs.ts: -------------------------------------------------------------------------------- 1 | import { readFile, appendFile } from 'fs'; 2 | import { promisify } from 'util'; 3 | 4 | export default { 5 | readFile: promisify(readFile), 6 | appendFile: promisify(appendFile), 7 | }; 8 | -------------------------------------------------------------------------------- /src/util/index.ts: -------------------------------------------------------------------------------- 1 | import vm from 'vm'; 2 | import Config from '@/models/config'; 3 | import RestAPI from '@/models/rest-api'; 4 | import pathToRegexp from 'path-to-regexp'; 5 | 6 | export const isRedirectCode = (code: number): boolean => code >= 300 && code < 400; 7 | 8 | export const jsonPath = (json: unknown, dataPath: string): unknown => { 9 | const sandbox = { 10 | obj: '', 11 | result: undefined, 12 | }; 13 | vm.createContext(sandbox); 14 | const code = `obj = ${JSON.stringify(json)}; result = obj${dataPath}`; 15 | vm.runInContext(code, sandbox); 16 | return sandbox.result; 17 | }; 18 | 19 | const uriParamterRegExp = /\{((?:.|\n)+?)\}/g; 20 | 21 | export const replaceUriParameters = (uri: string, callback) => 22 | uri.replace(uriParamterRegExp, callback); 23 | 24 | export const toExpressUri = (uri: string): string => { 25 | let result = uri; 26 | replaceUriParameters(uri, (match, expression: string) => { 27 | result = result.replace(match, `:${expression}`); 28 | }); 29 | return result; 30 | }; 31 | 32 | export const getHost = (config: Config): string => { 33 | const env = process.env.NODE_ENV; 34 | let host = `http://localhost:${config.port}`; 35 | if (config.runner && env) { 36 | host = config.runner[env] as string; 37 | } 38 | if (!host) { 39 | throw Error(`Can't find host in .raml-config.json when env is "${env}"`); 40 | } 41 | return host; 42 | }; 43 | 44 | // copy from: https://github.com/sindresorhus/indent-string 45 | export const indentString = ( 46 | str: string, 47 | count = 1, 48 | opts: object = { indent: ' ', includeEmptyLines: false }, 49 | ): string => { 50 | // Support older versions: use the third parameter as options.indent 51 | // TODO: Remove the workaround in the next major version 52 | const options = 53 | typeof opts === 'object' && 54 | Object.assign({ indent: ' ', includeEmptyLines: false }, opts); 55 | count = count === undefined ? 1 : count; 56 | 57 | if (typeof options.indent !== 'string') { 58 | throw new TypeError( 59 | `Expected \`options.indent\` to be a \`string\`, got \`${typeof options.indent}\``, 60 | ); 61 | } 62 | 63 | if (count === 0) { 64 | return str; 65 | } 66 | 67 | const regex = options.includeEmptyLines ? /^/gm : /^(?!\s*$)/gm; 68 | return str.replace(regex, options.indent.repeat(count)); 69 | }; 70 | 71 | export const mergeRestApi = ( 72 | newRestAPIArr: RestAPI[], 73 | existRestAPIArr: RestAPI[], 74 | ): RestAPI[] => newRestAPIArr 75 | .map((restAPI) => { 76 | let result: RestAPI; 77 | existRestAPIArr.forEach((existRestApi) => { 78 | const urlMap = urlCompare(restAPI.url, existRestApi.url); 79 | if (!urlMap) { 80 | return; 81 | } 82 | restAPI.url = existRestApi.url; 83 | restAPI.uriParameters = urlMap; 84 | restAPI.description = existRestApi.description; 85 | result = restAPI; 86 | }); 87 | return result; 88 | }) 89 | .filter((restAPI) => !!restAPI); 90 | 91 | export const urlCompare = (url: string, ramlUrlExpression: string): object => { 92 | const urlExpression = toExpressUri(ramlUrlExpression); 93 | const keys = []; 94 | const regexp = pathToRegexp(urlExpression, keys); 95 | 96 | if (!regexp.test(url)) { 97 | return; 98 | } 99 | const result = regexp.exec(url); 100 | const uriMap = {}; 101 | keys.forEach((key, i) => { 102 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 103 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call 104 | uriMap[key.name.trim()] = result[i + 1]; 105 | }); 106 | return uriMap; 107 | }; 108 | 109 | export const isJSONType = (mimeType: string): boolean => /\/json/.test(mimeType); 110 | -------------------------------------------------------------------------------- /src/validate.ts: -------------------------------------------------------------------------------- 1 | import Ajv from 'ajv'; 2 | import { jsonPath } from './util'; 3 | import Schema from './models/schema'; 4 | 5 | const buildErrorMessage = (error: Ajv.ErrorObject, data): string => { 6 | let msg = ''; 7 | let result = data; 8 | const { message, dataPath } = error; 9 | msg = message; 10 | if (dataPath) { 11 | result = jsonPath(data, dataPath); 12 | } 13 | msg += `\ninfo:${dataPath}\n${JSON.stringify(result, null, '\t')}\n`; 14 | return msg; 15 | }; 16 | 17 | export default class SchemaValidate { 18 | private ajv: Ajv.Ajv; 19 | 20 | constructor(definitionSchema: Schema) { 21 | const ajv = new Ajv(); 22 | this.ajv = ajv.addSchema(definitionSchema); 23 | } 24 | 25 | execute(schema: Schema, data): boolean { 26 | let validate: Ajv.ValidateFunction; 27 | try { 28 | validate = this.ajv.compile(schema); 29 | } catch (error) { 30 | if (error instanceof Ajv.MissingRefError) { 31 | throw Error(`Missing custom type "${error.missingRef.split('/').pop()}"`); 32 | } 33 | throw error; 34 | } 35 | 36 | const valid = validate(data) as boolean; 37 | if (!valid) { 38 | const error = validate.errors.pop(); 39 | throw new Error(buildErrorMessage(error, data)); 40 | } 41 | return valid; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/filter-path.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import filterPath from '@/har-convert/filter-path'; 3 | import { HarEntry } from '@/models/harTypes'; 4 | 5 | test('Given entries, then get filter path entries', async (t) => { 6 | // tslint:disable:max-line-length 7 | const entries: HarEntry[] = [ 8 | { 9 | request: { 10 | method: 'GET', 11 | url: 'http://abc.com/', 12 | queryString: [], 13 | }, 14 | response: { 15 | status: 200, 16 | content: { 17 | mimeType: 'text/html', 18 | text: 19 | '\n\n \n \n \n \n \n oneweb-ta\n \n \n \n
\n \n \n\n', 20 | }, 21 | redirectURL: '', 22 | }, 23 | }, 24 | { 25 | request: { 26 | method: 'GET', 27 | url: 'http://localhost:8080/', 28 | queryString: [], 29 | }, 30 | response: { 31 | status: 200, 32 | content: { 33 | mimeType: 'text/html', 34 | text: 35 | 'hello one!', 36 | }, 37 | redirectURL: '', 38 | }, 39 | }, 40 | ]; 41 | 42 | const expectResult = [{ 43 | request: { 44 | method: 'GET', 45 | url: 'http://localhost:8080/', 46 | queryString: [], 47 | }, 48 | response: { 49 | status: 200, 50 | content: { 51 | mimeType: 'text/html', 52 | text: 53 | 'hello one!', 54 | }, 55 | redirectURL: '', 56 | }, 57 | }]; 58 | 59 | const data = filterPath(entries, 'localhost'); 60 | t.deepEqual(data, expectResult); 61 | }); 62 | -------------------------------------------------------------------------------- /test/har-convert/api.raml: -------------------------------------------------------------------------------- 1 | #%RAML 1.0 2 | --- 3 | baseUri: / 4 | mediaType: application/json 5 | 6 | /api/test/raml/orders/T012019011828586: 7 | get: 8 | description: get_api_test_raml_orders_T012019011828586 9 | queryParameters: 10 | param1: 11 | example: value1 12 | responses: 13 | 200: 14 | body: 15 | example: | 16 | {"name":"你好"} 17 | -------------------------------------------------------------------------------- /test/har-convert/har-convert.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import path from 'path'; 3 | import fs from '@/util/fs'; 4 | import sinon from 'sinon'; 5 | import RestAPI from '@/models/rest-api'; 6 | import { read, mergeRestApiToSpec, save } from '@/har-convert'; 7 | 8 | test('Given a har file, then get xhr request arr', async (t) => { 9 | const restAPIs: RestAPI[] = [ 10 | { 11 | url: '/api/test/raml/orders/T012019011828586', 12 | description: 'get_api_test_raml_orders_T012019011828586', 13 | method: 'GET', 14 | queryParameters: [{ 15 | name: 'param1', 16 | example: ':hello:city:Beijing:model:', 17 | }], 18 | responses: [ 19 | { 20 | code: 200, 21 | body: { 22 | mimeType: 'application/json', 23 | text: '{"name":"你好"}', 24 | }, 25 | }, 26 | ], 27 | }, { 28 | url: '/api/test/raml/orders/T012019011828586/redeem', 29 | description: 'post_api_test_raml_orders_T012019011828586_redeem', 30 | method: 'POST', 31 | queryParameters: [], 32 | body: { 33 | mimeType: 'application/json;charset=UTF-8', 34 | text: '{"a":1,"b":2}', 35 | }, 36 | responses: [{ 37 | code: 200, 38 | body: { 39 | mimeType: 'application/json', 40 | text: '{}', 41 | }, 42 | }], 43 | }, 44 | ]; 45 | const data = await fs.readFile(`${__dirname}/localhost.har`, 'utf8'); 46 | const result = read(data); 47 | t.deepEqual(result, restAPIs); 48 | }); 49 | 50 | 51 | test('Given a har file and use filter, When read Then get filter arr', async (t) => { 52 | const restAPIs: RestAPI[] = [ 53 | { 54 | url: '/api/test/raml/orders/T012019011828586/redeem', 55 | description: 'post_api_test_raml_orders_T012019011828586_redeem', 56 | method: 'POST', 57 | queryParameters: [], 58 | body: { 59 | mimeType: 'application/json;charset=UTF-8', 60 | text: '{"a":1,"b":2}', 61 | }, 62 | responses: [{ 63 | code: 200, 64 | body: { 65 | mimeType: 'application/json', 66 | text: '{}', 67 | }, 68 | }], 69 | }, 70 | ]; 71 | const data = await fs.readFile(`${__dirname}/localhost.har`, 'utf8'); 72 | const result = read(data, '/orders/T012019011828586/redeem'); 73 | t.deepEqual(result, restAPIs); 74 | }); 75 | 76 | 77 | test.serial('Given restAPIs, When mergeRestApiToSpec Then got spec str', async (t) => { 78 | const restAPIs: RestAPI[] = [ 79 | { 80 | url: '/api/test/raml/orders/T012019011828586', 81 | description: 'get_api_test_raml_orders_T012019011828586', 82 | method: 'GET', 83 | queryParameters: [{ 84 | name: 'param1', 85 | example: 'value2', 86 | }], 87 | responses: [ 88 | { 89 | code: 200, 90 | body: { 91 | mimeType: 'application/json', 92 | text: '{"name":"你好"}', 93 | }, 94 | }, 95 | ], 96 | }, 97 | ]; 98 | 99 | const expectResult = ` 100 | const assert = require('assert'); 101 | const { loadApi } = require('@xbl/raml-mocker'); 102 | 103 | it('Case Name', async () => { 104 | const getFn0 = loadApi('get_api_test_raml_orders_T012019011828586'); 105 | const { status: status0, data: data0 } = await getFn0({},{"param1":"value2"},{}); 106 | 107 | assert.equal(status0, 200); 108 | // TODO: assert 109 | 110 | }); 111 | `.trim(); 112 | 113 | const template = await fs.readFile(path.resolve(__dirname, '../../src/har-convert/template/test.spec.ejs')); 114 | const callback = sinon.stub(); 115 | callback.onCall(0).resolves(` 116 | { 117 | "controller": "./controller", 118 | "raml": "./test/har-convert", 119 | "main": "api.raml", 120 | "port": 3000, 121 | "runner": { 122 | "test": "http://localhost:3000" 123 | } 124 | } 125 | `); 126 | callback.onCall(1).resolves(template); 127 | sinon.replace(fs, 'readFile', callback); 128 | 129 | const result = await mergeRestApiToSpec(restAPIs); 130 | t.is(expectResult, result.trim()); 131 | sinon.restore(); 132 | }); 133 | 134 | test.serial('Given restAPIs, When save target is js Then appendFile spec str', async (t) => { 135 | const restAPIs: RestAPI[] = [ 136 | { 137 | url: '/api/test/raml/orders/T012019011828586', 138 | description: 'get_api_test_raml_orders_T012019011828586', 139 | method: 'GET', 140 | queryParameters: [{ 141 | name: 'param1', 142 | example: 'value2', 143 | }], 144 | responses: [ 145 | { 146 | code: 200, 147 | body: { 148 | mimeType: 'application/json', 149 | text: '{"name":"你好"}', 150 | }, 151 | }, 152 | ], 153 | }, 154 | ]; 155 | 156 | const expectResult = ` 157 | const assert = require('assert'); 158 | const { loadApi } = require('@xbl/raml-mocker'); 159 | 160 | it('Case Name', async () => { 161 | const getFn0 = loadApi('get_api_test_raml_orders_T012019011828586'); 162 | const { status: status0, data: data0 } = await getFn0({},{"param1":"value2"},{}); 163 | 164 | assert.equal(status0, 200); 165 | // TODO: assert 166 | 167 | }); 168 | `.trim(); 169 | 170 | const template = await fs.readFile(path.resolve(__dirname, '../../src/har-convert/template/test.spec.ejs')); 171 | const callback = sinon.stub(); 172 | callback.onCall(0).resolves(` 173 | { 174 | "controller": "./controller", 175 | "raml": "./test/har-convert", 176 | "main": "api.raml", 177 | "port": 3000, 178 | "runner": { 179 | "test": "http://localhost:3000" 180 | } 181 | } 182 | `); 183 | callback.onCall(1).resolves(template); 184 | sinon.replace(fs, 'readFile', callback); 185 | 186 | const target = '1.js'; 187 | sinon.replace(fs, 'appendFile', (fileTarget, str) => { 188 | t.is(fileTarget, target); 189 | t.is(str, expectResult); 190 | }); 191 | 192 | await save(restAPIs, target); 193 | sinon.restore(); 194 | }); 195 | 196 | test.serial('Given restAPIs, When save target is raml Then appendFile raml str', async (t) => { 197 | const restAPIs: RestAPI[] = [ 198 | { 199 | url: '/api/test/raml/orders/T012019011828586', 200 | description: 'get_api_test_raml_orders_T012019011828586', 201 | method: 'GET', 202 | queryParameters: [{ 203 | name: 'param1', 204 | example: 'value1', 205 | }], 206 | responses: [ 207 | { 208 | code: 200, 209 | body: { 210 | mimeType: 'application/json', 211 | text: '{"name":"你好"}', 212 | }, 213 | }, 214 | ], 215 | }, 216 | ]; 217 | 218 | const expectResult = ` 219 | /api/test/raml/orders/T012019011828586: 220 | get: 221 | description: get_api_test_raml_orders_T012019011828586 222 | queryParameters: 223 | param1: 224 | example: value1 225 | responses: 226 | 200: 227 | body: 228 | example: | 229 | {"name":"你好"} 230 | `.trim(); 231 | 232 | const template = await fs.readFile(path.resolve(__dirname, '../../src/har-convert/template/api.ejs')); 233 | sinon.replace(fs, 'readFile', sinon.stub().resolves(template)); 234 | 235 | const target = '1.raml'; 236 | sinon.replace(fs, 'appendFile', (fileTarget, str) => { 237 | t.is(fileTarget, target); 238 | t.is(str, expectResult); 239 | }); 240 | 241 | await save(restAPIs, target); 242 | sinon.restore(); 243 | }); 244 | -------------------------------------------------------------------------------- /test/har-convert/localhost.har: -------------------------------------------------------------------------------- 1 | { 2 | "log": { 3 | "version": "1.2", 4 | "creator": { 5 | "name": "WebInspector", 6 | "version": "537.36" 7 | }, 8 | "pages": [ 9 | { 10 | "startedDateTime": "2019-02-11T07:56:23.303Z", 11 | "id": "page_2", 12 | "title": "http://localhost:8080/", 13 | "pageTimings": { 14 | "onContentLoad": 394.50199995189905, 15 | "onLoad": 650.7139999885112 16 | } 17 | } 18 | ], 19 | "entries": [ 20 | { 21 | "startedDateTime": "2019-02-11T07:56:23.303Z", 22 | "time": 8.63599986769259, 23 | "request": { 24 | "method": "GET", 25 | "url": "http://localhost:8080/", 26 | "httpVersion": "HTTP/1.1", 27 | "headers": [ 28 | { 29 | "name": "Host", 30 | "value": "localhost:8080" 31 | }, 32 | { 33 | "name": "Connection", 34 | "value": "keep-alive" 35 | }, 36 | { 37 | "name": "Pragma", 38 | "value": "no-cache" 39 | }, 40 | { 41 | "name": "Cache-Control", 42 | "value": "no-cache" 43 | }, 44 | { 45 | "name": "Upgrade-Insecure-Requests", 46 | "value": "1" 47 | }, 48 | { 49 | "name": "User-Agent", 50 | "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36" 51 | }, 52 | { 53 | "name": "Accept", 54 | "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8" 55 | }, 56 | { 57 | "name": "Accept-Encoding", 58 | "value": "gzip, deflate, br" 59 | }, 60 | { 61 | "name": "Accept-Language", 62 | "value": "zh-CN,zh;q=0.9" 63 | }, 64 | { 65 | "name": "Cookie", 66 | "value": "spy_user_id=59b8b094-df21-4f71-972e-c7a395c841e8; _smt_uid=5bde4ab1.b0d3013" 67 | } 68 | ], 69 | "queryString": [], 70 | "cookies": [ 71 | { 72 | "name": "spy_user_id", 73 | "value": "59b8b094-df21-4f71-972e-c7a395c841e8", 74 | "expires": null, 75 | "httpOnly": false, 76 | "secure": false 77 | }, 78 | { 79 | "name": "_smt_uid", 80 | "value": "5bde4ab1.b0d3013", 81 | "expires": null, 82 | "httpOnly": false, 83 | "secure": false 84 | } 85 | ], 86 | "headersSize": 518, 87 | "bodySize": 0 88 | }, 89 | "response": { 90 | "status": 200, 91 | "statusText": "OK", 92 | "httpVersion": "HTTP/1.1", 93 | "headers": [ 94 | { 95 | "name": "X-Powered-By", 96 | "value": "Express" 97 | }, 98 | { 99 | "name": "Accept-Ranges", 100 | "value": "bytes" 101 | }, 102 | { 103 | "name": "Content-Type", 104 | "value": "text/html; charset=UTF-8" 105 | }, 106 | { 107 | "name": "Content-Length", 108 | "value": "642" 109 | }, 110 | { 111 | "name": "ETag", 112 | "value": "W/\"282-fbEFbChqsuZsWsToT5LUJL49jzU\"" 113 | }, 114 | { 115 | "name": "Date", 116 | "value": "Mon, 11 Feb 2019 07:56:23 GMT" 117 | }, 118 | { 119 | "name": "Connection", 120 | "value": "keep-alive" 121 | } 122 | ], 123 | "cookies": [], 124 | "content": { 125 | "size": 642, 126 | "mimeType": "text/html", 127 | "compression": 0, 128 | "text": "\n\n \n \n \n \n \n oneweb-ta\n \n \n \n
\n \n \n\n" 129 | }, 130 | "redirectURL": "", 131 | "headersSize": 229, 132 | "bodySize": 642, 133 | "_transferSize": 871 134 | }, 135 | "cache": {}, 136 | "timings": { 137 | "blocked": 3.1470000321865084, 138 | "dns": -1, 139 | "ssl": -1, 140 | "connect": -1, 141 | "send": 0.04899999999999993, 142 | "wait": 0.9100000336170195, 143 | "receive": 4.529999801889062, 144 | "_blocked_queueing": 0.3100000321865082 145 | }, 146 | "serverIPAddress": "127.0.0.1", 147 | "_initiator": { 148 | "type": "other" 149 | }, 150 | "_priority": "VeryHigh", 151 | "connection": "4364776", 152 | "pageref": "page_2" 153 | }, 154 | { 155 | "startedDateTime": "2019-02-11T07:56:23.687Z", 156 | "time": 17.852000193670392, 157 | "request": { 158 | "method": "GET", 159 | "url": "http://localhost:8080/api/test/raml/orders/T012019011828586", 160 | "httpVersion": "HTTP/1.1", 161 | "headers": [ 162 | { 163 | "name": "Pragma", 164 | "value": "no-cache" 165 | }, 166 | { 167 | "name": "Accept-Encoding", 168 | "value": "gzip, deflate, br" 169 | }, 170 | { 171 | "name": "Host", 172 | "value": "localhost:8080" 173 | }, 174 | { 175 | "name": "Accept-Language", 176 | "value": "zh-CN,zh;q=0.9" 177 | }, 178 | { 179 | "name": "User-Agent", 180 | "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36" 181 | }, 182 | { 183 | "name": "Accept", 184 | "value": "application/json, text/plain, */*" 185 | }, 186 | { 187 | "name": "Referer", 188 | "value": "http://localhost:8080/" 189 | }, 190 | { 191 | "name": "Cookie", 192 | "value": "spy_user_id=59b8b094-df21-4f71-972e-c7a395c841e8; _smt_uid=5bde4ab1.b0d3013" 193 | }, 194 | { 195 | "name": "Connection", 196 | "value": "keep-alive" 197 | }, 198 | { 199 | "name": "Cache-Control", 200 | "value": "no-cache" 201 | } 202 | ], 203 | "queryString": [ 204 | { 205 | "name": "param1", 206 | "value": "%3Ahello%3Acity%3ABeijing%3Amodel%3A", 207 | "comment": "" 208 | } 209 | ], 210 | "cookies": [], 211 | "headersSize": 516, 212 | "bodySize": 0 213 | }, 214 | "response": { 215 | "status": 200, 216 | "statusText": "OK", 217 | "httpVersion": "HTTP/1.1", 218 | "headers": [ 219 | { 220 | "name": "date", 221 | "value": "Mon, 11 Feb 2019 07:56:23 GMT" 222 | }, 223 | { 224 | "name": "connection", 225 | "value": "close" 226 | }, 227 | { 228 | "name": "x-powered-by", 229 | "value": "Express" 230 | }, 231 | { 232 | "name": "etag", 233 | "value": "W/\"454-bBZ1i1lppVl8Rcdyj2fLjtRh1Ao\"" 234 | }, 235 | { 236 | "name": "content-length", 237 | "value": "1108" 238 | }, 239 | { 240 | "name": "content-type", 241 | "value": "application/json; charset=utf-8" 242 | } 243 | ], 244 | "cookies": [], 245 | "content": { 246 | "size": 1108, 247 | "mimeType": "application/json", 248 | "compression": 0, 249 | "text": "{\"name\":\"你好\"}" 250 | }, 251 | "redirectURL": "", 252 | "headersSize": 210, 253 | "bodySize": 1108, 254 | "_transferSize": 1318 255 | }, 256 | "cache": {}, 257 | "timings": { 258 | "blocked": 2.325000002592802, 259 | "dns": -1, 260 | "ssl": -1, 261 | "connect": -1, 262 | "send": 0.07399999999999984, 263 | "wait": 14.746999937519432, 264 | "receive": 0.7060002535581589, 265 | "_blocked_queueing": 0.45900000259280205 266 | }, 267 | "serverIPAddress": "127.0.0.1", 268 | "_initiator": { 269 | "type": "script", 270 | "stack": { 271 | "callFrames": [ 272 | { 273 | "functionName": "dispatchXhrRequest", 274 | "scriptId": "1293", 275 | "url": "webpack-internal:///./node_modules/axios/lib/adapters/xhr.js", 276 | "lineNumber": 177, 277 | "columnNumber": 12 278 | }, 279 | { 280 | "functionName": "xhrAdapter", 281 | "scriptId": "1293", 282 | "url": "webpack-internal:///./node_modules/axios/lib/adapters/xhr.js", 283 | "lineNumber": 11, 284 | "columnNumber": 9 285 | }, 286 | { 287 | "functionName": "dispatchRequest", 288 | "scriptId": "1301", 289 | "url": "webpack-internal:///./node_modules/axios/lib/core/dispatchRequest.js", 290 | "lineNumber": 58, 291 | "columnNumber": 9 292 | } 293 | ], 294 | "parent": { 295 | "description": "Promise.then", 296 | "callFrames": [ 297 | { 298 | "functionName": "request", 299 | "scriptId": "1289", 300 | "url": "webpack-internal:///./node_modules/axios/lib/core/Axios.js", 301 | "lineNumber": 50, 302 | "columnNumber": 22 303 | }, 304 | { 305 | "functionName": "Axios.(anonymous function)", 306 | "scriptId": "1289", 307 | "url": "webpack-internal:///./node_modules/axios/lib/core/Axios.js", 308 | "lineNumber": 60, 309 | "columnNumber": 16 310 | }, 311 | { 312 | "functionName": "wrap", 313 | "scriptId": "1287", 314 | "url": "webpack-internal:///./node_modules/axios/lib/helpers/bind.js", 315 | "lineNumber": 8, 316 | "columnNumber": 14 317 | }, 318 | { 319 | "functionName": "API.getOrderByCode", 320 | "scriptId": "1283", 321 | "url": "webpack-internal:///./src/api/index.ts", 322 | "lineNumber": 22, 323 | "columnNumber": 95 324 | }, 325 | { 326 | "functionName": "", 327 | "scriptId": "1401", 328 | "url": "webpack-internal:///./src/services/order-service.ts", 329 | "lineNumber": 26, 330 | "columnNumber": 94 331 | }, 332 | { 333 | "functionName": "step", 334 | "scriptId": "1252", 335 | "url": "webpack-internal:///./node_modules/tslib/tslib.es6.js", 336 | "lineNumber": 116, 337 | "columnNumber": 22 338 | }, 339 | { 340 | "functionName": "", 341 | "scriptId": "1252", 342 | "url": "webpack-internal:///./node_modules/tslib/tslib.es6.js", 343 | "lineNumber": 97, 344 | "columnNumber": 52 345 | }, 346 | { 347 | "functionName": "", 348 | "scriptId": "1252", 349 | "url": "webpack-internal:///./node_modules/tslib/tslib.es6.js", 350 | "lineNumber": 90, 351 | "columnNumber": 70 352 | }, 353 | { 354 | "functionName": "__awaiter", 355 | "scriptId": "1252", 356 | "url": "webpack-internal:///./node_modules/tslib/tslib.es6.js", 357 | "lineNumber": 86, 358 | "columnNumber": 11 359 | }, 360 | { 361 | "functionName": "get", 362 | "scriptId": "1401", 363 | "url": "webpack-internal:///./src/services/order-service.ts", 364 | "lineNumber": 22, 365 | "columnNumber": 62 366 | }, 367 | { 368 | "functionName": "", 369 | "scriptId": "1420", 370 | "url": "webpack-internal:///./node_modules/cache-loader/dist/cjs.js?!./node_modules/ts-loader/index.js?!./node_modules/cache-loader/dist/cjs.js?!./node_modules/vue-loader/lib/index.js?!./src/views/order-detail/index.vue?vue&type=script&lang=ts&", 371 | "lineNumber": 38, 372 | "columnNumber": 113 373 | }, 374 | { 375 | "functionName": "step", 376 | "scriptId": "1252", 377 | "url": "webpack-internal:///./node_modules/tslib/tslib.es6.js", 378 | "lineNumber": 116, 379 | "columnNumber": 22 380 | }, 381 | { 382 | "functionName": "", 383 | "scriptId": "1252", 384 | "url": "webpack-internal:///./node_modules/tslib/tslib.es6.js", 385 | "lineNumber": 97, 386 | "columnNumber": 52 387 | }, 388 | { 389 | "functionName": "", 390 | "scriptId": "1252", 391 | "url": "webpack-internal:///./node_modules/tslib/tslib.es6.js", 392 | "lineNumber": 90, 393 | "columnNumber": 70 394 | }, 395 | { 396 | "functionName": "__awaiter", 397 | "scriptId": "1252", 398 | "url": "webpack-internal:///./node_modules/tslib/tslib.es6.js", 399 | "lineNumber": 86, 400 | "columnNumber": 11 401 | }, 402 | { 403 | "functionName": "OrderDetailView.created", 404 | "scriptId": "1420", 405 | "url": "webpack-internal:///./node_modules/cache-loader/dist/cjs.js?!./node_modules/ts-loader/index.js?!./node_modules/cache-loader/dist/cjs.js?!./node_modules/vue-loader/lib/index.js?!./src/views/order-detail/index.vue?vue&type=script&lang=ts&", 406 | "lineNumber": 34, 407 | "columnNumber": 62 408 | }, 409 | { 410 | "functionName": "callHook", 411 | "scriptId": "1233", 412 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 413 | "lineNumber": 3023, 414 | "columnNumber": 20 415 | }, 416 | { 417 | "functionName": "Vue._init", 418 | "scriptId": "1233", 419 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 420 | "lineNumber": 4711, 421 | "columnNumber": 4 422 | }, 423 | { 424 | "functionName": "OrderDetailView", 425 | "scriptId": "1233", 426 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 427 | "lineNumber": 4877, 428 | "columnNumber": 11 429 | }, 430 | { 431 | "functionName": "createComponentInstanceForVnode", 432 | "scriptId": "1233", 433 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 434 | "lineNumber": 4385, 435 | "columnNumber": 9 436 | }, 437 | { 438 | "functionName": "init", 439 | "scriptId": "1233", 440 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 441 | "lineNumber": 4216, 442 | "columnNumber": 44 443 | }, 444 | { 445 | "functionName": "createComponent", 446 | "scriptId": "1233", 447 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 448 | "lineNumber": 5684, 449 | "columnNumber": 8 450 | }, 451 | { 452 | "functionName": "createElm", 453 | "scriptId": "1233", 454 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 455 | "lineNumber": 5631, 456 | "columnNumber": 8 457 | }, 458 | { 459 | "functionName": "createChildren", 460 | "scriptId": "1233", 461 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 462 | "lineNumber": 5759, 463 | "columnNumber": 8 464 | }, 465 | { 466 | "functionName": "createElm", 467 | "scriptId": "1233", 468 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 469 | "lineNumber": 5660, 470 | "columnNumber": 8 471 | }, 472 | { 473 | "functionName": "patch", 474 | "scriptId": "1233", 475 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 476 | "lineNumber": 6183, 477 | "columnNumber": 6 478 | }, 479 | { 480 | "functionName": "Vue._update", 481 | "scriptId": "1233", 482 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 483 | "lineNumber": 2763, 484 | "columnNumber": 18 485 | }, 486 | { 487 | "functionName": "updateComponent", 488 | "scriptId": "1233", 489 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 490 | "lineNumber": 2884, 491 | "columnNumber": 9 492 | }, 493 | { 494 | "functionName": "get", 495 | "scriptId": "1233", 496 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 497 | "lineNumber": 3254, 498 | "columnNumber": 24 499 | }, 500 | { 501 | "functionName": "Watcher", 502 | "scriptId": "1233", 503 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 504 | "lineNumber": 3243, 505 | "columnNumber": 11 506 | }, 507 | { 508 | "functionName": "mountComponent", 509 | "scriptId": "1233", 510 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 511 | "lineNumber": 2891, 512 | "columnNumber": 2 513 | }, 514 | { 515 | "functionName": "Vue.$mount", 516 | "scriptId": "1233", 517 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 518 | "lineNumber": 8066, 519 | "columnNumber": 9 520 | }, 521 | { 522 | "functionName": "init", 523 | "scriptId": "1233", 524 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 525 | "lineNumber": 4220, 526 | "columnNumber": 12 527 | }, 528 | { 529 | "functionName": "createComponent", 530 | "scriptId": "1233", 531 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 532 | "lineNumber": 5684, 533 | "columnNumber": 8 534 | }, 535 | { 536 | "functionName": "createElm", 537 | "scriptId": "1233", 538 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 539 | "lineNumber": 5631, 540 | "columnNumber": 8 541 | }, 542 | { 543 | "functionName": "patch", 544 | "scriptId": "1233", 545 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 546 | "lineNumber": 6222, 547 | "columnNumber": 8 548 | }, 549 | { 550 | "functionName": "Vue._update", 551 | "scriptId": "1233", 552 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 553 | "lineNumber": 2763, 554 | "columnNumber": 18 555 | }, 556 | { 557 | "functionName": "updateComponent", 558 | "scriptId": "1233", 559 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 560 | "lineNumber": 2884, 561 | "columnNumber": 9 562 | }, 563 | { 564 | "functionName": "get", 565 | "scriptId": "1233", 566 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 567 | "lineNumber": 3254, 568 | "columnNumber": 24 569 | }, 570 | { 571 | "functionName": "Watcher", 572 | "scriptId": "1233", 573 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 574 | "lineNumber": 3243, 575 | "columnNumber": 11 576 | }, 577 | { 578 | "functionName": "mountComponent", 579 | "scriptId": "1233", 580 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 581 | "lineNumber": 2891, 582 | "columnNumber": 2 583 | }, 584 | { 585 | "functionName": "Vue.$mount", 586 | "scriptId": "1233", 587 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 588 | "lineNumber": 8066, 589 | "columnNumber": 9 590 | }, 591 | { 592 | "functionName": "", 593 | "scriptId": "1232", 594 | "url": "webpack-internal:///./src/main.ts", 595 | "lineNumber": 17, 596 | "columnNumber": 3 597 | }, 598 | { 599 | "functionName": "./src/main.ts", 600 | "scriptId": "1202", 601 | "url": "http://localhost:8080/app.js", 602 | "lineNumber": 2817, 603 | "columnNumber": 0 604 | }, 605 | { 606 | "functionName": "__webpack_require__", 607 | "scriptId": "1202", 608 | "url": "http://localhost:8080/app.js", 609 | "lineNumber": 723, 610 | "columnNumber": 29 611 | }, 612 | { 613 | "functionName": "fn", 614 | "scriptId": "1202", 615 | "url": "http://localhost:8080/app.js", 616 | "lineNumber": 100, 617 | "columnNumber": 19 618 | }, 619 | { 620 | "functionName": "0", 621 | "scriptId": "1202", 622 | "url": "http://localhost:8080/app.js", 623 | "lineNumber": 3730, 624 | "columnNumber": 17 625 | }, 626 | { 627 | "functionName": "__webpack_require__", 628 | "scriptId": "1202", 629 | "url": "http://localhost:8080/app.js", 630 | "lineNumber": 723, 631 | "columnNumber": 29 632 | }, 633 | { 634 | "functionName": "", 635 | "scriptId": "1202", 636 | "url": "http://localhost:8080/app.js", 637 | "lineNumber": 790, 638 | "columnNumber": 36 639 | }, 640 | { 641 | "functionName": "", 642 | "scriptId": "1202", 643 | "url": "http://localhost:8080/app.js", 644 | "lineNumber": 793, 645 | "columnNumber": 9 646 | } 647 | ] 648 | } 649 | } 650 | }, 651 | "_priority": "High", 652 | "connection": "4364776", 653 | "pageref": "page_2" 654 | }, 655 | { 656 | "startedDateTime": "2019-02-11T07:56:28.344Z", 657 | "time": 8.38600005954504, 658 | "request": { 659 | "method": "POST", 660 | "url": "http://localhost:8080/api/test/raml/orders/T012019011828586/redeem", 661 | "httpVersion": "HTTP/1.1", 662 | "headers": [ 663 | { 664 | "name": "Pragma", 665 | "value": "no-cache" 666 | }, 667 | { 668 | "name": "Origin", 669 | "value": "http://localhost:8080" 670 | }, 671 | { 672 | "name": "Accept-Encoding", 673 | "value": "gzip, deflate, br" 674 | }, 675 | { 676 | "name": "Host", 677 | "value": "localhost:8080" 678 | }, 679 | { 680 | "name": "Accept-Language", 681 | "value": "zh-CN,zh;q=0.9" 682 | }, 683 | { 684 | "name": "User-Agent", 685 | "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36" 686 | }, 687 | { 688 | "name": "Accept", 689 | "value": "application/json, text/plain, */*" 690 | }, 691 | { 692 | "name": "Cache-Control", 693 | "value": "no-cache" 694 | }, 695 | { 696 | "name": "Referer", 697 | "value": "http://localhost:8080/" 698 | }, 699 | { 700 | "name": "Cookie", 701 | "value": "spy_user_id=59b8b094-df21-4f71-972e-c7a395c841e8; _smt_uid=5bde4ab1.b0d3013" 702 | }, 703 | { 704 | "name": "Connection", 705 | "value": "keep-alive" 706 | }, 707 | { 708 | "name": "Content-Length", 709 | "value": "0" 710 | } 711 | ], 712 | "queryString": [], 713 | "cookies": [], 714 | "headersSize": 578, 715 | "bodySize": 0, 716 | "postData": { 717 | "mimeType": "application/json;charset=UTF-8", 718 | "text": "{\"a\":1,\"b\":2}" 719 | } 720 | }, 721 | "response": { 722 | "status": 200, 723 | "statusText": "OK", 724 | "httpVersion": "HTTP/1.1", 725 | "headers": [ 726 | { 727 | "name": "date", 728 | "value": "Mon, 11 Feb 2019 07:56:28 GMT" 729 | }, 730 | { 731 | "name": "connection", 732 | "value": "close" 733 | }, 734 | { 735 | "name": "x-powered-by", 736 | "value": "Express" 737 | }, 738 | { 739 | "name": "etag", 740 | "value": "W/\"2-vyGp6PvFo4RvsFtPoIWeCReyIC8\"" 741 | }, 742 | { 743 | "name": "content-length", 744 | "value": "2" 745 | }, 746 | { 747 | "name": "content-type", 748 | "value": "application/json; charset=utf-8" 749 | } 750 | ], 751 | "cookies": [], 752 | "content": { 753 | "size": 2, 754 | "mimeType": "application/json", 755 | "compression": 0, 756 | "text": "{}" 757 | }, 758 | "redirectURL": "", 759 | "headersSize": 205, 760 | "bodySize": 2, 761 | "_transferSize": 207 762 | }, 763 | "cache": {}, 764 | "timings": { 765 | "blocked": 2.4360000699460507, 766 | "dns": -1, 767 | "ssl": -1, 768 | "connect": -1, 769 | "send": 0.09299999999999997, 770 | "wait": 5.207999937236309, 771 | "receive": 0.6490000523626804, 772 | "_blocked_queueing": 0.5290000699460506 773 | }, 774 | "serverIPAddress": "127.0.0.1", 775 | "_initiator": { 776 | "type": "script", 777 | "stack": { 778 | "callFrames": [ 779 | { 780 | "functionName": "dispatchXhrRequest", 781 | "scriptId": "1293", 782 | "url": "webpack-internal:///./node_modules/axios/lib/adapters/xhr.js", 783 | "lineNumber": 177, 784 | "columnNumber": 12 785 | }, 786 | { 787 | "functionName": "xhrAdapter", 788 | "scriptId": "1293", 789 | "url": "webpack-internal:///./node_modules/axios/lib/adapters/xhr.js", 790 | "lineNumber": 11, 791 | "columnNumber": 9 792 | }, 793 | { 794 | "functionName": "dispatchRequest", 795 | "scriptId": "1301", 796 | "url": "webpack-internal:///./node_modules/axios/lib/core/dispatchRequest.js", 797 | "lineNumber": 58, 798 | "columnNumber": 9 799 | } 800 | ], 801 | "parent": { 802 | "description": "Promise.then", 803 | "callFrames": [ 804 | { 805 | "functionName": "request", 806 | "scriptId": "1289", 807 | "url": "webpack-internal:///./node_modules/axios/lib/core/Axios.js", 808 | "lineNumber": 50, 809 | "columnNumber": 22 810 | }, 811 | { 812 | "functionName": "Axios.(anonymous function)", 813 | "scriptId": "1289", 814 | "url": "webpack-internal:///./node_modules/axios/lib/core/Axios.js", 815 | "lineNumber": 70, 816 | "columnNumber": 16 817 | }, 818 | { 819 | "functionName": "wrap", 820 | "scriptId": "1287", 821 | "url": "webpack-internal:///./node_modules/axios/lib/helpers/bind.js", 822 | "lineNumber": 8, 823 | "columnNumber": 14 824 | }, 825 | { 826 | "functionName": "API.redeem", 827 | "scriptId": "1283", 828 | "url": "webpack-internal:///./src/api/index.ts", 829 | "lineNumber": 20, 830 | "columnNumber": 60 831 | }, 832 | { 833 | "functionName": "", 834 | "scriptId": "1401", 835 | "url": "webpack-internal:///./src/services/order-service.ts", 836 | "lineNumber": 65, 837 | "columnNumber": 94 838 | }, 839 | { 840 | "functionName": "step", 841 | "scriptId": "1252", 842 | "url": "webpack-internal:///./node_modules/tslib/tslib.es6.js", 843 | "lineNumber": 116, 844 | "columnNumber": 22 845 | }, 846 | { 847 | "functionName": "", 848 | "scriptId": "1252", 849 | "url": "webpack-internal:///./node_modules/tslib/tslib.es6.js", 850 | "lineNumber": 97, 851 | "columnNumber": 52 852 | }, 853 | { 854 | "functionName": "", 855 | "scriptId": "1252", 856 | "url": "webpack-internal:///./node_modules/tslib/tslib.es6.js", 857 | "lineNumber": 90, 858 | "columnNumber": 70 859 | }, 860 | { 861 | "functionName": "__awaiter", 862 | "scriptId": "1252", 863 | "url": "webpack-internal:///./node_modules/tslib/tslib.es6.js", 864 | "lineNumber": 86, 865 | "columnNumber": 11 866 | }, 867 | { 868 | "functionName": "sendRedeem", 869 | "scriptId": "1401", 870 | "url": "webpack-internal:///./src/services/order-service.ts", 871 | "lineNumber": 61, 872 | "columnNumber": 62 873 | }, 874 | { 875 | "functionName": "OrderDetailView.sendRedeem", 876 | "scriptId": "1420", 877 | "url": "webpack-internal:///./node_modules/cache-loader/dist/cjs.js?!./node_modules/ts-loader/index.js?!./node_modules/cache-loader/dist/cjs.js?!./node_modules/vue-loader/lib/index.js?!./src/views/order-detail/index.vue?vue&type=script&lang=ts&", 878 | "lineNumber": 81, 879 | "columnNumber": 72 880 | }, 881 | { 882 | "functionName": "invoker", 883 | "scriptId": "1233", 884 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 885 | "lineNumber": 2117, 886 | "columnNumber": 17 887 | }, 888 | { 889 | "functionName": "Vue.$emit", 890 | "scriptId": "1233", 891 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 892 | "lineNumber": 2637, 893 | "columnNumber": 17 894 | }, 895 | { 896 | "functionName": "emit", 897 | "scriptId": "1253", 898 | "url": "webpack-internal:///./node_modules/vue-property-decorator/lib/vue-property-decorator.js", 899 | "lineNumber": 117, 900 | "columnNumber": 28 901 | }, 902 | { 903 | "functionName": "emitter", 904 | "scriptId": "1253", 905 | "url": "webpack-internal:///./node_modules/vue-property-decorator/lib/vue-property-decorator.js", 906 | "lineNumber": 126, 907 | "columnNumber": 16 908 | }, 909 | { 910 | "functionName": "invoker", 911 | "scriptId": "1233", 912 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 913 | "lineNumber": 2117, 914 | "columnNumber": 17 915 | }, 916 | { 917 | "functionName": "fn._withTask.fn._withTask", 918 | "scriptId": "1233", 919 | "url": "webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js", 920 | "lineNumber": 1902, 921 | "columnNumber": 16 922 | } 923 | ] 924 | } 925 | } 926 | }, 927 | "_priority": "High", 928 | "connection": "4364778", 929 | "pageref": "page_2" 930 | } 931 | ] 932 | } 933 | } 934 | -------------------------------------------------------------------------------- /test/output.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import Output from '@/output'; 3 | import OutputRequest from '@/models/output-request'; 4 | 5 | test('When message one line , then title is message', (t) => { 6 | const message = 'abc'; 7 | const [, title] = Output.processMessage(message); 8 | t.is(title, message); 9 | }); 10 | 11 | test('When message multiple line , then title is first line', (t) => { 12 | const expectTitle = 'the first line'; 13 | const expectValidInfo = `and this is next line 14 | and this is last line`; 15 | const message = `${expectTitle} 16 | ${expectValidInfo}`; 17 | const [, title, validInfo] = Output.processMessage(message); 18 | t.is(title, expectTitle); 19 | t.is(validInfo, expectValidInfo); 20 | }); 21 | 22 | test('Give push error , then get failCount 1', (t) => { 23 | const failCount = 1; 24 | const output = new Output('host'); 25 | const outputRequest: OutputRequest = 26 | new OutputRequest({ path: '', method: '' }); 27 | output.push(Output.ERROR, outputRequest); 28 | t.is(output.failCount, failCount); 29 | t.is(output.successCount, 0); 30 | 31 | output.push(Output.SUCCESS, outputRequest); 32 | t.is(output.successCount, 1); 33 | }); 34 | -------------------------------------------------------------------------------- /test/parameter.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import Parameter from '@/models/parameter'; 3 | 4 | test('When Parameter.build, Give json object, Then get Parameter Array', (t) => { 5 | const json = { 6 | a: 1, 7 | b: 2, 8 | }; 9 | const expectResult: Parameter[] = [{ 10 | name: 'a', 11 | example: 1, 12 | }, { 13 | name: 'b', 14 | example: 2, 15 | }]; 16 | const result = Parameter.build(json); 17 | t.deepEqual(result, expectResult); 18 | }); 19 | 20 | test('When Parameter.toJSON, Give get Parameter Array, Then json object', (t) => { 21 | const expectJson = { 22 | a: 1, 23 | b: 2, 24 | }; 25 | const arr: Parameter[] = [{ 26 | name: 'a', 27 | example: 1, 28 | }, { 29 | name: 'b', 30 | example: 2, 31 | }]; 32 | const result = Parameter.toJSON(arr); 33 | t.deepEqual(result, expectJson); 34 | }); 35 | -------------------------------------------------------------------------------- /test/read-raml/definition-schema.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { parseRAMLSync } from 'raml-1-parser'; 3 | import { Api } from 'raml-1-parser/dist/parser/artifacts/raml10parserapi'; 4 | import { getDefinitionSchema } from '@/read-raml/definition-schema'; 5 | 6 | test('Given read raml and Product type When getDefinitionSchema Then get object', (t) => { 7 | const definitionSchema = { 8 | $id: '/definitionSchema', 9 | definitions: { 10 | Product: { 11 | type: 'object', 12 | properties: { 13 | productId: { 14 | type: ['string'], 15 | }, 16 | name: { 17 | type: ['number'], 18 | }, 19 | }, 20 | required: ['productId', 'name'], 21 | }, 22 | }, 23 | }; 24 | const ramlStr = ` 25 | #%RAML 1.0 26 | --- 27 | types: 28 | Product: 29 | type: object 30 | properties: 31 | productId: 32 | type: string 33 | name: number 34 | `; 35 | const apiJSON = parseRAMLSync(ramlStr) as Api; 36 | t.deepEqual(getDefinitionSchema(apiJSON), definitionSchema); 37 | }); 38 | 39 | test(`Given read raml And Product.productId no required 40 | When getDefinitionSchema Then get definitionSchema object`, (t) => { 41 | const definitionSchema = { 42 | $id: '/definitionSchema', 43 | definitions: { 44 | Product: { 45 | type: 'object', 46 | properties: { 47 | productId: { 48 | type: ['string'], 49 | }, 50 | name: { 51 | type: ['number'], 52 | }, 53 | }, 54 | required: ['name'], 55 | }, 56 | }, 57 | }; 58 | const ramlStr = ` 59 | #%RAML 1.0 60 | --- 61 | types: 62 | Product: 63 | type: object 64 | properties: 65 | productId: 66 | type: string 67 | required: false 68 | name: number 69 | `; 70 | const apiJSON = parseRAMLSync(ramlStr) as Api; 71 | t.deepEqual(getDefinitionSchema(apiJSON), definitionSchema); 72 | }); 73 | 74 | test('Given read raml And Product.productId has minLength When definitionSchema then got object', (t) => { 75 | const definitionSchema = { 76 | $id: '/definitionSchema', 77 | definitions: { 78 | Product: { 79 | type: 'object', 80 | properties: { 81 | productId: { 82 | type: ['string'], 83 | minLength: 1, 84 | maxLength: 22, 85 | }, 86 | name: { 87 | type: ['number'], 88 | }, 89 | }, 90 | required: ['name'], 91 | }, 92 | }, 93 | }; 94 | const ramlStr = ` 95 | #%RAML 1.0 96 | --- 97 | types: 98 | Product: 99 | type: object 100 | properties: 101 | productId: 102 | type: string 103 | required: false 104 | minLength: 1 105 | maxLength: 22 106 | name: number 107 | `; 108 | const apiJSON = parseRAMLSync(ramlStr) as Api; 109 | t.deepEqual(getDefinitionSchema(apiJSON), definitionSchema); 110 | }); 111 | 112 | test('when read raml given Product and Paragraph then get definitionSchema object', (t) => { 113 | const definitionSchema = { 114 | $id: '/definitionSchema', 115 | definitions: { 116 | Product: { 117 | type: 'object', 118 | properties: { 119 | productId: { 120 | type: ['string'], 121 | minLength: 1, 122 | maxLength: 22, 123 | }, 124 | name: { 125 | type: ['number'], 126 | }, 127 | }, 128 | required: ['name'], 129 | }, 130 | Paragraph: { 131 | type: 'object', 132 | properties: { 133 | title: { 134 | type: ['string'], 135 | }, 136 | text: { 137 | type: ['string'], 138 | }, 139 | }, 140 | required: ['title', 'text'], 141 | }, 142 | }, 143 | }; 144 | const ramlStr = ` 145 | #%RAML 1.0 146 | --- 147 | types: 148 | Product: 149 | type: object 150 | properties: 151 | productId: 152 | type: string 153 | required: false 154 | minLength: 1 155 | maxLength: 22 156 | name: number 157 | 158 | Paragraph: 159 | type: object 160 | properties: 161 | title: 162 | text: 163 | `; 164 | const apiJSON = parseRAMLSync(ramlStr) as Api; 165 | t.deepEqual(getDefinitionSchema(apiJSON), definitionSchema); 166 | }); 167 | 168 | test('Given read raml And Product of Paragraph When definitionSchema Then get object', (t) => { 169 | const definitionSchema = { 170 | $id: '/definitionSchema', 171 | definitions: { 172 | Product: { 173 | type: 'object', 174 | properties: { 175 | productId: { 176 | type: ['string'], 177 | minLength: 1, 178 | maxLength: 22, 179 | }, 180 | name: { 181 | type: ['number'], 182 | }, 183 | }, 184 | required: ['name'], 185 | }, 186 | Paragraph: { 187 | type: 'object', 188 | properties: { 189 | title: { 190 | type: ['string'], 191 | }, 192 | text: { 193 | type: ['string'], 194 | }, 195 | product: { 196 | $ref: '/definitionSchema#/definitions/Product', 197 | }, 198 | }, 199 | required: ['title', 'text'], 200 | }, 201 | }, 202 | }; 203 | const ramlStr = ` 204 | #%RAML 1.0 205 | --- 206 | types: 207 | Product: 208 | type: object 209 | properties: 210 | productId: 211 | type: string 212 | required: false 213 | minLength: 1 214 | maxLength: 22 215 | name: number 216 | 217 | Paragraph: 218 | type: object 219 | properties: 220 | title: 221 | text: 222 | product: 223 | type: Product 224 | required: false 225 | `; 226 | const apiJSON = parseRAMLSync(ramlStr) as Api; 227 | t.deepEqual(getDefinitionSchema(apiJSON), definitionSchema); 228 | }); 229 | 230 | test('Given read raml and Type is Array When getDefinitionSchema Then get definitionSchema object', (t) => { 231 | const definitionSchema = { 232 | $id: '/definitionSchema', 233 | definitions: { 234 | Article: { 235 | type: 'object', 236 | properties: { 237 | articleId: { 238 | type: ['string'], 239 | }, 240 | author: { 241 | type: ['string'], 242 | }, 243 | paragraphs: { 244 | items: [ 245 | { 246 | $ref: '/definitionSchema#/definitions/Paragraph', 247 | }, 248 | ], 249 | additionalItems: { 250 | $ref: '/definitionSchema#/definitions/Paragraph', 251 | }, 252 | }, 253 | }, 254 | required: ['author', 'paragraphs'], 255 | }, 256 | Product: { 257 | type: 'object', 258 | properties: { 259 | productId: { 260 | type: ['string'], 261 | minLength: 1, 262 | maxLength: 22, 263 | }, 264 | name: { 265 | type: ['number'], 266 | }, 267 | }, 268 | required: ['name'], 269 | }, 270 | Paragraph: { 271 | type: 'object', 272 | properties: { 273 | title: { 274 | type: ['string'], 275 | }, 276 | text: { 277 | type: ['string'], 278 | }, 279 | product: { 280 | $ref: '/definitionSchema#/definitions/Product', 281 | }, 282 | }, 283 | required: ['title', 'text'], 284 | }, 285 | }, 286 | }; 287 | const ramlStr = ` 288 | #%RAML 1.0 289 | --- 290 | types: 291 | Article: 292 | type: object 293 | properties: 294 | articleId: 295 | type: string 296 | required: false 297 | author: string 298 | paragraphs: Paragraph[] 299 | Product: 300 | type: object 301 | properties: 302 | productId: 303 | type: string 304 | required: false 305 | minLength: 1 306 | maxLength: 22 307 | name: number 308 | 309 | Paragraph: 310 | type: object 311 | properties: 312 | title: 313 | text: 314 | product: 315 | type: Product 316 | required: false 317 | `; 318 | const apiJSON = parseRAMLSync(ramlStr) as Api; 319 | t.deepEqual(getDefinitionSchema(apiJSON), definitionSchema); 320 | }); 321 | 322 | test('Given read raml, type is string array When getDefinitionSchema Then get definitionSchema object', (t) => { 323 | const definitionSchema = { 324 | $id: '/definitionSchema', 325 | definitions: { 326 | Product: { 327 | type: 'object', 328 | properties: { 329 | productId: { 330 | type: ['string'], 331 | }, 332 | coverImage: { 333 | items: [{ type: 'string' }], 334 | additionalItems: { 335 | type: 'string', 336 | }, 337 | }, 338 | }, 339 | required: ['productId', 'coverImage'], 340 | }, 341 | }, 342 | }; 343 | const ramlStr = ` 344 | #%RAML 1.0 345 | --- 346 | types: 347 | Product: 348 | type: object 349 | properties: 350 | productId: 351 | type: string 352 | coverImage: string[] 353 | `; 354 | const apiJSON = parseRAMLSync(ramlStr) as Api; 355 | t.deepEqual(getDefinitionSchema(apiJSON), definitionSchema); 356 | }); 357 | -------------------------------------------------------------------------------- /test/read-raml/read-raml.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { parseRAMLSync } from 'raml-1-parser'; 3 | import { getRestApiArr, getAnnotationByName } from '@/read-raml'; 4 | import { Api } from 'raml-1-parser/dist/parser/artifacts/raml10parserapi'; 5 | 6 | test('Given read raml /products When getRestApiArr() Then get webAPI array', (t) => { 7 | const webAPIArr = [ 8 | { 9 | url: '/products', 10 | method: 'get', 11 | description: '商品列表', 12 | queryParameters: [], 13 | uriParameters: {}, 14 | responses: [ 15 | { 16 | code: 200, 17 | body: { 18 | mimeType: 'application/json', 19 | text: `{ 20 | "a": 1 21 | } 22 | `, 23 | }, 24 | }, 25 | ], 26 | }, 27 | ]; 28 | const ramlStr = ` 29 | #%RAML 1.0 30 | --- 31 | baseUri: / 32 | mediaType: application/json 33 | /products: 34 | get: 35 | description: 商品列表 36 | responses: 37 | 200: 38 | body: 39 | example: | 40 | { 41 | "a": 1 42 | } 43 | `; 44 | const apiJSON = parseRAMLSync(ramlStr) as Api; 45 | 46 | const result = getRestApiArr(apiJSON); 47 | t.deepEqual(result, webAPIArr); 48 | }); 49 | 50 | test('Given url has queryParameter When read raml Then get webAPI array', (t) => { 51 | const webAPIArr = [ 52 | { 53 | url: '/products', 54 | uriParameters: {}, 55 | method: 'get', 56 | description: '商品列表', 57 | queryParameters: [{ 58 | name: 'isStar', 59 | example: 'true', 60 | required: false, 61 | type: 'boolean', 62 | }], 63 | responses: [ 64 | { 65 | code: 200, 66 | body: { 67 | mimeType: 'application/json', 68 | text: `{ 69 | "a": 1 70 | } 71 | `, 72 | }, 73 | }, 74 | ], 75 | }, 76 | ]; 77 | const ramlStr = ` 78 | #%RAML 1.0 79 | --- 80 | baseUri: / 81 | mediaType: application/json 82 | /products: 83 | get: 84 | description: 商品列表 85 | queryParameters: 86 | isStar: 87 | description: 是否精选 88 | type: boolean 89 | required: false 90 | example: true 91 | responses: 92 | 200: 93 | body: 94 | example: | 95 | { 96 | "a": 1 97 | } 98 | `; 99 | const apiJSON = parseRAMLSync(ramlStr) as Api; 100 | 101 | const result = getRestApiArr(apiJSON); 102 | t.deepEqual(result, webAPIArr); 103 | }); 104 | 105 | test('Given read raml When post /products has data Then get webAPI array', (t) => { 106 | const webAPIArr = [ 107 | { 108 | url: '/products', 109 | method: 'post', 110 | description: '商品列表', 111 | queryParameters: [], 112 | uriParameters: {}, 113 | body: { 114 | mimeType: 'application/json', 115 | text: `{ 116 | "isStar": true 117 | }`, 118 | }, 119 | responses: [ 120 | { 121 | code: 200, 122 | body: { 123 | mimeType: 'application/json', 124 | text: `{ 125 | "a": 1 126 | } 127 | `, 128 | }, 129 | }, 130 | ], 131 | }, 132 | ]; 133 | const ramlStr = ` 134 | #%RAML 1.0 135 | --- 136 | baseUri: / 137 | mediaType: application/json 138 | /products: 139 | post: 140 | description: 商品列表 141 | body: 142 | example: 143 | { 144 | isStar: true 145 | } 146 | responses: 147 | 200: 148 | body: 149 | example: | 150 | { 151 | "a": 1 152 | } 153 | `; 154 | const apiJSON = parseRAMLSync(ramlStr) as Api; 155 | 156 | const result = getRestApiArr(apiJSON); 157 | t.deepEqual(result, webAPIArr); 158 | }); 159 | 160 | test(`Given read raml and response type is xml 161 | When post /products has data Then get webAPI array`, (t) => { 162 | const webAPIArr = [ 163 | { 164 | url: '/products', 165 | method: 'post', 166 | description: '商品列表', 167 | queryParameters: [], 168 | uriParameters: {}, 169 | body: { 170 | mimeType: 'application/json', 171 | text: `{ 172 | "isStar": true 173 | }`, 174 | }, 175 | responses: [ 176 | { 177 | code: 200, 178 | body: { 179 | mimeType: 'application/xml', 180 | text: `abc 181 | `, 182 | }, 183 | }, 184 | ], 185 | }, 186 | ]; 187 | const ramlStr = ` 188 | #%RAML 1.0 189 | --- 190 | baseUri: / 191 | mediaType: application/json 192 | /products: 193 | post: 194 | description: 商品列表 195 | body: 196 | example: 197 | { 198 | isStar: true 199 | } 200 | responses: 201 | 200: 202 | body: 203 | application/xml: 204 | example: | 205 | abc 206 | `; 207 | const apiJSON = parseRAMLSync(ramlStr) as Api; 208 | 209 | const result = getRestApiArr(apiJSON); 210 | t.deepEqual(result, webAPIArr); 211 | }); 212 | 213 | test('Given read raml When /products has uriParameters Then get webAPI array', (t) => { 214 | const expectedResult = [ 215 | { 216 | url: '/products/{productId}', 217 | method: 'get', 218 | uriParameters: { 219 | id: 'aaaa', 220 | }, 221 | queryParameters: [], 222 | responses: [ 223 | { 224 | code: 200, 225 | body: { 226 | mimeType: 'application/json', 227 | text: `{ 228 | "a": 1 229 | } 230 | `, 231 | }, 232 | }, 233 | ], 234 | }, 235 | ]; 236 | const ramlStr = ` 237 | #%RAML 1.0 238 | --- 239 | baseUri: / 240 | mediaType: application/json 241 | /products/{productId}: 242 | get: 243 | (uriParameters): 244 | id: 245 | description: article id 246 | example: aaaa 247 | responses: 248 | 200: 249 | body: 250 | example: | 251 | { 252 | "a": 1 253 | } 254 | `; 255 | const apiJSON = parseRAMLSync(ramlStr) as Api; 256 | 257 | const result = getRestApiArr(apiJSON); 258 | t.deepEqual(result, expectedResult); 259 | }); 260 | 261 | test('Given read raml When(runner) annotations Then get annotation object', (t) => { 262 | const expectResult = { 263 | id: { 264 | description: 'article id', 265 | example: 'aaaa', 266 | }, 267 | }; 268 | const ramlStr = ` 269 | #%RAML 1.0 270 | --- 271 | baseUri: / 272 | mediaType: application/json 273 | /products/{productId}: 274 | get: 275 | (runner): 276 | id: 277 | description: article id 278 | example: aaaa 279 | responses: 280 | 200: 281 | body: 282 | example: | 283 | { 284 | "a": 1 285 | } 286 | `; 287 | const apiJSON = parseRAMLSync(ramlStr) as Api; 288 | 289 | const [resource] = apiJSON.allResources(); 290 | const [method] = resource.methods(); 291 | const result = getAnnotationByName('runner', method); 292 | t.deepEqual(result, expectResult); 293 | }); 294 | 295 | test('Given raml has two uri When getRestApiArr() Then RestApi[] length is 2', (t) => { 296 | const expectResult = { 297 | id: { 298 | description: 'article id', 299 | example: 'aaaa', 300 | }, 301 | }; 302 | const ramlStr = ` 303 | #%RAML 1.0 304 | --- 305 | baseUri: / 306 | mediaType: application/json 307 | 308 | /products: 309 | get: 310 | responses: 311 | 200: 312 | body: 313 | example: | 314 | { 315 | "a": 1 316 | } 317 | /products/{productId}: 318 | get: 319 | responses: 320 | 200: 321 | body: 322 | example: | 323 | { 324 | "b": 1 325 | } 326 | `; 327 | const apiJSON = parseRAMLSync(ramlStr) as Api; 328 | 329 | const result = getRestApiArr(apiJSON); 330 | t.is(result.length, 2); 331 | }); 332 | 333 | test('Given raml has two uri and multiple methods When getRestApiArr() Then RestApi[] length is 3', (t) => { 334 | const expectResult = { 335 | id: { 336 | description: 'article id', 337 | example: 'aaaa', 338 | }, 339 | }; 340 | const ramlStr = ` 341 | #%RAML 1.0 342 | --- 343 | baseUri: / 344 | mediaType: application/json 345 | 346 | /products: 347 | get: 348 | responses: 349 | 200: 350 | body: 351 | example: | 352 | { 353 | "a": 1 354 | } 355 | /products/{productId}: 356 | get: 357 | responses: 358 | 200: 359 | body: 360 | example: | 361 | { 362 | "b": 1 363 | } 364 | post: 365 | responses: 366 | 200: 367 | body: 368 | example: | 369 | { 370 | "b": 1 371 | } 372 | `; 373 | const apiJSON = parseRAMLSync(ramlStr) as Api; 374 | 375 | const result = getRestApiArr(apiJSON); 376 | t.is(result.length, 3); 377 | }); 378 | 379 | 380 | test('Given raml has 2 response code And example When getRestApiArr() Then RestApi has 2 responses', (t) => { 381 | const expectResult = { 382 | url: '/products', 383 | uriParameters: {}, 384 | method: 'get', 385 | queryParameters: [], 386 | responses: [ 387 | { 388 | code: 200, 389 | body: { 390 | mimeType: 'application/json', 391 | text: `{ 392 | "a": 1 393 | } 394 | `, 395 | }, 396 | }, 397 | { 398 | code: 400, 399 | body: { 400 | mimeType: 'application/json', 401 | text: `{ 402 | "b": 1 403 | } 404 | `, 405 | }, 406 | }, 407 | ], 408 | }; 409 | const ramlStr = ` 410 | #%RAML 1.0 411 | --- 412 | baseUri: / 413 | mediaType: application/json 414 | 415 | /products: 416 | get: 417 | responses: 418 | 200: 419 | body: 420 | example: | 421 | { 422 | "a": 1 423 | } 424 | 400: 425 | body: 426 | example: | 427 | { 428 | "b": 1 429 | } 430 | `; 431 | const apiJSON = parseRAMLSync(ramlStr) as Api; 432 | 433 | const restApi = getRestApiArr(apiJSON).pop(); 434 | t.deepEqual(restApi, expectResult); 435 | t.is(restApi.responses.length, 2); 436 | }); 437 | 438 | 439 | test('Given raml has base origin When getRestApiArr() Then RestApi just has pathname', (t) => { 440 | const expectResult = { 441 | url: '/products', 442 | uriParameters: {}, 443 | method: 'get', 444 | queryParameters: [], 445 | responses: [ 446 | { 447 | code: 200, 448 | body: { 449 | mimeType: 'application/json', 450 | text: `{ 451 | "a": 1 452 | } 453 | `, 454 | }, 455 | }, 456 | ], 457 | }; 458 | const ramlStr = ` 459 | #%RAML 1.0 460 | --- 461 | baseUri: http://www.a.com 462 | mediaType: application/json 463 | 464 | /products: 465 | get: 466 | responses: 467 | 200: 468 | body: 469 | example: | 470 | { 471 | "a": 1 472 | } 473 | `; 474 | const apiJSON = parseRAMLSync(ramlStr) as Api; 475 | 476 | const restApi = getRestApiArr(apiJSON).pop(); 477 | t.deepEqual(restApi, expectResult); 478 | }); 479 | -------------------------------------------------------------------------------- /test/read-raml/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { getPathname } from '@/read-raml/utils'; 3 | 4 | test('Given has origin url When utils.getPathname() Then get pathname', (t) => { 5 | const expectedPathname = '/abc/123'; 6 | const pathname = getPathname('https://www.a.com/abc/123'); 7 | t.is(pathname, expectedPathname); 8 | }); 9 | 10 | test('Given has no origin url When utils.getPathname() Then get pathname', (t) => { 11 | const expectedPathname = '/abc/1234'; 12 | const pathname = getPathname('/abc/1234'); 13 | t.is(pathname, expectedPathname); 14 | }); 15 | 16 | test('Given has no origin url And has {id} When utils.getPathname() Then get pathname', (t) => { 17 | const expectedPathname = '/abc/1234/{id}'; 18 | const pathname = getPathname('/abc/1234/{id}'); 19 | console.log(pathname); 20 | t.is(pathname, expectedPathname); 21 | }); 22 | 23 | 24 | test('Given has origin url And has {id} When utils.getPathname() Then get pathname', (t) => { 25 | const expectedPathname = '/abc/1234/{id}'; 26 | const pathname = getPathname('http://localhost/abc/1234/{id}'); 27 | console.log(pathname); 28 | t.is(pathname, expectedPathname); 29 | }); 30 | -------------------------------------------------------------------------------- /test/runner/runner-util.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { getResponseByStatusCode, sortByRunner, splitByParameter } from '@/runner/runner-util'; 3 | import Response from '@/models/response'; 4 | import RestAPI from '@/models/rest-api'; 5 | 6 | test('When getResponseByStatusCode, Given Response Array, Then get a Response', (t) => { 7 | const respArr: Response[] = [ 8 | { 9 | code: 200, 10 | redirectURL: 'http://abc.com', 11 | }, 12 | { 13 | code: 302, 14 | redirectURL: 'http://def.com', 15 | }, 16 | ]; 17 | 18 | const expectResult = { 19 | code: 200, 20 | redirectURL: 'http://abc.com', 21 | }; 22 | const result = getResponseByStatusCode(200, respArr); 23 | t.deepEqual(result, expectResult); 24 | }); 25 | 26 | test('When getResponseByStatusCode, Given Response Array, Then get undefined', (t) => { 27 | const respArr: Response[] = [ 28 | { 29 | code: 200, 30 | redirectURL: 'http://abc.com', 31 | }, 32 | { 33 | code: 302, 34 | redirectURL: 'http://def.com', 35 | }, 36 | ]; 37 | 38 | const expectResult = undefined; 39 | const result = getResponseByStatusCode(400, respArr); 40 | t.deepEqual(result, expectResult); 41 | }); 42 | 43 | test('When getResponseByStatusCode, Given empty Response Array, Then get undefined', (t) => { 44 | const respArr: Response[] = undefined; 45 | 46 | const expectResult = undefined; 47 | const result = getResponseByStatusCode(400, respArr); 48 | t.deepEqual(result, expectResult); 49 | }); 50 | 51 | test('When sortByRunner, Given RestApi Array, Then get sort by runner', (t) => { 52 | const restApiArr: RestAPI[] = [ 53 | { 54 | url: 'https://abc.com', 55 | method: 'get', 56 | }, 57 | { 58 | url: 'https://def.com', 59 | method: 'post', 60 | runner: { 61 | after: 'after.js', 62 | }, 63 | }, 64 | ]; 65 | 66 | const expectResult: RestAPI[] = [ 67 | { 68 | url: 'https://def.com', 69 | method: 'post', 70 | runner: { 71 | after: 'after.js', 72 | }, 73 | }, 74 | { 75 | url: 'https://abc.com', 76 | method: 'get', 77 | }, 78 | ]; 79 | const result = sortByRunner(restApiArr); 80 | t.deepEqual(result, expectResult); 81 | }); 82 | 83 | 84 | test('When sortByRunner, Given RestApi undefined Array, Then get undefined', (t) => { 85 | const restApiArr: RestAPI[] = undefined; 86 | 87 | const expectResult: RestAPI[] = undefined; 88 | const result = sortByRunner(restApiArr); 89 | t.deepEqual(result, expectResult); 90 | }); 91 | 92 | test('When splitByParameter, Given RestApi with one queryParameter, Then get RestApi Arr', (t) => { 93 | const restApi: RestAPI = { 94 | url: '/products', 95 | method: 'get', 96 | description: '商品列表', 97 | queryParameters: [{ 98 | name: 'isStar', 99 | example: 'true', 100 | required: false, 101 | type: 'boolean', 102 | }], 103 | }; 104 | 105 | const expectResult: RestAPI[] = [ 106 | { 107 | url: '/products', 108 | method: 'get', 109 | description: '商品列表', 110 | queryParameters: [], 111 | }, 112 | { 113 | url: '/products', 114 | method: 'get', 115 | description: '商品列表', 116 | queryParameters: [{ 117 | name: 'isStar', 118 | example: 'true', 119 | required: false, 120 | type: 'boolean', 121 | }], 122 | }, 123 | ]; 124 | const result = splitByParameter(restApi); 125 | t.deepEqual(result, expectResult); 126 | }); 127 | 128 | test('When splitByParameter, Given RestApi with different queryParameter, Then get RestApi Arr', (t) => { 129 | const restApi: RestAPI = { 130 | url: '/products', 131 | method: 'get', 132 | description: '商品列表', 133 | queryParameters: [{ 134 | name: 'isStar', 135 | example: 'true', 136 | required: false, 137 | type: 'boolean', 138 | }, 139 | { 140 | name: 'isOk', 141 | example: 'true', 142 | required: false, 143 | type: 'boolean', 144 | }], 145 | }; 146 | 147 | const expectResult: RestAPI[] = [ 148 | { 149 | url: '/products', 150 | method: 'get', 151 | description: '商品列表', 152 | queryParameters: [], 153 | }, 154 | { 155 | url: '/products', 156 | method: 'get', 157 | description: '商品列表', 158 | queryParameters: [{ 159 | name: 'isStar', 160 | example: 'true', 161 | required: false, 162 | type: 'boolean', 163 | }], 164 | }, 165 | { 166 | url: '/products', 167 | method: 'get', 168 | description: '商品列表', 169 | queryParameters: [{ 170 | name: 'isOk', 171 | example: 'true', 172 | required: false, 173 | type: 'boolean', 174 | }], 175 | }, 176 | { 177 | url: '/products', 178 | method: 'get', 179 | description: '商品列表', 180 | queryParameters: [{ 181 | name: 'isStar', 182 | example: 'true', 183 | required: false, 184 | type: 'boolean', 185 | }, { 186 | name: 'isOk', 187 | example: 'true', 188 | required: false, 189 | type: 'boolean', 190 | }], 191 | }, 192 | ]; 193 | const result = splitByParameter(restApi); 194 | t.deepEqual(result, expectResult); 195 | }); 196 | 197 | test('When splitByParameter, Given RestApi with no queryParameter, Then get RestApi Arr', (t) => { 198 | const restApi: RestAPI = { 199 | url: '/products', 200 | method: 'get', 201 | description: '商品列表', 202 | queryParameters: [], 203 | }; 204 | 205 | const result = splitByParameter(restApi); 206 | t.deepEqual(result, [restApi]); 207 | }); 208 | -------------------------------------------------------------------------------- /test/schama.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import Ajv from 'ajv'; 3 | import SchemaValidate from '@/validate'; 4 | 5 | test('give single type then validate return true', (t) => { 6 | const responseBody = { 7 | productId: 'P00001', 8 | name: '水杯,生命在于喝水,你不喝就会口渴', 9 | coverImage: 'https://s1.ax1x.com/2018/11/06/ioOhKs.png', 10 | description: 11 | '这是一个神奇的水杯,非常的神奇,倒进去的是热水,出来的还是热水。', 12 | price: 999, 13 | }; 14 | 15 | const definitionSchema = { 16 | $id: '/definitionSchema', 17 | definitions: { 18 | Product: { 19 | type: 'object', 20 | properties: { 21 | productId: { 22 | type: ['string'], 23 | }, 24 | name: { 25 | type: ['string'], 26 | }, 27 | }, 28 | required: ['productId', 'name'], 29 | }, 30 | }, 31 | }; 32 | 33 | const schema = { 34 | $ref: '/definitionSchema#/definitions/Product', 35 | }; 36 | 37 | const ajv = new Ajv(); 38 | const validate = ajv.addSchema(definitionSchema).compile(schema); 39 | const valid = validate(responseBody); 40 | let msg = ''; 41 | if (!valid) { 42 | const [error] = validate.errors; 43 | const { message } = error; 44 | msg = message; 45 | } 46 | t.true(valid, msg); 47 | }); 48 | 49 | test('Give type array then validate return true', (t) => { 50 | const responseBody = { 51 | productId: null, 52 | name: '水杯,生命在于喝水,你不喝就会口渴', 53 | coverImage: 'https://s1.ax1x.com/2018/11/06/ioOhKs.png', 54 | description: 55 | '这是一个神奇的水杯,非常的神奇,倒进去的是热水,出来的还是热水。', 56 | price: 999, 57 | }; 58 | 59 | const definitionSchema = { 60 | $id: '/definitionSchema', 61 | definitions: { 62 | Product: { 63 | type: 'object', 64 | properties: { 65 | productId: { 66 | type: ['string', 'null'], 67 | }, 68 | name: { 69 | type: ['string'], 70 | }, 71 | }, 72 | required: ['productId', 'name'], 73 | }, 74 | }, 75 | }; 76 | 77 | const schema = { 78 | $ref: '/definitionSchema#/definitions/Product', 79 | }; 80 | 81 | const ajv = new Ajv(); 82 | const validate = ajv.addSchema(definitionSchema).compile(schema); 83 | const valid = validate(responseBody); 84 | let msg = ''; 85 | if (!valid) { 86 | const [error] = validate.errors; 87 | const { message } = error; 88 | msg = message; 89 | } 90 | t.true(valid, msg); 91 | }); 92 | 93 | test('give array type then validate return true', (t) => { 94 | const responseBody = [ 95 | { 96 | productId: 'P00001', 97 | name: '水杯,生命在于喝水,你不喝就会口渴', 98 | coverImage: 'https://s1.ax1x.com/2018/11/06/ioOhKs.png', 99 | description: 100 | '这是一个神奇的水杯,非常的神奇,倒进去的是热水,出来的还是热水。', 101 | price: 999, 102 | }, 103 | { 104 | productId: 'P00001', 105 | name: '水杯,生命在于喝水,你不喝就会口渴', 106 | coverImage: 'https://s1.ax1x.com/2018/11/06/ioOhKs.png', 107 | description: 108 | '这是一个神奇的水杯,非常的神奇,倒进去的是热水,出来的还是热水。', 109 | price: 999, 110 | }, 111 | ]; 112 | 113 | const definitionSchema = { 114 | $id: '/definitionSchema', 115 | definitions: { 116 | Product: { 117 | type: 'object', 118 | properties: { 119 | productId: { 120 | type: ['string'], 121 | }, 122 | name: { 123 | type: ['string'], 124 | }, 125 | }, 126 | required: ['productId', 'name'], 127 | }, 128 | }, 129 | }; 130 | 131 | const $ref = { $ref: '/definitionSchema#/definitions/Product' }; 132 | const schema = { 133 | items: [$ref], 134 | additionalItems: $ref, 135 | }; 136 | 137 | const ajv = new Ajv(); 138 | const validate = ajv.addSchema(definitionSchema).compile(schema); 139 | const valid = validate(responseBody); 140 | let msg = ''; 141 | if (!valid) { 142 | const [error] = validate.errors; 143 | const { message } = error; 144 | msg = message; 145 | } 146 | t.true(valid, msg); 147 | }); 148 | 149 | test('give string array type then validate return true', (t) => { 150 | const responseBody = [ 151 | { 152 | productId: 'P00001', 153 | coverImage: ['1.png', '2.png'], 154 | }, 155 | ]; 156 | 157 | const definitionSchema = { 158 | $id: '/definitionSchema', 159 | definitions: { 160 | Product: { 161 | type: 'object', 162 | properties: { 163 | productId: { 164 | type: ['string'], 165 | }, 166 | coverImage: { 167 | items: [{ type: 'string' }], 168 | additionalItems: { 169 | type: 'string', 170 | }, 171 | }, 172 | }, 173 | required: ['productId', 'coverImage'], 174 | }, 175 | }, 176 | }; 177 | 178 | const $ref = { $ref: '/definitionSchema#/definitions/Product' }; 179 | const schema = { 180 | items: [$ref], 181 | additionalItems: $ref, 182 | }; 183 | 184 | const ajv = new Ajv(); 185 | const validate = ajv.addSchema(definitionSchema).compile(schema); 186 | const valid = validate(responseBody); 187 | let msg = ''; 188 | if (!valid) { 189 | const [error] = validate.errors; 190 | const { message } = error; 191 | msg = message; 192 | } 193 | t.true(valid, msg); 194 | }); 195 | 196 | 197 | test('give string array type then SchemaValidate return true', (t) => { 198 | const responseBody = [ 199 | { 200 | productId: 'P00001', 201 | coverImage: ['1.png', '2.png'], 202 | }, 203 | ]; 204 | 205 | const definitionSchema = { 206 | $id: '/definitionSchema', 207 | definitions: { 208 | Product: { 209 | type: 'object', 210 | properties: { 211 | productId: { 212 | type: ['string'], 213 | }, 214 | coverImage: { 215 | items: [{ type: 'string' }], 216 | additionalItems: { 217 | type: 'string', 218 | }, 219 | }, 220 | }, 221 | required: ['productId', 'coverImage'], 222 | }, 223 | }, 224 | }; 225 | 226 | const $ref = { $ref: '/definitionSchema#/definitions/Product' }; 227 | const schema = { 228 | items: [$ref], 229 | additionalItems: $ref, 230 | }; 231 | const schemaValidate = new SchemaValidate(definitionSchema); 232 | const valid = schemaValidate.execute(schema, responseBody); 233 | t.true(valid); 234 | }); 235 | 236 | test('give string array type then SchemaValidate throws error', (t) => { 237 | const responseBody = [ 238 | { 239 | productId: 1, 240 | coverImage: ['1.png', '2.png'], 241 | }, 242 | ]; 243 | 244 | const definitionSchema = { 245 | $id: '/definitionSchema', 246 | definitions: { 247 | Product: { 248 | type: 'object', 249 | properties: { 250 | productId: { 251 | type: ['string'], 252 | }, 253 | coverImage: { 254 | items: [{ type: 'string' }], 255 | additionalItems: { 256 | type: 'string', 257 | }, 258 | }, 259 | }, 260 | required: ['productId', 'coverImage'], 261 | }, 262 | }, 263 | }; 264 | 265 | const $ref = { $ref: '/definitionSchema#/definitions/Product' }; 266 | const schema = { 267 | items: [$ref], 268 | additionalItems: $ref, 269 | }; 270 | 271 | const schemaValidate = new SchemaValidate(definitionSchema); 272 | const error = t.throws(() => { 273 | schemaValidate.execute(schema, responseBody); 274 | }); 275 | t.truthy(error.message); 276 | }); 277 | 278 | 279 | test('give string array type then SchemaValidate throws error Missing custom type', (t) => { 280 | const responseBody = [ 281 | { 282 | productId: 1, 283 | coverImage: ['1.png', '2.png'], 284 | }, 285 | ]; 286 | 287 | const definitionSchema = { 288 | $id: '/definitionSchema', 289 | definitions: { 290 | Product: { 291 | type: 'object', 292 | properties: { 293 | productId: { 294 | type: ['string'], 295 | }, 296 | coverImage: { 297 | items: [{ type: 'string' }], 298 | additionalItems: { 299 | type: 'string', 300 | }, 301 | }, 302 | }, 303 | required: ['productId', 'coverImage'], 304 | }, 305 | }, 306 | }; 307 | 308 | const $ref = { $ref: '/definitionSchema#/definitions/Product1' }; 309 | const schema = { 310 | items: [$ref], 311 | additionalItems: $ref, 312 | }; 313 | 314 | const schemaValidate = new SchemaValidate(definitionSchema); 315 | const error = t.throws(() => { 316 | schemaValidate.execute(schema, responseBody); 317 | }); 318 | t.truthy(error.message.includes('Missing custom type')); 319 | }); 320 | -------------------------------------------------------------------------------- /test/to-raml.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import toRaml from '@/har-convert/to-raml'; 3 | import RestAPI from '@/models/rest-api'; 4 | 5 | test('Given restAPI array, then get raml str', async (t) => { 6 | const restAPIArr: RestAPI[] = [ 7 | { 8 | url: '/api/test/raml/orders/T012019011828586', 9 | description: 'get_api_test_raml_orders_T012019011828586', 10 | method: 'GET', 11 | queryParameters: [{ 12 | name: 'param1', 13 | example: 'value1', 14 | }], 15 | responses: [ 16 | { 17 | code: 200, 18 | body: { 19 | mimeType: 'application/json', 20 | text: '{"name":"你好"}', 21 | }, 22 | }, 23 | ], 24 | }, { 25 | url: '/api/test/raml/orders/T012019011828586/redeem', 26 | description: 'post_api_test_raml_orders_T012019011828586_redeem', 27 | method: 'POST', 28 | queryParameters: [], 29 | body: { 30 | mimeType: 'application/json;charset=UTF-8', 31 | text: '{"a":1,"b":2}', 32 | }, 33 | responses: [{ 34 | code: 200, 35 | body: { 36 | mimeType: 'application/json', 37 | text: '{}', 38 | }, 39 | }], 40 | }, 41 | ]; 42 | 43 | const expectResult = ` 44 | /api/test/raml/orders/T012019011828586: 45 | get: 46 | description: get_api_test_raml_orders_T012019011828586 47 | queryParameters: 48 | param1: 49 | example: value1 50 | responses: 51 | 200: 52 | body: 53 | example: | 54 | {"name":"你好"} 55 | 56 | /api/test/raml/orders/T012019011828586/redeem: 57 | post: 58 | description: post_api_test_raml_orders_T012019011828586_redeem 59 | body: 60 | example: | 61 | {"a":1,"b":2} 62 | responses: 63 | 200: 64 | body: 65 | example: | 66 | {} 67 | `.trim(); 68 | const result = await toRaml(restAPIArr); 69 | t.is(result.trim(), expectResult); 70 | }); 71 | 72 | test('Given restAPI duplicate array, then get raml str', async (t) => { 73 | const restAPIArr: RestAPI[] = [ 74 | { 75 | url: '/api/test/raml/orders/T012019011828586', 76 | description: 'get_api_test_raml_orders_T012019011828586', 77 | method: 'GET', 78 | queryParameters: [{ 79 | name: 'param1', 80 | example: 'value1', 81 | }], 82 | responses: [ 83 | { 84 | code: 200, 85 | body: { 86 | mimeType: 'application/json', 87 | text: '{"name":"你好"}', 88 | }, 89 | }, 90 | ], 91 | }, { 92 | url: '/api/test/raml/orders/T012019011828586', 93 | description: 'get_api_test_raml_orders_T012019011828586', 94 | method: 'GET', 95 | queryParameters: [{ 96 | name: 'param1', 97 | example: 'value2', 98 | }], 99 | responses: [ 100 | { 101 | code: 200, 102 | body: { 103 | mimeType: 'application/json', 104 | text: '{"name":"你好"}', 105 | }, 106 | }, 107 | ], 108 | }, { 109 | url: '/api/test/raml/orders/T012019011828586/redeem', 110 | description: 'post_api_test_raml_orders_T012019011828586_redeem', 111 | method: 'POST', 112 | queryParameters: [], 113 | body: { 114 | mimeType: 'application/json;charset=UTF-8', 115 | text: '{"a":1,"b":2}', 116 | }, 117 | responses: [{ 118 | code: 200, 119 | body: { 120 | mimeType: 'application/json', 121 | text: '{}', 122 | }, 123 | }], 124 | }, 125 | ]; 126 | 127 | const expectResult = ` 128 | /api/test/raml/orders/T012019011828586: 129 | get: 130 | description: get_api_test_raml_orders_T012019011828586 131 | queryParameters: 132 | param1: 133 | example: value1 134 | responses: 135 | 200: 136 | body: 137 | example: | 138 | {"name":"你好"} 139 | 140 | /api/test/raml/orders/T012019011828586/redeem: 141 | post: 142 | description: post_api_test_raml_orders_T012019011828586_redeem 143 | body: 144 | example: | 145 | {"a":1,"b":2} 146 | responses: 147 | 200: 148 | body: 149 | example: | 150 | {} 151 | `.trim(); 152 | const result = await toRaml(restAPIArr); 153 | t.is(result.trim(), expectResult); 154 | }); 155 | -------------------------------------------------------------------------------- /test/to-spec.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import toSpec from '@/har-convert/to-spec'; 3 | import RestAPI from '@/models/rest-api'; 4 | 5 | test('Given restAPI array, then get spec str', async (t) => { 6 | const restAPIArr: RestAPI[] = [ 7 | { 8 | url: '/api/test/raml/orders/T012019011828586', 9 | description: 'get_api_test_raml_orders_T012019011828586', 10 | method: 'GET', 11 | queryParameters: [{ 12 | name: 'param1', 13 | example: 'value1', 14 | }], 15 | responses: [ 16 | { 17 | code: 200, 18 | body: { 19 | mimeType: 'application/json', 20 | text: '{"name":"你好"}', 21 | }, 22 | }, 23 | ], 24 | }, { 25 | url: '/api/test/raml/orders/T012019011828586/redeem', 26 | description: 'post_api_test_raml_orders_T012019011828586_redeem', 27 | method: 'POST', 28 | queryParameters: [], 29 | body: { 30 | mimeType: 'application/json;charset=UTF-8', 31 | text: '{"a":1,"b":2}', 32 | }, 33 | responses: [{ 34 | code: 200, 35 | body: { 36 | mimeType: 'application/json', 37 | text: '{}', 38 | }, 39 | }], 40 | }, 41 | ]; 42 | 43 | const expectResult = ` 44 | const assert = require('assert'); 45 | const { loadApi } = require('@xbl/raml-mocker'); 46 | 47 | it('Case Name', async () => { 48 | const getFn0 = loadApi('get_api_test_raml_orders_T012019011828586'); 49 | const { status: status0, data: data0 } = await getFn0({},{"param1":"value1"},{}); 50 | 51 | assert.equal(status0, 200); 52 | // TODO: assert 53 | 54 | const postFn1 = loadApi('post_api_test_raml_orders_T012019011828586_redeem'); 55 | const { status: status1, data: data1 } = await postFn1({},{},{"a":1,"b":2}); 56 | 57 | assert.equal(status1, 200); 58 | // TODO: assert 59 | 60 | }); 61 | `.trim(); 62 | const result = await toSpec(restAPIArr); 63 | t.is(result.trim(), expectResult); 64 | }); 65 | 66 | 67 | test('Given restAPI array and urlParameters, then get spec str', async (t) => { 68 | const restAPIArr: RestAPI[] = [ 69 | { 70 | url: '/api/test/raml/orders/{id}', 71 | description: 'get_api_test_raml_orders_T012019011828586', 72 | method: 'GET', 73 | uriParameters: { 74 | id: 'T012019011828586', 75 | }, 76 | queryParameters: [{ 77 | name: 'param1', 78 | example: 'value1', 79 | }], 80 | responses: [ 81 | { 82 | code: 200, 83 | body: { 84 | mimeType: 'application/json', 85 | text: '{"name":"你好"}', 86 | }, 87 | }, 88 | ], 89 | }, { 90 | url: '/api/test/raml/orders/1234/redeem', 91 | description: 'post_api_test_raml_orders_T012019011828586_redeem', 92 | method: 'POST', 93 | queryParameters: [], 94 | body: { 95 | mimeType: 'application/json;charset=UTF-8', 96 | text: '{"a":1,"b":2}', 97 | }, 98 | responses: [{ 99 | code: 200, 100 | body: { 101 | mimeType: 'application/json', 102 | text: '{}', 103 | }, 104 | }], 105 | }, 106 | ]; 107 | 108 | const expectResult = ` 109 | const assert = require('assert'); 110 | const { loadApi } = require('@xbl/raml-mocker'); 111 | 112 | it('Case Name', async () => { 113 | const getFn0 = loadApi('get_api_test_raml_orders_T012019011828586'); 114 | const { status: status0, data: data0 } = await getFn0({"id":"T012019011828586"},{"param1":"value1"},{}); 115 | 116 | assert.equal(status0, 200); 117 | // TODO: assert 118 | 119 | const postFn1 = loadApi('post_api_test_raml_orders_T012019011828586_redeem'); 120 | const { status: status1, data: data1 } = await postFn1({},{},{"a":1,"b":2}); 121 | 122 | assert.equal(status1, 200); 123 | // TODO: assert 124 | 125 | }); 126 | `.trim(); 127 | const result = await toSpec(restAPIArr); 128 | t.is(result.trim(), expectResult); 129 | }); 130 | 131 | 132 | test('Given restAPI post body is not JSON str, then get spec str', async (t) => { 133 | const restAPIArr: RestAPI[] = [ 134 | { 135 | url: '/api/test/raml/orders/{id}', 136 | description: 'get_api_test_raml_orders_T012019011828586', 137 | method: 'GET', 138 | uriParameters: { 139 | id: 'T012019011828586', 140 | }, 141 | queryParameters: [{ 142 | name: 'param1', 143 | example: 'value1', 144 | }], 145 | responses: [ 146 | { 147 | code: 200, 148 | body: { 149 | mimeType: 'application/json', 150 | text: '{"name":"你好"}', 151 | }, 152 | }, 153 | ], 154 | }, { 155 | url: '/api/test/raml/orders/1234/redeem', 156 | description: 'post_api_test_raml_orders_T012019011828586_redeem', 157 | method: 'POST', 158 | queryParameters: [], 159 | body: { 160 | mimeType: 'application/x-www-form-urlencoded;charset=UTF-8', 161 | text: 'a=1&b=2', 162 | }, 163 | responses: [{ 164 | code: 200, 165 | body: { 166 | mimeType: 'application/json', 167 | text: '{}', 168 | }, 169 | }], 170 | }, 171 | ]; 172 | 173 | const expectResult = ` 174 | const assert = require('assert'); 175 | const { loadApi } = require('@xbl/raml-mocker'); 176 | 177 | it('Case Name', async () => { 178 | const getFn0 = loadApi('get_api_test_raml_orders_T012019011828586'); 179 | const { status: status0, data: data0 } = await getFn0({"id":"T012019011828586"},{"param1":"value1"},{}); 180 | 181 | assert.equal(status0, 200); 182 | // TODO: assert 183 | 184 | const postFn1 = loadApi('post_api_test_raml_orders_T012019011828586_redeem'); 185 | const { status: status1, data: data1 } = await postFn1({},{},'a=1&b=2'); 186 | 187 | assert.equal(status1, 200); 188 | // TODO: assert 189 | 190 | }); 191 | `.trim(); 192 | const result = await toSpec(restAPIArr); 193 | t.is(result.trim(), expectResult); 194 | }); 195 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "noImplicitAny": false, 7 | "target": "es6", 8 | "moduleResolution": "node", 9 | "outDir": "dist", 10 | "baseUrl": ".", 11 | "sourceMap": true, 12 | "paths": { 13 | "*": ["node_modules/*", "src/types/*"], 14 | "@/*": [ 15 | "../src/*" 16 | ] 17 | }, 18 | "lib": ["es7"] 19 | }, 20 | "include": ["../src/**/*", "./**/*"] 21 | } 22 | -------------------------------------------------------------------------------- /test/util/config-util.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import sinon from 'sinon'; 3 | import fs from '@/util/fs'; 4 | import Config from '@/models/config'; 5 | import { loadConfig } from '@/util/config-util'; 6 | 7 | test.serial('Given When loadConfig Then got Config object', async (t) => { 8 | sinon.replace(fs, 'readFile', sinon.fake.resolves(` 9 | { 10 | "controller": "./controller", 11 | "raml": "./raml", 12 | "main": "api.raml", 13 | "port": 3000, 14 | "runner": { 15 | "test": "http://localhost:3000", 16 | "dev": "http://abc.com:3001" 17 | }, 18 | "plugins": ["1.js"] 19 | } 20 | `)); 21 | 22 | sinon.replace(process, 'cwd', sinon.fake.returns('/Users/')); 23 | 24 | const expectResult: Config = { 25 | controller: '/Users/controller', 26 | raml: '/Users/raml', 27 | main: 'api.raml', 28 | port: 3000, 29 | runner: { 30 | test: 'http://localhost:3000', 31 | dev: 'http://abc.com:3001', 32 | }, 33 | plugins: ['/Users/1.js'], 34 | }; 35 | 36 | const config​​ = await loadConfig(); 37 | t.deepEqual(expectResult, config); 38 | sinon.restore(); 39 | }); 40 | 41 | test.serial('Given When loadConfig Then got no file Error', async (t) => { 42 | const expectResult = '在当前目录'; 43 | sinon.replace(fs, 'readFile', sinon.fake.rejects(new Error(expectResult))); 44 | sinon.replace(process, 'exit', sinon.fake.returns('')); 45 | 46 | const error = await t.throwsAsync(loadConfig()); 47 | t.truthy(error.message.includes(expectResult)); 48 | sinon.restore(); 49 | }); 50 | 51 | test.serial('Given When loadConfig Then got json Error', async (t) => { 52 | sinon.replace(fs, 'readFile', sinon.fake.resolves(` 53 | {ddd} 54 | `)); 55 | sinon.replace(process, 'exit', sinon.fake.returns('')); 56 | 57 | const error = await t.throwsAsync(loadConfig()); 58 | t.truthy(error.message); 59 | sinon.restore(); 60 | }); 61 | -------------------------------------------------------------------------------- /test/util/util.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import RestAPI from '@/models/rest-api'; 3 | import { jsonPath, replaceUriParameters, toExpressUri, 4 | indentString, mergeRestApi, urlCompare, getHost } from '@/util'; 5 | import Config from '@/models/config'; 6 | 7 | test('Given json object When dataPath [0] str Then get json[0] object', (t) => { 8 | const expectResult = { 9 | a: 1, 10 | b: 2, 11 | }; 12 | const json = [expectResult]; 13 | const dataPath = '[0]'; 14 | const result = jsonPath(json, dataPath); 15 | t.deepEqual(result, expectResult); 16 | }); 17 | 18 | test('Given /products/{productId} When replaceUriParameters Then get /products/:productId', (t) => { 19 | const given = '/products/{productId}'; 20 | const expectResult = '/products/:productId'; 21 | let result = given; 22 | replaceUriParameters(given, (match, expression) => { 23 | result = result.replace(match, `:${expression}`); 24 | }); 25 | t.is(result, expectResult); 26 | }); 27 | 28 | test('Given /products/{productId} When toExpressUri Then get /products/:productId', (t) => { 29 | const given = '/products/{productId}'; 30 | const expectResult = '/products/:productId'; 31 | t.is(toExpressUri(given), expectResult); 32 | }); 33 | 34 | test('Given json str When indentString Then got format json', (t) => { 35 | // tslint:disable:max-line-length 36 | const given = '{\n "realName": "金金",\n "mobile": "15811111111",\n "avatar": "",\n "nickName": "",\n "born": null,\n "gender": "MALE",\n "email": "",\n "address": "",\n "occupation": "",\n "interestedClasses": [""],\n "hobbies": [""],\n "ownModels": [""]\n}\n'; 37 | const expectResult = ` 38 | { 39 | "realName": "金金", 40 | "mobile": "15811111111", 41 | "avatar": "", 42 | "nickName": "", 43 | "born": null, 44 | "gender": "MALE", 45 | "email": "", 46 | "address": "", 47 | "occupation": "", 48 | "interestedClasses": [""], 49 | "hobbies": [""], 50 | "ownModels": [""] 51 | }`; 52 | t.is(indentString(given, 4).trim(), expectResult.trim()); 53 | }); 54 | 55 | test('Given newRestApi and oldRestApi When mergeRestApi Then get newRestApi ', (t) => { 56 | const newRestApi: RestAPI[] = [ 57 | { 58 | url: '/api/test/raml/orders/T012019011828586', 59 | description: 'get_api_test_raml_orders_T012019011828586', 60 | method: 'GET', 61 | queryParameters: [{ 62 | name: 'param1', 63 | example: 'value1', 64 | }], 65 | responses: [ 66 | { 67 | code: 200, 68 | body: { 69 | mimeType: 'application/json', 70 | text: '{"name":"你好"}', 71 | }, 72 | }, 73 | ], 74 | }, { 75 | url: '/api/test/raml/orders/T012019011828586/redeem', 76 | description: 'post_api_test_raml_orders_T012019011828586_redeem', 77 | method: 'POST', 78 | queryParameters: [], 79 | body: { 80 | mimeType: 'application/json;charset=UTF-8', 81 | text: '{"a":1,"b":2}', 82 | }, 83 | responses: [{ 84 | code: 200, 85 | body: { 86 | mimeType: 'application/json', 87 | text: '{}', 88 | }, 89 | }], 90 | }, 91 | ]; 92 | 93 | const oldRestApi: RestAPI[] = [ 94 | { 95 | url: '/api/test/raml/orders/{id}', 96 | description: '获得', 97 | method: 'GET', 98 | queryParameters: [{ 99 | name: 'param1', 100 | example: 'value1', 101 | }], 102 | responses: [ 103 | { 104 | code: 200, 105 | body: { 106 | mimeType: 'application/json', 107 | text: '{"name":"你好"}', 108 | }, 109 | }, 110 | ], 111 | }, { 112 | url: '/api/test/raml/orders/{id}/redeem', 113 | description: '保存', 114 | method: 'POST', 115 | queryParameters: [], 116 | body: { 117 | mimeType: 'application/json;charset=UTF-8', 118 | text: '{"a":1,"b":2}', 119 | }, 120 | responses: [{ 121 | code: 200, 122 | body: { 123 | mimeType: 'application/json', 124 | text: '{}', 125 | }, 126 | }], 127 | }, 128 | ]; 129 | 130 | const expectResult: RestAPI[] = [ 131 | { 132 | url: '/api/test/raml/orders/{id}', 133 | description: '获得', 134 | method: 'GET', 135 | uriParameters: { 136 | id: 'T012019011828586', 137 | }, 138 | queryParameters: [{ 139 | name: 'param1', 140 | example: 'value1', 141 | }], 142 | responses: [ 143 | { 144 | code: 200, 145 | body: { 146 | mimeType: 'application/json', 147 | text: '{"name":"你好"}', 148 | }, 149 | }, 150 | ], 151 | }, { 152 | url: '/api/test/raml/orders/{id}/redeem', 153 | description: '保存', 154 | method: 'POST', 155 | uriParameters: { 156 | id: 'T012019011828586', 157 | }, 158 | queryParameters: [], 159 | body: { 160 | mimeType: 'application/json;charset=UTF-8', 161 | text: '{"a":1,"b":2}', 162 | }, 163 | responses: [{ 164 | code: 200, 165 | body: { 166 | mimeType: 'application/json', 167 | text: '{}', 168 | }, 169 | }], 170 | }, 171 | ]; 172 | t.deepEqual(mergeRestApi(newRestApi, oldRestApi), expectResult); 173 | }); 174 | 175 | 176 | test('Given a url and raml url When urlCompare Then get uriMap', (t) => { 177 | const uriMap = urlCompare('/abc/def/hello/jack', '/abc/{id}/hello/{name}'); 178 | const expectResult = { 179 | id: 'def', 180 | name: 'jack', 181 | }; 182 | t.deepEqual(expectResult, uriMap); 183 | }); 184 | 185 | test('Given a url and raml url When urlCompare Then get null', (t) => { 186 | const uriMap = urlCompare('/abc', '/abc/{id}/hello/{name}'); 187 | const expectResult = undefined; 188 | t.is(expectResult, uriMap); 189 | }); 190 | 191 | test('Given Config When getHost Then get http://localhost', (t) => { 192 | const config: Config = { 193 | controller: '', 194 | raml: '', 195 | main: '', 196 | port: 3000, 197 | runner: { 198 | dev: 'http://localhost', 199 | test: 'http://127.0.0.1', 200 | }, 201 | }; 202 | 203 | process.env.NODE_ENV = 'dev'; 204 | const host = getHost(config); 205 | const expectResult = 'http://localhost'; 206 | t.is(host, expectResult); 207 | }); 208 | 209 | test('Given Config has no runner NODE_ENV is dev1 When getHost Then got Error', (t) => { 210 | const config: Config = { 211 | controller: '', 212 | raml: '', 213 | main: '', 214 | port: 3000, 215 | runner: { 216 | dev: 'http://localhost', 217 | test: 'http://127.0.0.1', 218 | }, 219 | }; 220 | 221 | process.env.NODE_ENV = 'dev1'; 222 | const error = t.throws(() => { 223 | getHost(config); 224 | }); 225 | t.truthy(error.message); 226 | }); 227 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "noImplicitAny": false, 7 | "target": "es6", 8 | "moduleResolution": "node", 9 | "outDir": "dist", 10 | "baseUrl": ".", 11 | "sourceMap": true, 12 | "paths": { 13 | "*": ["node_modules/*", "src/types/*"], 14 | "@/*": [ 15 | "src/*" 16 | ] 17 | }, 18 | "lib": ["es7"] 19 | }, 20 | "include": ["./src/**/*"] 21 | } 22 | --------------------------------------------------------------------------------