Test server v2 using a simplified api optimized for a mock api (#251)

This commit is contained in:
Timothee Guerin 2021-01-15 09:41:17 -08:00 коммит произвёл GitHub
Родитель 84ab536f3c
Коммит da7d2d8b79
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
60 изменённых файлов: 1993 добавлений и 297 удалений

31
.azure-pipelines/ci.yml Normal file
Просмотреть файл

@ -0,0 +1,31 @@
name: Autorest Testserver CI
trigger: none
pr:
- master
pool:
vmImage: "ubuntu-latest"
jobs:
- job: main
displayName: "Build and test"
steps:
- task: NodeTool@0
inputs:
versionSpec: "14.x"
displayName: "Install Node.js"
- script: |
npm install -g npm
npm install
displayName: "Install dependencies"
- script: |
npm run build
displayName: "Build"
- script: npm run test:ci
displayName: Test
- script: npm run lint
displayName: Lint

30
.eslintrc.yml Normal file
Просмотреть файл

@ -0,0 +1,30 @@
root: true
plugins:
- "@typescript-eslint"
- prettier
- import
- unicorn
env:
browser: true
es6: true
node: true
extends:
- eslint:recommended
- plugin:@typescript-eslint/recommended
rules:
no-console: warn
"@typescript-eslint/no-empty-interface": off
"@typescript-eslint/no-unused-vars": off
import/no-default-export: warn
import/no-self-import: warn # This cause many circular dependency issues with index files.
import/no-internal-modules: off
import/order:
- warn
- groups: ["builtin", "external", "parent", "sibling", "index"]
alphabetize:
order: "asc"
caseInsensitive: true
unicorn/filename-case: warn
prettier/prettier: warn
parser: "@typescript-eslint/parser"

3
.gitignore поставляемый
Просмотреть файл

@ -291,3 +291,6 @@ package-lock.json
*.tgz
legacy/coverage/report-*.json
__files/
dist/
coverage/

5
.prettierrc.yml Normal file
Просмотреть файл

@ -0,0 +1,5 @@
trailingComma: "all"
printWidth: 120
quoteProps: "consistent"
endOfLine: "auto"
arrowParens: always

Просмотреть файл

@ -1,8 +1,6 @@
#!/usr/bin/env node
const { existsSync } = require('fs');
const { resolve } = require('path');
const { execute, queryUser, httpPost } = require('./process');
const { yellow, red, green, white, gray, cyan} = require('chalk');
const { execute, queryUser } = require('./process');
const { yellow, gray, cyan} = require('chalk');
function prepend( color,pText ,text) {
return text.replace( /^/gm, `${color(pText)} `)
@ -22,7 +20,7 @@ function prepend( color,pText ,text) {
let wrappedExitCode = 0;
async function main() {
console.log(yellow("WARNING: start-autorest-express is deprecated use autorest-testserver instead."));
const cmdArgs = [];
const switches = [];
let switchChecking = true;
@ -53,7 +51,7 @@ async function main() {
})
try {
await execute(process.execPath, [`${__dirname}/../legacy/startup/shutdown.js`]);
await execute(process.execPath, [`${__dirname}/../dist/cli/cli.js`, "stop"]);
verbose('Shutting down existing Express instance.')
} catch (e) {
verbose('Express was not running previously.')
@ -62,33 +60,33 @@ async function main() {
// start the express process
verbose('Starting Express Server.')
const spResult = execute(process.execPath, [`${__dirname}/../legacy/startup/www.js`], {
const spResult = execute(process.execPath, [`${__dirname}/../dist/cli/cli.js`], {
onCreate: (proc) => {
serverProc = proc;
proc.on('close', ()=>{
if( cmdProc && cmdProc.status === null ) {
proc.on("close", () => {
if (cmdProc && cmdProc.status === null) {
cmdProc.kill();
}
if( interactive ) {
if (interactive) {
process.exit(0);
}
})
});
},
onStdOutData: (chunk) => {
const c = chunk.toString().replace(/\s*$/, '');
const c = chunk.toString().replace(/\s*$/, "");
if (showMessages) {
console.log(prepend( gray, '[Express]', c));
console.log(prepend(gray, "[Express]", c));
}
if (/Server started/.exec(c)) {
spResolve();
}
},
onStdErrData: (chunk) => {
const c = chunk.toString().replace(/\s*$/, '');
const c = chunk.toString().replace(/\s*$/, "");
if (showMessages) {
console.log(prepend( yellow.dim, '[Express]', c ));
console.log(prepend(yellow.dim, "[Express]", c));
}
}
},
});
// when it's ready, run the command line
@ -125,7 +123,7 @@ async function main() {
// shutdown server process
verbose('Shutting down Express.');
try {
await execute(process.execPath, [`${__dirname}/../legacy/startup/shutdown.js`]);
await execute(process.execPath, [`${__dirname}/../dist/cli/cli.js`, "stop"]);
verbose('Shutting down existing Express instance.')
} catch (e) {
// who cares.

Просмотреть файл

@ -29,6 +29,8 @@ function getEntrypoint() {
}
async function main() {
console.log(yellow("WARNING: start-autorest-wiremock is deprecated use autorest-testserver instead."));
const wmFolderPath = getEntrypoint();
const cmdArgs = [];

13
.vscode/launch.json поставляемый
Просмотреть файл

@ -10,11 +10,12 @@
{
"type": "node",
"request": "launch",
"name": "Start Server",
"program": "${workspaceRoot}/legacy/startup/www.js",
"env": {
"PORT": "3000"
}
"name": "Run server",
"program": "${workspaceFolder}/src/cli/cli.ts",
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"console": "integratedTerminal",
"sourceMaps": true,
"cwd": "${workspaceFolder}"
}
]
}
}

11
.vscode/settings.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,11 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"search.exclude": {
"**/node_modules": true,
"dist": true
}
}

Просмотреть файл

@ -1,20 +1,23 @@
# Test Server
# Test server V2
This project contains a set of OpenAPI definitions and a server implementing the corresponding API.
Use this to test compliance of AutoRest generators.
## Requirements
# Contributing
- `Node.js`
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.microsoft.com.
## Getting started
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.
```bash
# Install dependencies
npm install
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
# Start for dev: Will start the server and automatically restart in case there is changes in the files.
npm run start:dev
# Start for running: Will build and start the server
npm run start
```
## Writing mock apis
See [docs](./docs/writing-mock-apis)

7
definitions/index.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,7 @@
/**
* Yargs is missing helpers definitions.
*/
declare module "yargs/helpers" {
export const hideBin: (argv: string[]) => string[];
}

117
docs/writing-mock-apis.md Normal file
Просмотреть файл

@ -0,0 +1,117 @@
# Writing mock apis
1. First step is to create a new file typescript file in the [src/test-routes](../src/test-routes) folder:
1. All the needed imports are from the `api` folder
1. Define the category for your apis using `app.category("vanilla" | "azure" | "optional", () => {})`
1. Start writing mock apis inside the `category` callback
## Example
```ts
import { app, json } from "../../api";
app.category("vanilla", () => {
app.get("/test", "GetMyTest", (req) => {
return {
status: 200,
body: json({
foo: "succeeded",
bar: "wut",
}),
};
});
app.post("/test", "PostMyTest", (req) => {
req.bodyEquals({ foo: "123", bar: "456" });
return {
status: 200,
body: json({
succeeded: true,
}),
};
});
});
```
## How to build response
Return the reponse object. [See type](../src/api/mock-response.ts)
```ts
// Minimum requirement is the status code.
return {
status: 200,
};
```
### Return a body
```ts
// Return json
return {
status: 200,
body: json({ foo: 123 }),
};
// Return raw content
return {
status: 200,
body: {
contentType: "application/text",
rawContent: "foobar",
},
};
```
### Return headers
```ts
// Return json
return {
status: 200,
headers: {
MyHeader: "value-1"
MyHeaderOther: req.headers.MyRequestHeader
}
};
```
## How to validate the request:
### Validate the body
- With `req.bodyEquals`
This will do a deep equals of the body to make sure it match.
```ts
app.post("/example", "Example", (req) => {
req.bodyEquals({ foo: "123", bar: "456" });
});
```
- With `req.rawBodyEquals`
This will compare the raw body sent.
```ts
app.post("/example", "Example", (req) => {
req.rawBodyEquals('"foo"');
});
```
### Custom validation
You can do any kind of validation accessing the `req: MockRequest` object and deciding to return a different response in some cases.
You can also always `throw` a `ValidationError`
Example:
```ts
app.post("/example", "Example", (req) => {
if (req.headers.MyCustomHeader.startsWith("x-foo")) {
throw new ValidationError("MyCustomHeader shouldn't start with x-foo", null, req.headers.MyCustomHeader);
}
});
```

25
jest.config.js Normal file
Просмотреть файл

@ -0,0 +1,25 @@
// @ts-check
const config = {
transform: {
"^.+\\.ts$": "ts-jest",
},
moduleFileExtensions: ["ts", "js", "json", "node"],
moduleNameMapper: {},
collectCoverage: true,
collectCoverageFrom: ["**/*.ts", "!**/node_modules/**", "!dist/**"],
coverageReporters: ["json", "lcov", "cobertura", "text", "html", "clover"],
coveragePathIgnorePatterns: ["/node_modules/", ".*/test/.*"],
modulePathIgnorePatterns: ["<rootDir>/sdk"],
globals: {
"ts-jest": {
tsconfig: "tsconfig.json",
},
},
testMatch: ["<rootDir>/src/**/*.test.ts", "<rootDir>/test/**/*.test.ts"],
setupFilesAfterEnv: ["<rootDir>/test/setup-jest.ts"],
verbose: true,
testEnvironment: "node",
};
module.exports = config;

Просмотреть файл

@ -3,6 +3,8 @@ var router = express.Router();
var util = require('util');
var utils = require('../util/utils');
const getRequestBaseUrl = (request) => `${request.protocol}://${request.get("host")}`;
var getLRORetryPutScenarioName = function (initialCode, initialState, finalState, finalCode) {
if (initialCode === 201 && initialState === 'Creating' && finalState === 'Succeeded' && finalCode === 200) {
return 'LRORetryPutSucceededWithBody';
@ -64,7 +66,7 @@ var lros = function (coverage) {
coverage['LROPut202Retry200'] = 0;
router.put('/put/202/retry/200', function (req, res, next) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/put/202/retry/operationResults/200';
var pollingUri = getRequestBaseUrl(req) + '/lro/put/202/retry/operationResults/200';
var headers = {
'Location': pollingUri
};
@ -152,7 +154,7 @@ var lros = function (coverage) {
var scenario = getLROAsyncScenarioName("putasync", retry, finalState);
if (scenario) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/putasync/' + retry + '/' + finalState.toLowerCase() + '/operationResults/200/';
var pollingUri = getRequestBaseUrl(req) + '/lro/putasync/' + retry + '/' + finalState.toLowerCase() + '/operationResults/200/';
var headers = {
'Azure-AsyncOperation': pollingUri,
'Location': pollingUri
@ -189,7 +191,7 @@ var lros = function (coverage) {
var scenario = getLROAsyncScenarioName(operation, retry, finalState);
console.log('In scenario: ' + scenario + '\n');
if (scenario) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/' + operation + '/' + retry + '/' + finalState.toLowerCase() + '/operationResults/' + code;
var pollingUri = getRequestBaseUrl(req) + '/lro/' + operation + '/' + retry + '/' + finalState.toLowerCase() + '/operationResults/' + code;
var headers = {
'Azure-AsyncOperation': pollingUri,
'Location': pollingUri
@ -212,7 +214,7 @@ var lros = function (coverage) {
coverage['LROPutNoHeaderInRetry'] = 0;
router.put('/put/noheader/202/200', function (req, res, next) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/put/noheader/operationresults';
var pollingUri = getRequestBaseUrl(req) + '/lro/put/noheader/operationresults';
var headers = {
'Location': pollingUri
};
@ -234,7 +236,7 @@ var lros = function (coverage) {
coverage['LROPutAsyncNoHeaderInRetry'] = 0;
router.put('/putasync/noheader/201/200', function (req, res, next) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/putasync/noheader/operationresults/123';
var pollingUri = getRequestBaseUrl(req) + '/lro/putasync/noheader/operationresults/123';
var headers = {
'Azure-AsyncOperation': pollingUri,
'Location': 'somethingBadWhichShouldNotBeUsed'
@ -261,7 +263,7 @@ var lros = function (coverage) {
coverage['LRODeleteNoHeaderInRetry'] = 0;
router.delete('/delete/noheader', function (req, res, next) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/delete/noheader/operationresults/123';
var pollingUri = getRequestBaseUrl(req) + '/lro/delete/noheader/operationresults/123';
var headers = {
'Location': pollingUri
};
@ -283,7 +285,7 @@ var lros = function (coverage) {
coverage['LRODeleteAsyncNoHeaderInRetry'] = 0;
router.delete('/deleteasync/noheader/202/204', function (req, res, next) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/deleteasync/noheader/operationresults/123';
var pollingUri = getRequestBaseUrl(req) + '/lro/deleteasync/noheader/operationresults/123';
var headers = {
'Azure-AsyncOperation': pollingUri,
'Location': 'somethingBadWhichShouldNotBeUsed'
@ -306,7 +308,7 @@ var lros = function (coverage) {
coverage['LROPutSubResourceInRetry'] = 0;
router.put('/putsubresource/202/200', function (req, res, next) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/putsubresource/operationresults';
var pollingUri = getRequestBaseUrl(req) + '/lro/putsubresource/operationresults';
var headers = {
'Location': pollingUri
};
@ -328,7 +330,7 @@ var lros = function (coverage) {
coverage['LROPutSubResourceAsyncInRetry'] = 0;
router.put('/putsubresourceasync/202/200', function (req, res, next) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/putsubresourceasync/operationresults/123';
var pollingUri = getRequestBaseUrl(req) + '/lro/putsubresourceasync/operationresults/123';
var headers = {
'Azure-AsyncOperation': pollingUri,
'Location': 'somethingBadWhichShouldNotBeUsed'
@ -355,7 +357,7 @@ var lros = function (coverage) {
coverage['LROPutNonResourceInRetry'] = 0;
router.put('/putnonresource/202/200', function (req, res, next) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/putnonresource/operationresults';
var pollingUri = getRequestBaseUrl(req) + '/lro/putnonresource/operationresults';
var headers = {
'Location': pollingUri
};
@ -377,7 +379,7 @@ var lros = function (coverage) {
coverage['LROPutNonResourceAsyncInRetry'] = 0;
router.put('/putnonresourceasync/202/200', function (req, res, next) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/putnonresourceasync/operationresults/123';
var pollingUri = getRequestBaseUrl(req) + '/lro/putnonresourceasync/operationresults/123';
var headers = {
'Azure-AsyncOperation': pollingUri,
'Location': 'somethingBadWhichShouldNotBeUsed'
@ -425,7 +427,7 @@ var lros = function (coverage) {
var finalCode = JSON.parse(req.params.finalCode);
var scenario = getLRODeleteProvisioningScenarioName(initialCode, initialState, finalCode, finalState);
if (scenario) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/delete/provisioning/' + initialCode + '/' + initialState.toLowerCase() + '/' + finalCode + '/' + finalState.toLowerCase();
var pollingUri = getRequestBaseUrl(req) + '/lro/delete/provisioning/' + initialCode + '/' + initialState.toLowerCase() + '/' + finalCode + '/' + finalState.toLowerCase();
var headers = {
'Location': pollingUri,
'Retry-After': 0
@ -475,7 +477,7 @@ var lros = function (coverage) {
var finalCode = JSON.parse(req.params.finalCode);
var scenario = getLRODeleteScenarioName(initialCode, retry, finalCode);
if (scenario) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/delete/' + initialCode + '/' + retry + '/' + finalCode;
var pollingUri = getRequestBaseUrl(req) + '/lro/delete/' + initialCode + '/' + retry + '/' + finalCode;
var headers = {
'Location': pollingUri
};
@ -511,7 +513,7 @@ var lros = function (coverage) {
var scenario = getLROAsyncScenarioName("deleteasync", retry, finalState);
if (scenario) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/deleteasync/' + retry + '/' + finalState.toLowerCase() + '/operationResults/200/';
var pollingUri = getRequestBaseUrl(req) + '/lro/deleteasync/' + retry + '/' + finalState.toLowerCase() + '/operationResults/200/';
var headers = {
'Azure-AsyncOperation': pollingUri,
'Location': pollingUri
@ -536,7 +538,7 @@ var lros = function (coverage) {
var scenario = getLROAsyncScenarioName(operation, retry, finalState);
console.log('In scenario: ' + scenario + '\n');
if (scenario) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/' + operation + '/' + retry + '/' + finalState.toLowerCase() + '/operationResults/' + code;
var pollingUri = getRequestBaseUrl(req) + '/lro/' + operation + '/' + retry + '/' + finalState.toLowerCase() + '/operationResults/' + code;
var headers = {
'Azure-AsyncOperation': pollingUri,
'Location': pollingUri
@ -574,7 +576,7 @@ var lros = function (coverage) {
var finalCode = JSON.parse(req.params.finalCode);
var scenario = getLROPostScenarioName(initialCode, retry, finalCode);
if (scenario) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/post/' + initialCode + '/' + retry + '/' + finalCode;
var pollingUri = getRequestBaseUrl(req) + '/lro/post/' + initialCode + '/' + retry + '/' + finalCode;
var headers = {
'Location': pollingUri
};
@ -594,7 +596,7 @@ var lros = function (coverage) {
var finalCode = JSON.parse(req.params.finalCode);
var scenario = getLROPostScenarioName(initialCode, retry, finalCode);
if (scenario) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/post/newuri/' + initialCode + '/' + retry + '/' + finalCode;
var pollingUri = getRequestBaseUrl(req) + '/lro/post/newuri/' + initialCode + '/' + retry + '/' + finalCode;
var headers = {
'Location': pollingUri
};
@ -627,7 +629,7 @@ var lros = function (coverage) {
coverage['LROPost200'] = 0;
router.post('/post/payload/200', function (req, res, next) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/post/payload/200';
var pollingUri = getRequestBaseUrl(req) + '/lro/post/payload/200';
var headers = {
'Location': pollingUri,
'Retry-After': 0
@ -642,27 +644,6 @@ var lros = function (coverage) {
res.status(200).type('json').end('{"id":"1", "name":"product"}');
});
// Initial call is 202 with no body and Location and Azure-AsyncOperation
// Configured to follow Location
// Then, should poll Azure-AsyncOperation and see it's done
// Then, should do final GET on the initial Location
// ARM guidance ok, and implemented in VM capture after 2018-04-01
coverage['LROPostDoubleHeadersFinalLocationGet'] = 0;
router.post('/LROPostDoubleHeadersFinalLocationGet', function (req, res, next) {
var headers = {
'Azure-AsyncOperation': 'http://localhost:' + utils.getPort() + '/lro/LROPostDoubleHeadersFinalLocationGet/asyncOperationUrl',
'Location': 'http://localhost:' + utils.getPort() + '/lro/LROPostDoubleHeadersFinalLocationGet/location'
};
res.set(headers).status(202).end('');
});
router.get('/LROPostDoubleHeadersFinalLocationGet/asyncOperationUrl', function (req, res, next) {
res.status(200).type('json').end('{ "status": "succeeded"} ');
});
router.get('/LROPostDoubleHeadersFinalLocationGet/location', function (req, res, next) {
res.status(200).type('json').end('{ "id": "100", "name": "foo" }');
coverage['LROPostDoubleHeadersFinalLocationGet']++;
});
// Initial call is 202 with no body and Location and Azure-AsyncOperation
// Configured to follow Azure-AsyncOperation
// Then, should poll Azure-AsyncOperation and see it's done
@ -670,8 +651,8 @@ var lros = function (coverage) {
coverage['LROPostDoubleHeadersFinalAzureHeaderGet'] = 0;
router.post('/LROPostDoubleHeadersFinalAzureHeaderGet', function (req, res, next) {
var headers = {
'Azure-AsyncOperation': 'http://localhost:' + utils.getPort() + '/lro/LROPostDoubleHeadersFinalAzureHeaderGet/asyncOperationUrl',
'Location': 'http://localhost:' + utils.getPort() + '/lro/LROPostDoubleHeadersFinalAzureHeaderGet/location'
'Azure-AsyncOperation': getRequestBaseUrl(req) + '/lro/LROPostDoubleHeadersFinalAzureHeaderGet/asyncOperationUrl',
'Location': getRequestBaseUrl(req) + '/lro/LROPostDoubleHeadersFinalAzureHeaderGet/location'
};
res.set(headers).status(202).end('');
});
@ -691,8 +672,8 @@ var lros = function (coverage) {
coverage['LROPostDoubleHeadersFinalAzureHeaderGetDefault'] = 0;
router.post('/LROPostDoubleHeadersFinalAzureHeaderGetDefault', function (req, res, next) {
var headers = {
'Azure-AsyncOperation': 'http://localhost:' + utils.getPort() + '/lro/LROPostDoubleHeadersFinalAzureHeaderGetDefault/asyncOperationUrl',
'Location': 'http://localhost:' + utils.getPort() + '/lro/LROPostDoubleHeadersFinalAzureHeaderGetDefault/location'
'Azure-AsyncOperation': getRequestBaseUrl(req) + '/lro/LROPostDoubleHeadersFinalAzureHeaderGetDefault/asyncOperationUrl',
'Location': getRequestBaseUrl(req) + '/lro/LROPostDoubleHeadersFinalAzureHeaderGetDefault/location'
};
res.set(headers).status(202).end('');
});
@ -707,8 +688,8 @@ var lros = function (coverage) {
coverage['LROPostAndGetList'] = 0;
router.post('/list', function (req, res, next) {
var headers = {
'Azure-AsyncOperation': 'http://localhost:' + utils.getPort() + '/lro/list/pollingGet',
'Location': 'http://localhost:' + utils.getPort() + '/lro/list/finalGet'
'Azure-AsyncOperation': getRequestBaseUrl(req) + '/lro/list/pollingGet',
'Location': getRequestBaseUrl(req) + '/lro/list/finalGet'
};
res.set(headers).status(202).end();
});
@ -732,10 +713,10 @@ var lros = function (coverage) {
var scenario = getLROAsyncScenarioName("postasync", retry, finalState);
if (scenario) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/postasync/' + retry + '/' + finalState.toLowerCase() + '/operationResults/200/';
var pollingUri = getRequestBaseUrl(req) + '/lro/postasync/' + retry + '/' + finalState.toLowerCase() + '/operationResults/200/';
var headers = {
'Azure-AsyncOperation': pollingUri,
'Location': 'http://localhost:' + utils.getPort() + '/lro/postasync/' + retry + '/Succeeded/operationResults/foo/200/'
'Location': getRequestBaseUrl(req) + '/lro/postasync/' + retry + '/Succeeded/operationResults/foo/200/'
};
if (retry === 'retry') {
headers['Retry-After'] = 0;
@ -757,10 +738,10 @@ var lros = function (coverage) {
var scenario = getLROAsyncScenarioName(operation, retry, finalState);
console.log('In scenario: ' + scenario + '\n');
if (scenario) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/' + operation + '/' + retry + '/' + finalState.toLowerCase() + '/operationResults/' + code;
var pollingUri = getRequestBaseUrl(req) + '/lro/' + operation + '/' + retry + '/' + finalState.toLowerCase() + '/operationResults/' + code;
var headers = {
'Azure-AsyncOperation': pollingUri,
'Location': 'http://localhost:' + utils.getPort() + '/lro/postasync/' + retry + '/Succeeded/operationResults/foo/200/'
'Location': getRequestBaseUrl(req) + '/lro/postasync/' + retry + '/Succeeded/operationResults/foo/200/'
};
if (retry === 'retry') {
headers['Retry-After'] = 0;
@ -848,7 +829,7 @@ var lros = function (coverage) {
coverage['LRORetryErrorPutAsyncSucceededPolling'] = 0;
router.put('/retryerror/putasync/retry/succeeded', function (req, res, next) {
var scenario = 'LRORetryErrorPutAsyncSucceeded';
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/retryerror/putasync/retry/succeeded/operationResults/200';
var pollingUri = getRequestBaseUrl(req) + '/lro/retryerror/putasync/retry/succeeded/operationResults/200';
var headers = {
'Azure-AsyncOperation': pollingUri,
'Location': pollingUri,
@ -894,7 +875,7 @@ var lros = function (coverage) {
addScenarioCookie(res, scenario);
res.status(500).end();
} else {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/retryerror/delete/provisioning/202/accepted/200/succeeded';
var pollingUri = getRequestBaseUrl(req) + '/lro/retryerror/delete/provisioning/202/accepted/200/succeeded';
var headers = {
'Location': pollingUri
};
@ -923,7 +904,7 @@ var lros = function (coverage) {
addScenarioCookie(res, scenario);
res.status(500).end();
} else {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/retryerror/delete/202/retry/200';
var pollingUri = getRequestBaseUrl(req) + '/lro/retryerror/delete/202/retry/200';
var headers = {
'Location': pollingUri
};
@ -952,9 +933,9 @@ var lros = function (coverage) {
addScenarioCookie(res, scenario);
res.status(500).end();
} else {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/retryerror/deleteasync/retry/succeeded/operationResults/200';
var pollingUri = getRequestBaseUrl(req) + '/lro/retryerror/deleteasync/retry/succeeded/operationResults/200';
var headers = {
'Location': 'http://localhost:' + utils.getPort() + '/lro/retryerror/deleteasync/retry/succeeded/operationResults/foo/200',
'Location': getRequestBaseUrl(req) + '/lro/retryerror/deleteasync/retry/succeeded/operationResults/foo/200',
'Azure-AsyncOperation': pollingUri,
'Retry-After': 0
};
@ -995,7 +976,7 @@ var lros = function (coverage) {
addScenarioCookie(res, scenario);
res.status(500).end();
} else {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/retryerror/post/202/retry/200/operationResults';
var pollingUri = getRequestBaseUrl(req) + '/lro/retryerror/post/202/retry/200/operationResults';
var headers = {
'Location': pollingUri
};
@ -1024,10 +1005,10 @@ var lros = function (coverage) {
addScenarioCookie(res, scenario);
res.status(500).end();
} else {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/retryerror/postasync/retry/succeeded/operationResults/200';
var pollingUri = getRequestBaseUrl(req) + '/lro/retryerror/postasync/retry/succeeded/operationResults/200';
var headers = {
/*Passing absolute uri */
'Location': 'http://localhost:' + utils.getPort() + '/lro/retryerror/postasync/retry/succeeded/operationResults/foo/200',
'Location': getRequestBaseUrl(req) + '/lro/retryerror/postasync/retry/succeeded/operationResults/foo/200',
'Azure-AsyncOperation': pollingUri,
'Retry-After': 0
};
@ -1090,7 +1071,7 @@ var lros = function (coverage) {
coverage['LRONonRetryPutAsyncRetry400'] = 0;
router.put('/nonretryerror/putasync/retry/400', function (req, res, next) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/nonretryerror/putasync/retry/failed/operationResults/400';
var pollingUri = getRequestBaseUrl(req) + '/lro/nonretryerror/putasync/retry/failed/operationResults/400';
var headers = {
'Azure-AsyncOperation': pollingUri,
'Location': pollingUri,
@ -1112,7 +1093,7 @@ var lros = function (coverage) {
coverage['LRONonRetryDelete202Retry400'] = 0;
router.delete('/nonretryerror/delete/202/retry/400', function (req, res, next) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/nonretryerror/delete/202/retry/400';
var pollingUri = getRequestBaseUrl(req) + '/lro/nonretryerror/delete/202/retry/400';
var headers = {
'Location': pollingUri,
'Retry-After': 0
@ -1127,7 +1108,7 @@ var lros = function (coverage) {
coverage['LRONonRetryDeleteAsyncRetry400'] = 0;
router.delete('/nonretryerror/deleteasync/retry/400', function (req, res, next) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/nonretryerror/deleteasync/retry/failed/operationResults/400';
var pollingUri = getRequestBaseUrl(req) + '/lro/nonretryerror/deleteasync/retry/failed/operationResults/400';
var headers = {
'Azure-AsyncOperation': pollingUri,
'Location': pollingUri,
@ -1149,7 +1130,7 @@ var lros = function (coverage) {
coverage['LRONonRetryPost202Retry400'] = 0;
router.post('/nonretryerror/post/202/retry/400', function (req, res, next) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/nonretryerror/post/202/retry/400';
var pollingUri = getRequestBaseUrl(req) + '/lro/nonretryerror/post/202/retry/400';
var headers = {
'Location': pollingUri,
'Retry-After': 0
@ -1164,7 +1145,7 @@ var lros = function (coverage) {
coverage['LRONonRetryPostAsyncRetry400'] = 0;
router.post('/nonretryerror/postasync/retry/400', function (req, res, next) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/nonretryerror/postasync/retry/failed/operationResults/400';
var pollingUri = getRequestBaseUrl(req) + '/lro/nonretryerror/postasync/retry/failed/operationResults/400';
var headers = {
'Azure-AsyncOperation': pollingUri,
'Location': pollingUri,
@ -1191,7 +1172,7 @@ var lros = function (coverage) {
coverage['LROErrorPutAsyncNoPollingStatus'] = 0;
router.put('/error/putasync/retry/nostatus', function (req, res, next) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/error/putasync/retry/failed/operationResults/nostatus';
var pollingUri = getRequestBaseUrl(req) + '/lro/error/putasync/retry/failed/operationResults/nostatus';
var headers = {
'Azure-AsyncOperation': pollingUri,
'Location': pollingUri,
@ -1211,7 +1192,7 @@ var lros = function (coverage) {
coverage['LROErrorPutAsyncNoPollingStatusPayload'] = 0;
router.put('/error/putasync/retry/nostatuspayload', function (req, res, next) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/error/putasync/retry/failed/operationResults/nostatuspayload';
var pollingUri = getRequestBaseUrl(req) + '/lro/error/putasync/retry/failed/operationResults/nostatuspayload';
var headers = {
'Azure-AsyncOperation': pollingUri,
'Location': pollingUri,
@ -1253,7 +1234,7 @@ var lros = function (coverage) {
coverage['LROErrorPutAsyncInvalidJsonPolling'] = 0;
router.put('/error/putasync/retry/invalidjsonpolling', function (req, res, next) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/error/putasync/retry/failed/operationResults/invalidjsonpolling';
var pollingUri = getRequestBaseUrl(req) + '/lro/error/putasync/retry/failed/operationResults/invalidjsonpolling';
var headers = {
'Azure-AsyncOperation': pollingUri,
'Location': pollingUri,
@ -1286,7 +1267,7 @@ var lros = function (coverage) {
coverage['LROErrorDeleteAsyncNoPollingStatus'] = 0;
router.delete('/error/deleteasync/retry/nostatus', function (req, res, next) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/error/deleteasync/retry/failed/operationResults/nostatus';
var pollingUri = getRequestBaseUrl(req) + '/lro/error/deleteasync/retry/failed/operationResults/nostatus';
var headers = {
'Azure-AsyncOperation': pollingUri,
'Location': pollingUri,
@ -1314,7 +1295,7 @@ var lros = function (coverage) {
coverage['LROErrorDeleteAsyncInvalidJsonPolling'] = 0;
router.delete('/error/deleteasync/retry/invalidjsonpolling', function (req, res, next) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/error/deleteasync/retry/failed/operationResults/invalidjsonpolling';
var pollingUri = getRequestBaseUrl(req) + '/lro/error/deleteasync/retry/failed/operationResults/invalidjsonpolling';
var headers = {
'Azure-AsyncOperation': pollingUri,
'Location': pollingUri,
@ -1347,7 +1328,7 @@ var lros = function (coverage) {
coverage['LROErrorPostAsyncNoPollingPayload'] = 0;
router.post('/error/postasync/retry/nopayload', function (req, res, next) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/error/postasync/retry/failed/operationResults/nopayload';
var pollingUri = getRequestBaseUrl(req) + '/lro/error/postasync/retry/failed/operationResults/nopayload';
var headers = {
'Azure-AsyncOperation': pollingUri,
'Location': pollingUri,
@ -1375,7 +1356,7 @@ var lros = function (coverage) {
coverage['LROErrorPostAsyncInvalidJsonPolling'] = 0;
router.post('/error/postasync/retry/invalidjsonpolling', function (req, res, next) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/error/postasync/retry/failed/operationResults/invalidjsonpolling';
var pollingUri = getRequestBaseUrl(req) + '/lro/error/postasync/retry/failed/operationResults/invalidjsonpolling';
var headers = {
'Azure-AsyncOperation': pollingUri,
'Location': pollingUri,
@ -1392,7 +1373,7 @@ var lros = function (coverage) {
router.put('/customheader/putasync/retry/succeeded', function (req, res, next) {
var header = req.get("x-ms-client-request-id");
if (header && header.toLowerCase() === "9C4D50EE-2D56-4CD3-8152-34347DC9F2B0".toLowerCase()) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/customheader/putasync/retry/succeeded/operationResults/200';
var pollingUri = getRequestBaseUrl(req) + '/lro/customheader/putasync/retry/succeeded/operationResults/200';
var headers = {
'Azure-AsyncOperation': pollingUri,
'Location': pollingUri
@ -1418,7 +1399,7 @@ var lros = function (coverage) {
var header = req.get("x-ms-client-request-id");
var scenario = 'CustomHeaderPutAsyncSucceded';
if (header && header.toLowerCase() === "9C4D50EE-2D56-4CD3-8152-34347DC9F2B0".toLowerCase()) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/customheader/putasync/retry/succeeded/operationResults/200';
var pollingUri = getRequestBaseUrl(req) + '/lro/customheader/putasync/retry/succeeded/operationResults/200';
var headers = {
'Azure-AsyncOperation': pollingUri,
'Location': pollingUri
@ -1440,7 +1421,7 @@ var lros = function (coverage) {
router.post('/customheader/postasync/retry/succeeded', function (req, res, next) {
var header = req.get("x-ms-client-request-id");
if (header && header.toLowerCase() === "9C4D50EE-2D56-4CD3-8152-34347DC9F2B0".toLowerCase()) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/customheader/postasync/retry/succeeded/operationResults/200';
var pollingUri = getRequestBaseUrl(req) + '/lro/customheader/postasync/retry/succeeded/operationResults/200';
var headers = {
'Azure-AsyncOperation': pollingUri
};
@ -1455,7 +1436,7 @@ var lros = function (coverage) {
var header = req.get("x-ms-client-request-id");
var scenario = 'CustomHeaderPostAsyncSucceded';
if (header && header.toLowerCase() === "9C4D50EE-2D56-4CD3-8152-34347DC9F2B0".toLowerCase()) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/customheader/postasync/retry/succeeded/operationResults/200';
var pollingUri = getRequestBaseUrl(req) + '/lro/customheader/postasync/retry/succeeded/operationResults/200';
var headers = {
'Azure-AsyncOperation': pollingUri
};
@ -1499,7 +1480,7 @@ var lros = function (coverage) {
var scenario = 'CustomHeaderPostSucceeded';
var header = req.get("x-ms-client-request-id");
if (header && header.toLowerCase() === "9C4D50EE-2D56-4CD3-8152-34347DC9F2B0".toLowerCase()) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/customheader/post/202/retry/200';
var pollingUri = getRequestBaseUrl(req) + '/lro/customheader/post/202/retry/200';
var headers = {
'Location': pollingUri
};
@ -1514,7 +1495,7 @@ var lros = function (coverage) {
var scenario = 'CustomHeaderPostSucceeded';
var header = req.get("x-ms-client-request-id");
if (header && header.toLowerCase() === "9C4D50EE-2D56-4CD3-8152-34347DC9F2B0".toLowerCase()) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/lro/customheader/post/newuri/202/retry/200';
var pollingUri = getRequestBaseUrl(req) + '/lro/customheader/post/newuri/202/retry/200';
var headers = {
'Location': pollingUri
};

Просмотреть файл

@ -3,6 +3,8 @@ var router = express.Router();
var util = require('util');
var utils = require('../util/utils');
const getRequestBaseUrl = (request) => `${request.protocol}://${request.get("host")}`;
var hasScenarioCookie = function(req, scenario) {
var cookies = req.headers['cookie'];
var cookieMatch;
@ -55,7 +57,7 @@ var paging = function(coverage) {
router.get('/nullnextlink', function(req, res, next) {
coverage["PagingNextLinkNameNull"]++;
res.status(200).json({ "values" : [ {"properties":{"id": 1, "name": "Product" }}], "nextLink":"http://localhost:" + utils.getPort() + "/paging/idontexistraise404" });
res.status(200).json({ "values" : [ {"properties":{"id": 1, "name": "Product" }}], "nextLink":getRequestBaseUrl(req) + "/paging/idontexistraise404" });
});
router.get('/single', function(req, res, next) {
@ -66,13 +68,13 @@ var paging = function(coverage) {
router.get('/multiple', function(req, res, next) {
coverage["PagingMultiple"]++;
res.status(200).json({ "values" : [ {"properties":{"id": 1, "name": "Product" }}], "nextLink":"http://localhost:" + utils.getPort() + "/paging/multiple/page/2" });
res.status(200).json({ "values" : [ {"properties":{"id": 1, "name": "Product" }}], "nextLink":getRequestBaseUrl(req) + "/paging/multiple/page/2" });
});
router.get('/multiple/getWithQueryParams', function(req, res, next) {
// No coverage added here, gets added in next operation nextOperationWithQueryParams
if (req.query['requiredQueryParameter'] == '100' && req.query['queryConstant'] == 'true') {
res.status(200).json({ "values" : [ {"properties":{"id": 1, "name": "Product" }}], "nextLink":"http://localhost:" + utils.getPort() + "/paging/multiple/nextOperationWithQueryParams" });
res.status(200).json({ "values" : [ {"properties":{"id": 1, "name": "Product" }}], "nextLink":getRequestBaseUrl(req) + "/paging/multiple/nextOperationWithQueryParams" });
}
else{
utils.send400(res, next, 'The query parameters to getWithQueryParams were not passed correctly');
@ -91,7 +93,7 @@ var paging = function(coverage) {
router.get('/multiple/page/:pagenumber', function(req, res, next) {
if (req.params.pagenumber < 10) {
res.status(200).json({ "values": [ {"properties":{"id" : parseInt(req.params.pagenumber), "name": "product"}} ], "nextLink": "http://localhost:" + utils.getPort() + "/paging/multiple/page/" + (++req.params.pagenumber) });
res.status(200).json({ "values": [ {"properties":{"id" : parseInt(req.params.pagenumber), "name": "product"}} ], "nextLink": getRequestBaseUrl(req) + "/paging/multiple/page/" + (++req.params.pagenumber) });
} else {
res.status(200).json({"values": [ {"properties":{"id" : parseInt(req.params.pagenumber), "name": "product"}} ]});
}
@ -100,12 +102,12 @@ var paging = function(coverage) {
router.get('/multiple/odata', function(req, res, next) {
coverage["PagingOdataMultiple"]++;
res.status(200).json({ "values" : [ {"properties":{"id": 1, "name": "Product" }}], "odata.nextLink":"http://localhost:" + utils.getPort() + "/paging/multiple/odata/page/2" });
res.status(200).json({ "values" : [ {"properties":{"id": 1, "name": "Product" }}], "odata.nextLink":getRequestBaseUrl(req) + "/paging/multiple/odata/page/2" });
});
router.get('/multiple/odata/page/:pagenumber', function(req, res, next) {
if (req.params.pagenumber < 10) {
res.status(200).json({ "values": [ {"properties":{"id" : parseInt(req.params.pagenumber), "name": "product"}} ], "odata.nextLink": "http://localhost:" + utils.getPort() + "/paging/multiple/odata/page/" + (++req.params.pagenumber) });
res.status(200).json({ "values": [ {"properties":{"id" : parseInt(req.params.pagenumber), "name": "product"}} ], "odata.nextLink": getRequestBaseUrl(req) + "/paging/multiple/odata/page/" + (++req.params.pagenumber) });
} else {
res.status(200).json({"values": [ {"properties":{"id" : parseInt(req.params.pagenumber), "name": "product"}} ]});
}
@ -114,12 +116,12 @@ var paging = function(coverage) {
router.get('/multiple/withpath/:offset', function(req, res, next) {
coverage["PagingMultiplePath"]++;
res.status(200).json({ "values" : [ {"properties":{"id": 1, "name": "Product" }}], "nextLink":"http://localhost:" + utils.getPort() + "/paging/multiple/withpath/page/" + req.params.offset + "/2" });
res.status(200).json({ "values" : [ {"properties":{"id": 1, "name": "Product" }}], "nextLink":getRequestBaseUrl(req) + "/paging/multiple/withpath/page/" + req.params.offset + "/2" });
});
router.get('/multiple/withpath/page/:offset/:pagenumber', function(req, res, next) {
if (req.params.pagenumber < 10) {
res.status(200).json({ "values": [ {"properties":{"id" : parseInt(req.params.pagenumber) + parseInt(req.params.offset), "name": "product"}} ], "nextLink": "http://localhost:" + utils.getPort() + "/paging/multiple/withpath/page/" + req.params.offset + "/" + ++req.params.pagenumber});
res.status(200).json({ "values": [ {"properties":{"id" : parseInt(req.params.pagenumber) + parseInt(req.params.offset), "name": "product"}} ], "nextLink": getRequestBaseUrl(req) + "/paging/multiple/withpath/page/" + req.params.offset + "/" + ++req.params.pagenumber});
} else {
res.status(200).json({"values": [ {"properties":{"id" : parseInt(req.params.pagenumber) + parseInt(req.params.offset), "name": "product"}} ]});
}
@ -133,12 +135,12 @@ var paging = function(coverage) {
} else {
coverage[scenario]++;
removeScenarioCookie(res);
res.status(200).json({ "values" : [ {"properties":{"id": 1, "name": "Product" }}], "nextLink": "http://localhost:" + utils.getPort() + "/paging/multiple/page/2" });
res.status(200).json({ "values" : [ {"properties":{"id": 1, "name": "Product" }}], "nextLink": getRequestBaseUrl(req) + "/paging/multiple/page/2" });
}
});
router.get('/multiple/retrysecond', function(req, res, next) {
res.status(200).json({ "values" : [ {"properties":{"id": 1, "name": "Product" }}], "nextLink": "http://localhost:" + utils.getPort() + "/paging/multiple/retrysecond/page/2" });
res.status(200).json({ "values" : [ {"properties":{"id": 1, "name": "Product" }}], "nextLink": getRequestBaseUrl(req) + "/paging/multiple/retrysecond/page/2" });
});
router.get('/multiple/retrysecond/page/:pagenumber', function(req, res, next) {
@ -150,10 +152,10 @@ var paging = function(coverage) {
} else {
coverage[scenario]++;
removeScenarioCookie(res);
res.status(200).json({ "values" : [ {"properties":{"id": 1, "name": "Product" }}], "nextLink": "http://localhost:" + utils.getPort() + "/paging/multiple/retrysecond/page/3" });
res.status(200).json({ "values" : [ {"properties":{"id": 1, "name": "Product" }}], "nextLink": getRequestBaseUrl(req) + "/paging/multiple/retrysecond/page/3" });
}
} else if (req.params.pagenumber < 10) {
res.status(200).json({ "values": [ {"properties":{"id" : parseInt(req.params.pagenumber), "name": "product"}} ], "nextLink": "http://localhost:" + utils.getPort() + "/paging/multiple/retrysecond/page/" + (++req.params.pagenumber)});
res.status(200).json({ "values": [ {"properties":{"id" : parseInt(req.params.pagenumber), "name": "product"}} ], "nextLink": getRequestBaseUrl(req) + "/paging/multiple/retrysecond/page/" + (++req.params.pagenumber)});
} else {
res.status(200).json({"values": [ {"properties":{"id" : parseInt(req.params.pagenumber), "name": "product"}} ]});
}
@ -221,10 +223,10 @@ var paging = function(coverage) {
/*** PAGEABLE LROs ***/
router.post('/multiple/lro', function (req, res, next) {
var pollingUri = 'http://localhost:' + utils.getPort() + '/paging/multiple/lro/200';
var pollingUri = getRequestBaseUrl(req) + '/paging/multiple/lro/200';
var headers = {
'Azure-AsyncOperation': pollingUri,
'Location': 'http://localhost:' + utils.getPort() + '/paging/multiple',
'Location': getRequestBaseUrl(req) + '/paging/multiple',
'Retry-After': 0
};
res.set(headers).status(202).json({ "status": "Accepted"});
@ -243,7 +245,7 @@ var paging = function(coverage) {
router.get('/multiple/failure', function(req, res, next) {
coverage["PagingMultipleFailure"]++;
res.status(200).json({ "values" : [ {"properties":{"id": 1, "name": "Product" }}], "nextLink": "http://localhost:" + utils.getPort() + "/paging/multiple/failure/page/2" });
res.status(200).json({ "values" : [ {"properties":{"id": 1, "name": "Product" }}], "nextLink": getRequestBaseUrl(req) + "/paging/multiple/failure/page/2" });
});
router.get('/multiple/failure/page/:pagenumber', function(req, res, next) {

Просмотреть файл

@ -45,7 +45,7 @@ const report = function(coverage, azureCoverage, optionalCoverage) {
Object.getOwnPropertyNames(coverage).forEach(function(val, idx, array) {
optionalCoverage[val] = 0;
});
res.status(200).send().end();
});
}

Просмотреть файл

@ -1,27 +0,0 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
var io = require('socket.io-client');
var Constants = require('../util/constants')
/**
* Get HTTP server port and socket client.
*/
var port = process.env.PORT || Constants.DEFAULT_SERVER_PORT;
var socketClient = io.connect('http://localhost:' + port);
/**
* Set a timeout so if we can't connect to the socket self-terminate.
*/
setTimeout(() => {
process.exit(0);
}, 3000);
/**
* Send the npmStop signal to the server process.
*/
socketClient.on('connect', () => {
socketClient.emit('npmStop');
});

Просмотреть файл

@ -1,105 +0,0 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../app');
var utils = require('../util/utils.js');
var debug = require('debug')('server:server');
var http = require('http');
var Constants = require('../util/constants')
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || Constants.DEFAULT_SERVER_PORT);
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Listen on a socket for the npmStop signal, this will terminate the HTTP server.
*/
let io = require('socket.io')(server);
io.on('connection', (socketServer) => {
socketServer.on('npmStop', () => {
console.log('Received npmStop signal, server will exit');
process.exit(0);
});
});
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
console.log('Server started at port ' + addr.port);
utils.setPort(addr.port);
}

Просмотреть файл

@ -22,15 +22,6 @@ exports.coerceDate = function(targetObject) {
return JSON.parse(stringRep);
};
exports.setPort = function(port)
{
serverPort = port;
}
exports.getPort = function () {
return serverPort;
}
exports.toPascalCase = function(input) {
return '' + input.charAt(0).toUpperCase() + input.slice(1);
}

Просмотреть файл

@ -1,8 +1,10 @@
{
"name": "@microsoft.azure/autorest.testserver",
"version": "2.10.68",
"main": "./legacy/startup/www.js",
"version": "3.0.0",
"description": "Autorest test server.",
"main": "dist/cli/cli.js",
"bin": {
"autorest-testserver": "./dist/cli/cli.js",
"start-autorest-express": "./.scripts/start-autorest-express.js",
"start-autorest-wiremock": "./.scripts/start-autorest-wiremock.js"
},
@ -13,40 +15,73 @@
"author": "Microsoft Corporation",
"license": "MIT",
"scripts": {
"start": "node ./legacy/startup/www",
"stop": "node ./legacy/startup/shutdown",
"testci": "echo no tests",
"publish-preview": "npm publish",
"dev": "npm run watch",
"start": "npm run build && node ./dist/cli/cli.js",
"start:prod": "node ./dist/cli/cli.js",
"start:dev": "concurrently \"npm run watch\" \"nodemon ./dist/cli/cli.js\"",
"stop": "node ./dist/cli/cli.js stop",
"fix": "eslint . --fix --ext .ts",
"lint": "eslint ./src --ext .ts --max-warnings=0",
"test": "jest --watchAll --coverage=false",
"test:ci": "jest --ci",
"watch": "tsc -p ./tsconfig.build.json --watch",
"build": "tsc -p ./tsconfig.build.json",
"clean": "rimraf dist/",
"coverage-push": "node ./legacy/coverage/app-immediate-push",
"coverage-show": "node ./legacy/coverage/app-show",
"show-results": "node ./legacy/coverage/app-show",
"build": "rimraf ./__files && cpy ./swagger/* ./__files && cd __files && shx ls *.json > files.txt",
"prepublish": "npm run build"
},
"engines": {
"node": ">=10"
},
"devDependencies": {
"cpy-cli": "~2.0.0",
"mkdirp": "~0.5.1",
"rimraf": "~3.0.0",
"shx": "0.3.2"
"@types/axios": "^0.14.0",
"@types/body-parser": "^1.19.0",
"@types/commonmark": "^0.27.0",
"@types/deep-equal": "^1.0.1",
"@types/express": "~4.17.9",
"@types/express-promise-router": "^3.0.0",
"@types/glob": "^7.1.3",
"@types/jest": "^26.0.20",
"@types/js-yaml": "~4.0.0",
"@types/morgan": "^1.9.2",
"@types/mustache": "^4.1.0",
"@types/node": "~14.14.20",
"@types/supertest": "^2.0.10",
"@types/xml2js": "^0.4.7",
"@types/yargs": "^15.0.12",
"@typescript-eslint/eslint-plugin": "^4.12.0",
"@typescript-eslint/parser": "^4.12.0",
"concurrently": "^5.3.0",
"eslint": "^7.17.0",
"eslint-plugin-import": "~2.22.1",
"eslint-plugin-prettier": "~3.1.4",
"eslint-plugin-unicorn": "~25.0.1",
"nodemon": "^2.0.7",
"prettier": "~2.2.1",
"rimraf": "^3.0.2",
"supertest": "^6.0.1",
"typescript": "~4.1.3"
},
"dependencies": {
"azure-storage": "^2.4.0",
"body-parser": "^1.18.3",
"busboy": "*",
"cookie-parser": "^1.4.3",
"cors": "^2.8.4",
"debug": "^3.1.0",
"express": "^4.16.3",
"pug": "^2.0.3",
"morgan": "^1.9.0",
"request": "^2.87.0",
"request-promise-native": "^1.0.5",
"serve-favicon": "^2.5.0",
"socket.io": "^2.1.1",
"socket.io-client": "^2.1.1",
"underscore": "*",
"xml2js": "^0.4.19",
"wiremock": "2.25.0",
"chalk": "3.0.0"
"axios": "^0.21.1",
"body-parser": "^1.19.0",
"busboy": "^0.3.1",
"commonmark": "^0.27.0",
"deep-equal": "^2.0.5",
"express": "~4.17.1",
"express-promise-router": "^4.0.1",
"jest": "^26.6.3",
"js-yaml": "~4.0.0",
"morgan": "^1.10.0",
"mustache": "^4.1.0",
"source-map-support": "^0.5.19",
"ts-jest": "^26.4.4",
"underscore": "^1.12.0",
"winston": "~3.3.3",
"wiremock": "^2.27.2",
"xml2js": "^0.4.23",
"yargs": "^16.2.0"
}
}

6
src/api/index.ts Normal file
Просмотреть файл

@ -0,0 +1,6 @@
export * from "./response-content-utils";
export * from "./mock-api-router";
export * from "./validation-error";
import { MockApiRouter } from "./mock-api-router";
export const app = new MockApiRouter();

120
src/api/mock-api-router.ts Normal file
Просмотреть файл

@ -0,0 +1,120 @@
import { Response, Router } from "express";
import PromiseRouter from "express-promise-router";
import { logger } from "../logger";
import { RequestExt } from "../server";
import { coverageService } from "../services";
import { MockRequestHandler, processRequest } from "./request-processor";
export type HttpMethod = "get" | "post" | "put" | "patch" | "delete" | "head";
export type Category = "vanilla" | "azure" | "optional";
export class MockApiRouter {
public router: Router;
private currentCategory: Category | undefined;
public constructor() {
this.router = PromiseRouter();
}
/**
* Set the category for the route definition inside of the provided function.
* @param category Category.
* @param callback Callback where are defined the mock routes.
*/
public category(category: Category, callback: () => void): void {
this.currentCategory = category;
callback();
this.currentCategory = undefined;
}
/**
* Register a GET request for the provided uri.
* @param uri URI to match.
* @param name Name of the scenario(For coverage).
* @param func Request handler.
*/
public get(uri: string, name: string, func: MockRequestHandler): void {
this.request("get", uri, name, func);
}
/**
* Register a POST request for the provided uri.
* @param uri URI to match.
* @param name Name of the scenario(For coverage).
* @param func Request handler.
*/
public post(uri: string, name: string, func: MockRequestHandler): void {
this.request("post", uri, name, func);
}
/**
* Register a PUT request for the provided uri.
* @param uri URI to match.
* @param name Name of the scenario(For coverage).
* @param func Request handler.
*/
public put(uri: string, name: string, func: MockRequestHandler): void {
this.request("put", uri, name, func);
}
/**
* Register a PATCH request for the provided uri.
* @param uri URI to match.
* @param name Name of the scenario(For coverage).
* @param func Request handler.
*/
public patch(uri: string, name: string, func: MockRequestHandler): void {
this.request("patch", uri, name, func);
}
/**
* Register a DELETE request for the provided uri.
* @param uri URI to match.
* @param name Name of the scenario(For coverage).
* @param func Request handler.
*/
public delete(uri: string, name: string, func: MockRequestHandler): void {
this.request("delete", uri, name, func);
}
/**
* Register a HEAD request for the provided uri.
* @param uri URI to match.
* @param name Name of the scenario(For coverage).
* @param func Request handler.
*/
public head(uri: string, name: string, func: MockRequestHandler): void {
this.request("head", uri, name, func);
}
/**
* Register a request for the provided uri.
* @param method Method to use.
* @param uri URI to match.
* @param name Name of the scenario(For coverage).
* @param func Request handler.
*
* @note prefer to use the coresponding method method directly instead of `#request()`(i.e `#get(), #post()`)
*/
public request(method: HttpMethod, uri: string, name: string, func: MockRequestHandler): void {
logger.info(`Registering route ${method} ${uri} (${name})`);
if (this.currentCategory === undefined) {
throw new Error(
[
`Cannot register route ${method} ${uri} (${name}), missing category.`,
`Please wrap it in:`,
`app.category("vanilla" | "azure", () => {`,
` // app.get(...`,
`});`,
"",
].join("\n"),
);
}
const category = this.currentCategory;
coverageService.register(category, name);
this.router.route(uri)[method](async (req: RequestExt, res: Response) => {
await processRequest(category, name, req, res, func);
});
}
}

33
src/api/mock-request.ts Normal file
Просмотреть файл

@ -0,0 +1,33 @@
import { RequestExt } from "../server";
import { getRequestBaseUrl } from "../utils";
import { validateBodyEquals, validateRawBodyEquals } from "./request-validations";
export const BODY_NOT_EQUAL_ERROR_MESSAGE = "Body provided doesn't match expected body.";
export class MockRequest {
public readonly baseUrl: string;
public readonly headers: { [key: string]: string };
public constructor(private originalRequest: RequestExt) {
this.baseUrl = getRequestBaseUrl(originalRequest);
this.headers = originalRequest.headers as { [key: string]: string };
}
/**
* Expect the raw body of the request to match the given string.
* @param rawBody Raw request body.
* @throws {ValidationError} if there is an error.
*/
public rawBodyEquals(expectedRawBody: string | undefined): void {
validateRawBodyEquals(this.originalRequest, expectedRawBody);
}
/**
* Expect the body of the request to match the given object.
* @param rawBody Raw request body.
* @throws {ValidationError} if there is an error.
*/
public bodyEquals(expectedRawBody: unknown | undefined): void {
validateBodyEquals(this.originalRequest, expectedRawBody);
}
}

12
src/api/mock-response.ts Normal file
Просмотреть файл

@ -0,0 +1,12 @@
export interface MockResponse {
status: number;
headers?: {
[key: string]: string;
};
body?: MockResponseBody;
}
export interface MockResponseBody {
contentType: string;
rawContent: string | undefined;
}

Просмотреть файл

@ -0,0 +1,66 @@
import { Response } from "express";
import { logger } from "../logger";
import { RequestExt } from "../server";
import { coverageService } from "../services";
import { MockRequest } from "./mock-request";
import { MockResponse } from "./mock-response";
import { ValidationError } from "./validation-error";
export type MockRequestHandler = (req: MockRequest) => MockResponse | Promise<MockResponse>;
export const processRequest = async (
category: string,
name: string,
request: RequestExt,
response: Response,
func: MockRequestHandler,
): Promise<void> => {
const mockRequest = new MockRequest(request);
const mockResponse = await callHandler(mockRequest, response, func);
if (mockResponse === undefined) {
return;
}
if (mockResponse.status >= 200 && mockResponse.status < 300) {
await coverageService.track(category, name);
}
processResponse(response, mockResponse);
};
const processResponse = (response: Response, mockResponse: MockResponse) => {
response.status(mockResponse.status);
if (mockResponse.headers) {
response.set(mockResponse.headers);
}
if (mockResponse.body) {
response.contentType(mockResponse.body.contentType).send(mockResponse.body.rawContent);
}
response.end();
};
const callHandler = async (
mockRequest: MockRequest,
response: Response,
func: MockRequestHandler,
): Promise<MockResponse | undefined> => {
try {
return func(mockRequest);
} catch (e) {
if (!(e instanceof ValidationError)) {
throw e;
}
logger.warn(
[`Request validation failed: ${e.message}:`, ` Expected:\n ${e.expected}`, ` Actual: \n${e.actual}`].join("\n"),
);
response
.status(400)
.contentType("application/json")
.send(e.toJSON ? e.toJSON() : JSON.stringify(e.message))
.end();
return undefined;
}
};

Просмотреть файл

@ -0,0 +1,41 @@
import deepEqual from "deep-equal";
import { RequestExt } from "../server";
import { ValidationError } from "./validation-error";
export const BODY_NOT_EQUAL_ERROR_MESSAGE = "Body provided doesn't match expected body";
export const validateRawBodyEquals = (request: RequestExt, expectedRawBody: string | undefined): void => {
const actualRawBody = request.rawBody;
if (expectedRawBody == null) {
if (!isBodyEmpty(actualRawBody)) {
throw new ValidationError(BODY_NOT_EQUAL_ERROR_MESSAGE, expectedRawBody, actualRawBody);
}
return;
}
if (actualRawBody !== expectedRawBody) {
throw new ValidationError(BODY_NOT_EQUAL_ERROR_MESSAGE, expectedRawBody, actualRawBody);
}
};
export const validateBodyEquals = (request: RequestExt, expectedBody: unknown | undefined): void => {
if (expectedBody == null) {
if (!isBodyEmpty(request.rawBody)) {
throw new ValidationError(BODY_NOT_EQUAL_ERROR_MESSAGE, expectedBody, request.rawBody);
}
return;
}
if (!deepEqual(request.body, expectedBody, { strict: true })) {
throw new ValidationError(BODY_NOT_EQUAL_ERROR_MESSAGE, expectedBody, request.body);
}
};
/**
* Check if the provided body is empty.
* @param body express.js request body.
*/
const isBodyEmpty = (body: string | undefined | null) => {
return body == null || body === "";
};

Просмотреть файл

@ -0,0 +1,13 @@
import { MockResponseBody } from "./mock-response";
/**
* Serialize the provided content as json to use in a MockResponse body.
* @content Object to return as json.
* @returns {MockResponseBody} response body with application/json content type.
*/
export const json = (content: unknown): MockResponseBody => {
return {
contentType: "application/json",
rawContent: JSON.stringify(content),
};
};

Просмотреть файл

@ -0,0 +1,15 @@
export class ValidationError extends Error {
/**
* Error thrown there there is a validation issue.
* @param message Message describing the error.
* @param expected expected value.
* @param actual actual value.
*/
constructor(message: string, public expected: unknown | undefined, public actual: unknown | undefined) {
super(message);
}
public toJSON(): string {
return JSON.stringify({ message: this.message, expected: this.expected, actual: this.actual });
}
}

39
src/app/app.ts Normal file
Просмотреть файл

@ -0,0 +1,39 @@
import path from "path";
import { app } from "../api";
import { registerLegacyRoutes } from "../legacy";
import { logger } from "../logger";
import { internalRouter } from "../routes";
import { MockApiServer } from "../server";
import { coverageService } from "../services";
import { findFilesFromPattern } from "../utils";
import { ApiMockAppConfig } from "./config";
const ROUTE_FOLDER = path.join(__dirname, "../test-routes");
export class ApiMockApp {
private server: MockApiServer;
constructor(private config: ApiMockAppConfig) {
coverageService.coverageDirectory = config.coverageDirectory;
this.server = new MockApiServer({ port: config.port });
}
public async start(): Promise<void> {
this.server.use("/", internalRouter);
await requireMockRoutes(ROUTE_FOLDER);
registerLegacyRoutes(this.server);
const apiRouter = app;
this.server.use("/", apiRouter.router);
this.server.start();
}
}
const requireMockRoutes = async (routesFolder: string) => {
const files = await findFilesFromPattern(path.join(routesFolder, "/**/*.js"));
logger.debug("Detected routes:", files);
for (const file of files) {
require(path.resolve(file));
}
};

4
src/app/config.ts Normal file
Просмотреть файл

@ -0,0 +1,4 @@
export interface ApiMockAppConfig {
port: number;
coverageDirectory: string;
}

2
src/app/index.ts Normal file
Просмотреть файл

@ -0,0 +1,2 @@
export * from "./app";
export * from "./config";

44
src/cli/args-parser.ts Normal file
Просмотреть файл

@ -0,0 +1,44 @@
import { join } from "path";
import yargs, { CommandModule } from "yargs";
import { CliConfig } from "./cli-config";
export const DEFAULT_PORT = 3000;
export const parseArgs = (argv: string[]): CliConfig => {
const cli = yargs(argv)
.help()
.strict()
.command("$0", "Run the autorest test server.")
.command("stop", "Stop the autorest test server running at the provided port.")
.option("verbose", {
alias: "v",
type: "boolean",
description: "Run with verbose logging level.",
})
.option("debug", {
type: "boolean",
description: "Run with debug logging level.",
})
.option("level", {
type: "string",
description: "Run with given logging level.",
})
.option("port", {
alias: "p",
type: "number",
description: "Port where to host the server",
default: DEFAULT_PORT,
})
.option("coverageDirectory", {
type: "string",
description: "Path of the directory where the coverage reports should be saved.",
default: join(process.cwd(), "coverage"),
});
const options = cli.argv;
return {
...options,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
command: (options._[0] as any) ?? "run",
};
};

21
src/cli/cli-config.ts Normal file
Просмотреть файл

@ -0,0 +1,21 @@
export interface CliConfig {
// Logging
verbose?: boolean;
debug?: boolean;
level?: string;
/**
* Port to serve the application.
*/
port: number;
/**
* Directory where the coverage reports should be saved.
*/
coverageDirectory: string;
/**
* Command to use
*/
command: "run" | "stop";
}

27
src/cli/cli.ts Normal file
Просмотреть файл

@ -0,0 +1,27 @@
#!/usr/bin/env node
// eslint-disable-next-line @typescript-eslint/no-var-requires
require("source-map-support").install();
import { hideBin } from "yargs/helpers";
import { logger, setLoggingLevelFromConfig } from "../logger";
import { parseArgs } from "./args-parser";
import { stopCommand, runCommand } from "./commands";
const run = async () => {
const cliConfig = parseArgs(hideBin(process.argv));
setLoggingLevelFromConfig(cliConfig);
switch (cliConfig.command) {
case "run":
await runCommand(cliConfig);
break;
case "stop":
await stopCommand(cliConfig);
break;
}
};
run().catch((e) => {
logger.error("Error", e);
process.exit(1);
});

Просмотреть файл

@ -0,0 +1,2 @@
export * from "./run-command";
export * from "./stop-command";

Просмотреть файл

@ -0,0 +1,14 @@
import { ApiMockApp, ApiMockAppConfig } from "../../app";
import { CliConfig } from "../cli-config";
const getAppConfig = (cliConfig: CliConfig): ApiMockAppConfig => {
return {
coverageDirectory: cliConfig.coverageDirectory,
port: cliConfig.port,
};
};
export const runCommand = async (cliConfig: CliConfig): Promise<void> => {
const app = new ApiMockApp(getAppConfig(cliConfig));
await app.start();
};

Просмотреть файл

@ -0,0 +1,18 @@
import axios from "axios";
import { AdminUrls } from "../../constants";
import { logger } from "../../logger";
import { CliConfig } from "../cli-config";
export const stopCommand = async ({ port }: CliConfig): Promise<void> => {
logger.info("Stopping server at port {port}", port);
try {
const url = `http://localhost:${port}${AdminUrls.stop}`;
const response = await axios.post(url);
logger.debug(`Call success: ${url} ${response.status}`);
logger.info(`Successfuly stopped server at port ${port}`);
process.exit(0);
} catch (e) {
logger.error("Error while trying to stop server", e);
process.exit(-1);
}
};

1
src/cli/index.ts Normal file
Просмотреть файл

@ -0,0 +1 @@
export * from "./args-parser";

7
src/constants.ts Normal file
Просмотреть файл

@ -0,0 +1,7 @@
import { join, resolve } from "path";
export const ProjectRoot = resolve(join(__dirname), "..");
export const AdminUrls = {
stop: "/.admin/stop",
};

1
src/legacy/index.ts Normal file
Просмотреть файл

@ -0,0 +1 @@
export * from "./legacy";

550
src/legacy/legacy.ts Normal file
Просмотреть файл

@ -0,0 +1,550 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/**
* This file handles loading the legacy test server routes.
*/
import { Category } from "../api";
import { ProjectRoot } from "../constants";
import { MockApiServer } from "../server";
import { CoverageMap, coverageService } from "../services";
const legacyRoutePath = `${ProjectRoot}/legacy/routes`;
const requireLegacy = (filename: string) => require(`${legacyRoutePath}/${filename}`);
const additionalProperties = requireLegacy("additionalProperties.js");
const array = requireLegacy("array");
const parameterGrouping = requireLegacy("azureParameterGrouping.js");
const azureSpecial = requireLegacy("azureSpecials");
const azureUrl = requireLegacy("azureUrl");
const bool = requireLegacy("bool");
const byte = requireLegacy("byte");
const complex = requireLegacy("complex");
const customUri = requireLegacy("customUri.js");
const date = requireLegacy("date");
const datetime = requireLegacy("datetime");
const datetimeRfc1123 = requireLegacy("datetime-rfc1123");
const dictionary = requireLegacy("dictionary");
const duration = requireLegacy("duration");
const errorStatusCodes = requireLegacy("errorStatusCodes.js");
const extensibleEnums = requireLegacy("extensibleEnums.js");
const files = requireLegacy("files");
const formData = requireLegacy("formData");
const header = requireLegacy("header");
const httpResponses = requireLegacy("httpResponses");
const routes = requireLegacy("index");
const integer = requireLegacy("int");
const lroParameterizedEndpoints = requireLegacy("lroParameterizedEndpoints.js");
const lros = requireLegacy("lros");
const mediatypes = requireLegacy("mediatypes");
const modelFlatten = requireLegacy("model-flatten");
const multiapi = requireLegacy("multiapi");
const multiapiCustomBaseUrl = requireLegacy("multiapiCustomBaseUrl.js");
const multipleInheritance = requireLegacy("multipleInheritance.js");
const nonStringEnums = requireLegacy("nonStringEnums.js");
const number = requireLegacy("number");
const objectType = requireLegacy("objectType.js");
const paging = requireLegacy("paging");
const pathitem = requireLegacy("pathitem");
const paths = requireLegacy("paths");
const queries = requireLegacy("queries");
const reqopt = requireLegacy("reqopt");
const time = requireLegacy("time.js");
const validation = requireLegacy("validation.js");
const xml = requireLegacy("xml.js");
const proxyCoverage = (category: Category, coverage: CoverageMap) => {
for (const key of Object.keys(coverage)) {
coverageService.legacyTrack(category, key as string, 0);
}
return new Proxy(coverage, {
get: (_, key) => coverageService.getAllForCategory(category)[key as string],
set: (obj, key, value) => {
coverageService.legacyTrack(category, key as string, value);
obj[key as string] = value;
return true;
},
});
};
export const registerLegacyRoutes = (app: MockApiServer): void => {
const azurecoverage = proxyCoverage("azure", {});
const optionalCoverage = proxyCoverage("optional", {
getDecimalInvalid: 0,
getDecimalBig: 0,
getDecimalSmall: 0,
getDecimalBigPositiveDecimal: 0,
getDecimalBigNegativeDecimal: 0,
putDecimalBig: 0,
putDecimalSmall: 0,
putDecimalBigPositiveDecimal: 0,
putDecimalBigNegativeDecimal: 0,
});
const coverage = proxyCoverage("vanilla", {
additionalPropertiesTrue: 0,
additionalPropertiesSubclass: 0,
additionalPropertiesTypeObject: 0,
additionalPropertiesTypeString: 0,
additionalPropertiesInProperties: 0,
additionalPropertiesInPropertiesWithAPTypeString: 0,
getArrayNull: 0,
getArrayEmpty: 0,
putArrayEmpty: 0,
getArrayInvalid: 0,
getArrayBooleanValid: 0,
putArrayBooleanValid: 0,
getArrayBooleanWithNull: 0,
getArrayBooleanWithString: 0,
getArrayIntegerValid: 0,
putArrayIntegerValid: 0,
getArrayIntegerWithNull: 0,
getArrayIntegerWithString: 0,
getArrayLongValid: 0,
putArrayLongValid: 0,
getArrayLongWithNull: 0,
getArrayLongWithString: 0,
getArrayFloatValid: 0,
putArrayFloatValid: 0,
getArrayFloatWithNull: 0,
getArrayFloatWithString: 0,
getArrayDoubleValid: 0,
putArrayDoubleValid: 0,
getArrayDoubleWithNull: 0,
getArrayDoubleWithString: 0,
getArrayStringValid: 0,
putArrayStringValid: 0,
getArrayEnumValid: 0,
putArrayEnumValid: 0,
getArrayStringEnumValid: 0,
putArrayStringEnumValid: 0,
getArrayStringWithNull: 0,
getArrayStringWithNumber: 0,
getArrayDateValid: 0,
putArrayDateValid: 0,
getArrayDateWithNull: 0,
getArrayDateWithInvalidChars: 0,
getArrayDateTimeValid: 0,
putArrayDateTimeValid: 0,
getArrayDateTimeWithNull: 0,
getArrayDateTimeWithInvalidChars: 0,
getArrayDateTimeRfc1123Valid: 0,
putArrayDateTimeRfc1123Valid: 0,
getArrayDurationValid: 0,
putArrayDurationValid: 0,
getArrayUuidValid: 0,
getArrayUuidWithInvalidChars: 0,
putArrayUuidValid: 0,
getArrayByteValid: 0,
putArrayByteValid: 0,
getArrayByteWithNull: 0,
getArrayArrayNull: 0,
getArrayArrayEmpty: 0,
getArrayArrayItemNull: 0,
getArrayArrayItemEmpty: 0,
getArrayArrayValid: 0,
putArrayArrayValid: 0,
getArrayComplexNull: 0,
getArrayComplexEmpty: 0,
getArrayComplexItemNull: 0,
getArrayComplexItemEmpty: 0,
getArrayComplexValid: 0,
putArrayComplexValid: 0,
getArrayDictionaryNull: 0,
getArrayDictionaryEmpty: 0,
getArrayDictionaryItemNull: 0,
getArrayDictionaryItemEmpty: 0,
getArrayDictionaryValid: 0,
putArrayDictionaryValid: 0,
getBoolTrue: 0,
putBoolTrue: 0,
getBoolFalse: 0,
putBoolFalse: 0,
getBoolInvalid: 0,
getBoolNull: 0,
getByteNull: 0,
getByteEmpty: 0,
getByteNonAscii: 0,
putByteNonAscii: 0,
getByteInvalid: 0,
getDateNull: 0,
getDateInvalid: 0,
getDateOverflow: 0,
getDateUnderflow: 0,
getDateMax: 0,
putDateMax: 0,
getDateMin: 0,
putDateMin: 0,
getDateTimeNull: 0,
getDateTimeInvalid: 0,
getDateTimeOverflow: 0,
getDateTimeUnderflow: 0,
putDateTimeMaxUtc: 0,
getDateTimeMaxUtcLowercase: 0,
getDateTimeMaxUtcUppercase: 0,
getDateTimeMaxLocalPositiveOffsetLowercase: 0,
getDateTimeMaxLocalPositiveOffsetUppercase: 0,
getDateTimeMaxLocalNegativeOffsetLowercase: 0,
getDateTimeMaxLocalNegativeOffsetUppercase: 0,
getDateTimeMinUtc: 0,
putDateTimeMinUtc: 0,
getDateTimeMinLocalPositiveOffset: 0,
getDateTimeMinLocalNegativeOffset: 0,
getDateTimeRfc1123Null: 0,
getDateTimeRfc1123Invalid: 0,
getDateTimeRfc1123Overflow: 0,
getDateTimeRfc1123Underflow: 0,
getDateTimeRfc1123MinUtc: 0,
putDateTimeRfc1123Max: 0,
putDateTimeRfc1123Min: 0,
getDateTimeRfc1123MaxUtcLowercase: 0,
getDateTimeRfc1123MaxUtcUppercase: 0,
getIntegerNull: 0,
getIntegerInvalid: 0,
getIntegerOverflow: 0,
getIntegerUnderflow: 0,
getLongOverflow: 0,
getLongUnderflow: 0,
putIntegerMax: 0,
putLongMax: 0,
putIntegerMin: 0,
putLongMin: 0,
getNumberNull: 0,
getFloatInvalid: 0,
getDoubleInvalid: 0,
getFloatBigScientificNotation: 0,
putFloatBigScientificNotation: 0,
getDoubleBigScientificNotation: 0,
putDoubleBigScientificNotation: 0,
getDoubleBigPositiveDecimal: 0,
putDoubleBigPositiveDecimal: 0,
getDoubleBigNegativeDecimal: 0,
putDoubleBigNegativeDecimal: 0,
getFloatSmallScientificNotation: 0,
putFloatSmallScientificNotation: 0,
getDoubleSmallScientificNotation: 0,
putDoubleSmallScientificNotation: 0,
getStringNull: 0,
putStringNull: 0,
getStringEmpty: 0,
putStringEmpty: 0,
getStringMultiByteCharacters: 0,
putStringMultiByteCharacters: 0,
getStringWithLeadingAndTrailingWhitespace: 0,
putStringWithLeadingAndTrailingWhitespace: 0,
getStringNotProvided: 0,
getEnumNotExpandable: 0,
putEnumNotExpandable: 0,
putComplexBasicValid: 0,
getComplexBasicValid: 0,
getComplexBasicEmpty: 0,
getComplexBasicNotProvided: 0,
getComplexBasicNull: 0,
getComplexBasicInvalid: 0,
putComplexPrimitiveInteger: 0,
putComplexPrimitiveLong: 0,
putComplexPrimitiveFloat: 0,
putComplexPrimitiveDouble: 0,
putComplexPrimitiveBool: 0,
putComplexPrimitiveString: 0,
putComplexPrimitiveDate: 0,
putComplexPrimitiveDateTime: 0,
putComplexPrimitiveDateTimeRfc1123: 0,
putComplexPrimitiveDuration: 0,
putComplexPrimitiveByte: 0,
getComplexPrimitiveInteger: 0,
getComplexPrimitiveLong: 0,
getComplexPrimitiveFloat: 0,
getComplexPrimitiveDouble: 0,
getComplexPrimitiveBool: 0,
getComplexPrimitiveString: 0,
getComplexPrimitiveDate: 0,
getComplexPrimitiveDateTime: 0,
getComplexPrimitiveDateTimeRfc1123: 0,
getComplexPrimitiveDuration: 0,
getComplexPrimitiveByte: 0,
putComplexArrayValid: 0,
putComplexArrayEmpty: 0,
getComplexArrayValid: 0,
getComplexArrayEmpty: 0,
getComplexArrayNotProvided: 0,
putComplexDictionaryValid: 0,
putComplexDictionaryEmpty: 0,
getComplexDictionaryValid: 0,
getComplexDictionaryEmpty: 0,
getComplexDictionaryNull: 0,
getComplexDictionaryNotProvided: 0,
putComplexInheritanceValid: 0,
getComplexInheritanceValid: 0,
putComplexPolymorphismValid: 0,
getComplexPolymorphismValid: 0,
putComplexPolymorphismComplicated: 0,
getComplexPolymorphismComplicated: 0,
putComplexPolymorphismNoDiscriminator: 0,
putComplexPolymorphicRecursiveValid: 0,
getComplexPolymorphicRecursiveValid: 0,
putComplexReadOnlyPropertyValid: 0,
getComplexReadOnlyPropertyValid: 0,
UrlPathsBoolFalse: 0,
UrlPathsBoolTrue: 0,
UrlPathsIntPositive: 0,
UrlPathsIntNegative: 0,
UrlPathsLongPositive: 0,
UrlPathsLongNegative: 0,
UrlPathsFloatPositive: 0,
UrlPathsFloatNegative: 0,
UrlPathsDoublePositive: 0,
UrlPathsDoubleNegative: 0,
UrlPathsStringUrlEncoded: 0,
UrlPathsStringUrlNonEncoded: 0,
UrlPathsStringEmpty: 0,
UrlPathsStringUnicode: 0,
UrlPathsEnumValid: 0,
UrlPathsByteMultiByte: 0,
UrlPathsByteEmpty: 0,
UrlPathsDateValid: 0,
UrlPathsDateTimeValid: 0,
UrlQueriesBoolFalse: 0,
UrlQueriesBoolTrue: 0,
UrlQueriesBoolNull: 0,
UrlQueriesIntPositive: 0,
UrlQueriesIntNegative: 0,
UrlQueriesIntNull: 0,
UrlQueriesLongPositive: 0,
UrlQueriesLongNegative: 0,
UrlQueriesLongNull: 0,
UrlQueriesFloatPositive: 0,
UrlQueriesFloatNegative: 0,
UrlQueriesFloatNull: 0,
UrlQueriesDoublePositive: 0,
UrlQueriesDoubleNegative: 0,
UrlQueriesDoubleNull: 0,
UrlQueriesStringUrlEncoded: 0,
UrlQueriesStringEmpty: 0,
UrlQueriesStringNull: 0,
UrlQueriesStringUnicode: 0,
UrlQueriesEnumValid: 0,
UrlQueriesEnumNull: 0,
UrlQueriesByteMultiByte: 0,
UrlQueriesByteEmpty: 0,
UrlQueriesByteNull: 0,
UrlQueriesDateValid: 0,
UrlQueriesDateNull: 0,
UrlQueriesDateTimeValid: 0,
UrlQueriesDateTimeNull: 0,
UrlQueriesArrayCsvNull: 0,
UrlQueriesArrayCsvEmpty: 0,
UrlQueriesArrayCsvValid: 0,
UrlQueriesArrayMultiNull: 0,
UrlQueriesArrayMultiEmpty: 0,
UrlQueriesArrayMultiValid: 0,
UrlQueriesArraySsvValid: 0,
UrlQueriesArrayPipesValid: 0,
UrlQueriesArrayTsvValid: 0,
UrlQueriesArrayNoCollectionFormatValid: 0,
UrlPathItemGetAll: 0,
UrlPathItemGetGlobalNull: 0,
UrlPathItemGetGlobalAndLocalNull: 0,
UrlPathItemGetPathItemAndLocalNull: 0,
putDictionaryEmpty: 0,
getDictionaryNull: 0,
getDictionaryEmpty: 0,
getDictionaryInvalid: 0,
getDictionaryNullValue: 0,
getDictionaryNullkey: 0,
getDictionaryKeyEmptyString: 0,
getDictionaryBooleanValid: 0,
getDictionaryBooleanWithNull: 0,
getDictionaryBooleanWithString: 0,
getDictionaryIntegerValid: 0,
getDictionaryIntegerWithNull: 0,
getDictionaryIntegerWithString: 0,
getDictionaryLongValid: 0,
getDictionaryLongWithNull: 0,
getDictionaryLongWithString: 0,
getDictionaryFloatValid: 0,
getDictionaryFloatWithNull: 0,
getDictionaryFloatWithString: 0,
getDictionaryDoubleValid: 0,
getDictionaryDoubleWithNull: 0,
getDictionaryDoubleWithString: 0,
getDictionaryStringValid: 0,
getDictionaryStringWithNull: 0,
getDictionaryStringWithNumber: 0,
getDictionaryDateValid: 0,
getDictionaryDateWithNull: 0,
getDictionaryDateWithInvalidChars: 0,
getDictionaryDateTimeValid: 0,
getDictionaryDateTimeWithNull: 0,
getDictionaryDateTimeWithInvalidChars: 0,
getDictionaryDateTimeRfc1123Valid: 0,
getDictionaryDurationValid: 0,
getDictionaryByteValid: 0,
getDictionaryByteWithNull: 0,
putDictionaryBooleanValid: 0,
putDictionaryIntegerValid: 0,
putDictionaryLongValid: 0,
putDictionaryFloatValid: 0,
putDictionaryDoubleValid: 0,
putDictionaryStringValid: 0,
putDictionaryDateValid: 0,
putDictionaryDateTimeValid: 0,
putDictionaryDateTimeRfc1123Valid: 0,
putDictionaryDurationValid: 0,
putDictionaryByteValid: 0,
getDictionaryComplexNull: 0,
getDictionaryComplexEmpty: 0,
getDictionaryComplexItemNull: 0,
getDictionaryComplexItemEmpty: 0,
getDictionaryComplexValid: 0,
putDictionaryComplexValid: 0,
getDictionaryArrayNull: 0,
getDictionaryArrayEmpty: 0,
getDictionaryArrayItemNull: 0,
getDictionaryArrayItemEmpty: 0,
getDictionaryArrayValid: 0,
putDictionaryArrayValid: 0,
getDictionaryDictionaryNull: 0,
getDictionaryDictionaryEmpty: 0,
getDictionaryDictionaryItemNull: 0,
getDictionaryDictionaryItemEmpty: 0,
getDictionaryDictionaryValid: 0,
putDictionaryDictionaryValid: 0,
putDurationPositive: 0,
getDurationNull: 0,
getDurationInvalid: 0,
getDurationPositive: 0,
HeaderParameterExistingKey: 0,
HeaderResponseExistingKey: 0,
HeaderResponseProtectedKey: 0,
HeaderParameterIntegerPositive: 0,
HeaderParameterIntegerNegative: 0,
HeaderParameterLongPositive: 0,
HeaderParameterLongNegative: 0,
HeaderParameterFloatPositive: 0,
HeaderParameterFloatNegative: 0,
HeaderParameterDoublePositive: 0,
HeaderParameterDoubleNegative: 0,
HeaderParameterBoolTrue: 0,
HeaderParameterBoolFalse: 0,
HeaderParameterStringValid: 0,
HeaderParameterStringNull: 0,
HeaderParameterStringEmpty: 0,
HeaderParameterDateValid: 0,
HeaderParameterDateMin: 0,
HeaderParameterDateTimeValid: 0,
HeaderParameterDateTimeMin: 0,
HeaderParameterDateTimeRfc1123Valid: 0,
HeaderParameterDateTimeRfc1123Min: 0,
HeaderParameterBytesValid: 0,
HeaderParameterDurationValid: 0,
HeaderResponseIntegerPositive: 0,
HeaderResponseIntegerNegative: 0,
HeaderResponseLongPositive: 0,
HeaderResponseLongNegative: 0,
HeaderResponseFloatPositive: 0,
HeaderResponseFloatNegative: 0,
HeaderResponseDoublePositive: 0,
HeaderResponseDoubleNegative: 0,
HeaderResponseBoolTrue: 0,
HeaderResponseBoolFalse: 0,
HeaderResponseStringValid: 0,
HeaderResponseStringNull: 0,
HeaderResponseStringEmpty: 0,
HeaderParameterEnumValid: 0,
HeaderParameterEnumNull: 0,
HeaderResponseEnumValid: 0,
HeaderResponseEnumNull: 0,
HeaderResponseDateValid: 0,
HeaderResponseDateMin: 0,
HeaderResponseDateTimeValid: 0,
HeaderResponseDateTimeMin: 0,
HeaderResponseDateTimeRfc1123Valid: 0,
HeaderResponseDateTimeRfc1123Min: 0,
HeaderResponseBytesValid: 0,
HeaderResponseDurationValid: 0,
ConstantsInPath: 0,
ConstantsInBody: 0,
CustomBaseUri: 0,
CustomBaseUriMoreOptions: 0,
getModelFlattenArray: 0,
putModelFlattenArray: 0,
getModelFlattenDictionary: 0,
putModelFlattenDictionary: 0,
getModelFlattenResourceCollection: 0,
putModelFlattenResourceCollection: 0,
putModelFlattenCustomBase: 0,
postModelFlattenCustomParameter: 0,
putModelFlattenCustomGroupedParameter: 0,
getStringBase64Encoded: 0,
getStringBase64UrlEncoded: 0,
putStringBase64UrlEncoded: 0,
getStringNullBase64UrlEncoding: 0,
getArrayBase64Url: 0,
getDictionaryBase64Url: 0,
UrlPathsStringBase64Url: 0,
UrlPathsArrayCSVInPath: 0,
getUnixTime: 0,
getInvalidUnixTime: 0,
getNullUnixTime: 0,
putUnixTime: 0,
UrlPathsIntUnixTime: 0,
expectedEnum: 0,
unexpectedEnum: 0,
allowedValueEnum: 0,
roundTripEnum: 0,
getEnumReferenced: 0,
putEnumReferenced: 0,
getEnumReferencedConstant: 0,
putEnumReferencedConstant: 0,
expectedNoErrors: 0,
expectedPetSadError: 0,
expectedPetHungryError: 0,
intError: 0,
stringError: 0,
animalNotFoundError: 0,
linkNotFoundError: 0,
});
app.use("/", routes);
app.use("/bool", new bool(coverage).router);
app.use("/int", new integer(coverage).router);
app.use("/number", new number(coverage, optionalCoverage).router);
app.use("/byte", new byte(coverage).router);
app.use("/date", new date(coverage).router);
app.use("/datetime", new datetime(coverage, optionalCoverage).router);
app.use("/datetimeRfc1123", new datetimeRfc1123(coverage).router);
app.use("/duration", new duration(coverage, optionalCoverage).router);
app.use("/array", new array(coverage).router);
app.use("/complex", new complex(coverage).router);
app.use("/dictionary", new dictionary(coverage).router);
app.use("/paths", new paths(coverage).router);
app.use("/queries", new queries(coverage).router);
app.use("/pathitem", new pathitem(coverage).router);
app.use("/header", new header(coverage, optionalCoverage).router);
app.use("/reqopt", new reqopt(coverage).router);
app.use("/files", new files(coverage).router);
app.use("/formdata", new formData(optionalCoverage).router);
app.use("/http", new httpResponses(coverage, optionalCoverage).router);
app.use("/model-flatten", new modelFlatten(coverage).router);
app.use("/lro", new lros(azurecoverage).router);
app.use("/lroParameterizedEndpoints", new lroParameterizedEndpoints(azurecoverage).router);
app.use("/paging", new paging(azurecoverage).router);
app.use("/azurespecials", new azureSpecial(azurecoverage).router);
app.use("/subscriptions", new azureUrl(azurecoverage).router);
app.use("/parameterGrouping", new parameterGrouping(azurecoverage).router);
app.use("/validation", new validation(coverage).router);
app.use("/customUri", new customUri(coverage).router);
app.use("/extensibleEnums", new extensibleEnums(coverage).router);
app.use("/errorStatusCodes", new errorStatusCodes(coverage, optionalCoverage).router);
app.use("/additionalProperties", new additionalProperties(coverage).router);
app.use("/mediatypes", new mediatypes(coverage).router);
app.use("/xml", new xml(coverage).router);
app.use("/multiapi", new multiapi(optionalCoverage).router);
app.use("/objectType", new objectType(coverage).router);
app.use("/nonStringEnums", new nonStringEnums(coverage).router);
app.use("/time", new time(coverage).router);
app.use("/multipleInheritance", new multipleInheritance(coverage).router);
app.use("/multiapiCustomBaseUrl", new multiapiCustomBaseUrl(optionalCoverage).router);
};

16
src/logger.ts Normal file
Просмотреть файл

@ -0,0 +1,16 @@
import winston from "winston";
import { CliConfig } from "./cli/cli-config";
export const logger = winston.createLogger({
level: "info",
transports: [
new winston.transports.Console({
format: winston.format.combine(winston.format.colorize(), winston.format.simple()),
}),
],
});
export const setLoggingLevelFromConfig = (config: CliConfig): void => {
const newLevel = config.level ?? (config.debug ? "debug" : config.verbose ? "verbose" : "info");
logger.level = newLevel;
};

15
src/routes/admin.ts Normal file
Просмотреть файл

@ -0,0 +1,15 @@
import { Router } from "express";
import { AdminUrls } from "../constants";
import { logger } from "../logger";
const router = Router();
router.post(AdminUrls.stop, (_req, res) => {
logger.info("Recieved signal to stop server. Exiting...");
res.status(202).end();
setTimeout(() => {
process.exit(0);
});
});
export const adminRoutes = router;

15
src/routes/coverage.ts Normal file
Просмотреть файл

@ -0,0 +1,15 @@
import { Router } from "express";
import { coverageService } from "../services";
const router = Router();
export const coverageRouter = router;
router.get("/report/clear", (_req, res) => {
coverageService.reset();
return res.status(200).send().end();
});
router.get("/report/:category?", (req, res) => {
const category = req.params.category?.toString() ?? "vanilla";
return res.json(coverageService.getAllForCategory(category)).end();
});

9
src/routes/index.ts Normal file
Просмотреть файл

@ -0,0 +1,9 @@
import { Router } from "express";
import { adminRoutes } from "./admin";
import { coverageRouter } from "./coverage";
const router = Router();
router.use("/", adminRoutes);
router.use("/", coverageRouter);
export const internalRouter = router;

2
src/server/index.ts Normal file
Просмотреть файл

@ -0,0 +1,2 @@
export * from "./server";
export * from "./request-ext";

Просмотреть файл

@ -0,0 +1,8 @@
import { Request } from "express";
/**
* Extension of the express.js request which include a rawBody.
*/
export interface RequestExt extends Request {
rawBody?: string;
}

56
src/server/server.ts Normal file
Просмотреть файл

@ -0,0 +1,56 @@
import { ServerResponse } from "http";
import bodyParser from "body-parser";
import express, { ErrorRequestHandler, RequestHandler, Response } from "express";
import morgan from "morgan";
import { logger } from "../logger";
import { cleanupBody } from "../utils";
import { RequestExt } from "./request-ext";
export interface MockApiServerConfig {
port: number;
}
const errorHandler: ErrorRequestHandler = (err, _req, res, _next) => {
logger.error("Error", err);
res.status(err.status || 500);
res
.contentType("application/json")
.send(err instanceof Error ? { name: err.name, message: err.message, stack: err.stack } : JSON.stringify(err))
.end();
};
const rawBodySaver = (req: RequestExt, res: ServerResponse, buf: Buffer, encoding: BufferEncoding) => {
if (buf && buf.length) {
req.rawBody = cleanupBody(buf.toString(encoding || "utf8"));
}
};
const loggerstream = {
write: (message: string) => logger.info(message),
};
export class MockApiServer {
private app: express.Application;
constructor(private config: MockApiServerConfig) {
this.app = express();
this.app.use(morgan("dev", { stream: loggerstream }));
this.app.use(bodyParser.json({ verify: rawBodySaver, strict: false }));
this.app.use(bodyParser.urlencoded({ verify: rawBodySaver, extended: true }));
this.app.use(bodyParser.text({ type: "*/pdf", verify: rawBodySaver }));
}
public use(route: string, ...handlers: RequestHandler[]): void {
this.app.use(route, ...handlers);
}
public start(): void {
this.app.use(errorHandler);
this.app.listen(this.config.port, () => {
logger.info(`Started server on port ${this.config.port}`);
});
}
}
export type ServerRequestHandler = (request: RequestExt, response: Response) => void;

Просмотреть файл

@ -0,0 +1,92 @@
import fs from "fs";
import { join } from "path";
import { logger } from "../logger";
import { ensureDir } from "../utils";
export interface CoverageMap {
[name: string]: number;
}
export class CoverageService {
public coverageDirectory = "./coverage";
private coverage: { [category: string]: CoverageMap } = {
defaultCategoryName: {},
};
public getAllForCategory(category: string): CoverageMap {
return this.coverage[category] ?? {};
}
/**
* Track usage of a scenario.
* @param category Category for the coverage
* @param name Name of the scenario.
* @param value {Optional} For legacy test set the value of the usage.
*/
public async track(category: string, name: string, value?: number): Promise<void> {
const map = this.coverage[category];
if (!map) {
throw new Error(`Unknown category '${category}'`);
}
if (!(name in map)) {
throw new Error(`Unknown coverage name '${name}' in '${category}'`);
}
map[name] += 1;
await this.saveCoverage(category);
}
/**
* For LEGACY test only.
* @depreacted
*/
public legacyTrack(category: string, name: string, value: number): void {
let map = this.coverage[category];
if (!map) {
map = this.coverage[category] = {};
}
if (!(name in map)) {
map[name] = 0;
}
map[name] = value;
}
public register(category: string, name: string): void {
let map = this.coverage[category];
if (!map) {
map = this.coverage[category] = {};
}
if (name in map) {
throw new Error(`Name '${name}' already exists in category '${category}' make sure it is unique.`);
}
map[name] = 0;
}
public reset(): void {
for (const categoryMap of Object.values(this.coverage)) {
for (const name of Object.keys(categoryMap)) {
categoryMap[name] = 0;
}
}
}
private async saveCoverage(category: string) {
const categoryMap = this.coverage[category];
await ensureDir(this.coverageDirectory);
const path = join(this.coverageDirectory, `report-${category}.json`);
try {
await fs.promises.writeFile(path, JSON.stringify(categoryMap, null, 2));
} catch (e) {
logger.warn("Error while saving coverage", e);
}
}
}
export const coverageService = new CoverageService();

1
src/services/index.ts Normal file
Просмотреть файл

@ -0,0 +1 @@
export * from "./coverage-service";

Просмотреть файл

@ -0,0 +1,41 @@
import { app, json } from "../../api";
app.category("azure", () => {
// Initial call is 202 with no body and Location and Azure-AsyncOperation
// Configured to follow Location
// Then, should poll Azure-AsyncOperation and see it's done
// Then, should do final GET on the initial Location
// ARM guidance ok, and implemented in VM capture after 2018-04-01
app.post("/lro/LROPostDoubleHeadersFinalLocationGet", "LROPostDoubleHeadersFinalLocationPost", (req) => {
return {
status: 202,
headers: {
"Azure-AsyncOperation": `${req.baseUrl}/lro/LROPostDoubleHeadersFinalLocationGet/asyncOperationUrl`,
"Location": `${req.baseUrl}/lro/LROPostDoubleHeadersFinalLocationGet/location`,
},
};
});
app.get(
"/lro/LROPostDoubleHeadersFinalLocationGet/asyncOperationUrl",
"LROPostDoubleHeadersFinalLocationAsync",
(req) => {
return {
status: 200,
body: json({
status: "succeeded",
}),
};
},
);
app.get("/lro/LROPostDoubleHeadersFinalLocationGet/location", "LROPostDoubleHeadersFinalLocationGet", (req) => {
return {
status: 200,
body: json({
id: "100",
name: "foo",
}),
};
});
});

Просмотреть файл

@ -0,0 +1,24 @@
import { app, json } from "../../api";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
app.category("example" as any, () => {
app.get("/test", "GetMyTest", (req) => {
return {
status: 200,
body: json({
foo: "succeeded",
bar: "wut",
}),
};
});
app.post("/test", "PostMyTest", (req) => {
req.bodyEquals({ foo: "123", bar: "456" });
return {
status: 200,
body: json({
succeeded: true,
}),
};
});
});

142
src/test-routes/string.ts Normal file
Просмотреть файл

@ -0,0 +1,142 @@
import { app, json } from "../api";
app.category("vanilla", () => {
app.get("/string/null", "getStringNull", (req) => {
return {
status: 200,
body: {
contentType: "application/json",
rawContent: undefined,
},
};
});
app.put("/string/null", "putStringNull", (req) => {
req.rawBodyEquals(undefined);
return { status: 200 };
});
app.get("/string/empty", "getStringEmpty", (req) => {
return {
status: 200,
body: {
contentType: "application/json",
rawContent: `""`,
},
};
});
app.put("/string/empty", "putStringEmpty", (req) => {
req.rawBodyEquals(`""`);
return { status: 200 };
});
app.get("/string/notProvided", "getStringNotProvided", (req) => {
return {
status: 200,
};
});
app.get("/string/whitespace", "getStringWithLeadingAndTrailingWhitespace", (req) => {
return {
status: 200,
body: {
contentType: "application/json",
rawContent: '" Now is the time for all good men to come to the aid of their country "',
},
};
});
app.put("/string/whitespace", "putStringWithLeadingAndTrailingWhitespace", (req) => {
req.rawBodyEquals('" Now is the time for all good men to come to the aid of their country "');
return { status: 200 };
});
app.get("/string/base64UrlEncoding", "getStringBase64UrlEncoded", (req) => {
return {
status: 200,
body: {
contentType: "application/json",
rawContent: '"YSBzdHJpbmcgdGhhdCBnZXRzIGVuY29kZWQgd2l0aCBiYXNlNjR1cmw"',
},
};
});
app.put("/string/base64UrlEncoding", "putStringBase64UrlEncoded", (req) => {
req.rawBodyEquals('"YSBzdHJpbmcgdGhhdCBnZXRzIGVuY29kZWQgd2l0aCBiYXNlNjR1cmw"');
return { status: 200 };
});
app.get("/string/base64Encoding", "getStringBase64Encoded", (req) => {
return {
status: 200,
body: {
contentType: "application/json",
rawContent: '"YSBzdHJpbmcgdGhhdCBnZXRzIGVuY29kZWQgd2l0aCBiYXNlNjQ="',
},
};
});
app.get("/string/nullBase64UrlEncoding", "getStringNullBase64UrlEncoding", (req) => {
return { status: 200 };
});
const MULTIBYTE_BUFFER_BODY =
"啊齄丂狛狜隣郎隣兀﨩ˊ〞〡¦℡㈱‐ー﹡﹢﹫、〓ⅰⅹ⒈€㈠㈩ⅠⅫ! ̄ぁんァヶΑ︴АЯаяāɡㄅㄩ─╋︵﹄︻︱︳︴ⅰⅹɑɡ〇〾⿻⺁䜣€";
app.get("/string/mbcs", "getStringMultiByteCharacters", (req) => {
return {
status: 200,
body: {
contentType: "application/json",
rawContent: `"${MULTIBYTE_BUFFER_BODY}"`,
},
};
});
app.put("/string/mbcs", "putStringMultiByteCharacters", (req) => {
req.bodyEquals(MULTIBYTE_BUFFER_BODY);
return { status: 200 };
});
app.get("/string/enum/notExpandable", "getEnumNotExpandable", (req) => {
return {
status: 200,
body: {
contentType: "application/json",
rawContent: '"red color"',
},
};
});
app.put("/string/enum/notExpandable", "putEnumNotExpandable", (req) => {
req.rawBodyEquals('"red color"');
return { status: 200 };
});
app.get("/string/enum/Referenced", "getEnumReferenced", (req) => {
return {
status: 200,
body: {
contentType: "application/json",
rawContent: '"red color"',
},
};
});
app.put("/string/enum/Referenced", "putEnumReferenced", (req) => {
req.rawBodyEquals('"red color"');
return { status: 200 };
});
app.get("/string/enum/ReferencedConstant", "getEnumReferencedConstant", (req) => {
return {
status: 200,
body: json({ field1: "Sample String" }),
};
});
app.put("/string/enum/ReferencedConstant", "putEnumReferencedConstant", (req) => {
req.bodyEquals({ ColorConstant: "green-color" });
return { status: 200 };
});
});

Просмотреть файл

@ -0,0 +1,17 @@
import { cleanupBody } from "./body-utils";
describe("BodyUtils", () => {
describe("cleanupBody()", () => {
it("remove trailing whitespaces", () => {
expect(cleanupBody(" foo ")).toEqual("foo");
});
it("remove trailing new lines", () => {
expect(cleanupBody("\nfoo\nbar\n")).toEqual("foo\nbar");
});
it("replace windows line endings", () => {
expect(cleanupBody("foo\r\nbar")).toEqual("foo\nbar");
});
});
});

7
src/utils/body-utils.ts Normal file
Просмотреть файл

@ -0,0 +1,7 @@
/**
* Cleanup the raw content:
* - trim whitespaces
* - replace \r\n with \n
* @param rawContent: raw content to clean.
*/
export const cleanupBody = (rawContent: string): string => rawContent.trim().replace(/\r?\n|\r/g, "\n");

21
src/utils/file-utils.ts Normal file
Просмотреть файл

@ -0,0 +1,21 @@
import fs from "fs";
import glob from "glob";
export const findFilesFromPattern = async (pattern: string): Promise<string[]> => {
return new Promise((resolve, reject) => {
glob(pattern, (err, matches) => {
if (err) {
reject(err);
}
resolve(matches);
});
});
};
/**
* Ensure the given dir exists.
* @param path Path to the dir.
*/
export const ensureDir = async (path: string): Promise<void> => {
await fs.promises.mkdir(path, { recursive: true });
};

3
src/utils/index.ts Normal file
Просмотреть файл

@ -0,0 +1,3 @@
export * from "./body-utils";
export * from "./file-utils";
export * from "./request-utils";

Просмотреть файл

@ -0,0 +1,3 @@
import { Request } from "express";
export const getRequestBaseUrl = (request: Request): string => `${request.protocol}://${request.get("host")}`;

3
test/setup-jest.ts Normal file
Просмотреть файл

@ -0,0 +1,3 @@
import { logger } from "../src/logger";
logger.silent = true;

4
tsconfig.build.json Normal file
Просмотреть файл

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["**/*.test.*"]
}

71
tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,71 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es2018" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
"lib": ["ESNext"] /* Specify library files to be included in the compilation. */,
// "allowJs": true /* Allow javascript files to be compiled. */,
// "checkJs": true /* Report errors in .js files. */,
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"declaration": true /* Generates corresponding '.d.ts' file. */,
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true /* Generates corresponding '.map' file. */,
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./dist" /* Redirect output structure to the directory. */,
// "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true /* Skip type checking of declaration files. */,
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
},
"include": ["src/**/*.ts", "src/**/*.js", "definitions/**/*.d.ts"]
}