From 6ad560d7810a657a58b199e21ccaa0d48f4c9c1d Mon Sep 17 00:00:00 2001 From: changlong-liu <59815250+changlong-liu@users.noreply.github.com> Date: Tue, 23 Nov 2021 10:22:12 +0800 Subject: [PATCH] mock-service-host: use discriminatorMaps (#2311) * use discriminatorMaps * fix boolean response * fix arrary response * use snapshot --- tools/mock-service-host/package.json | 3 +- .../mock-service-host/src/mid/coordinator.ts | 40 ++++++++++------- .../src/mid/oav/swaggerMocker.ts | 7 ++- tools/mock-service-host/src/mid/responser.ts | 14 ++++++ .../testData/payloads/valid_input_delete.json | 3 +- .../payloads/valid_input_delete.json.new | 23 ++++++++++ tools/mock-service-host/test/tools.ts | 16 ------- .../__snapshots__/testcoordinator.ts.snap | 45 +++++++++++++++++++ .../test/unittest/testcoordinator.ts | 9 ++-- 9 files changed, 118 insertions(+), 42 deletions(-) create mode 100644 tools/mock-service-host/test/testData/payloads/valid_input_delete.json.new create mode 100644 tools/mock-service-host/test/unittest/__snapshots__/testcoordinator.ts.snap diff --git a/tools/mock-service-host/package.json b/tools/mock-service-host/package.json index eb5d0203084..450cbf7b4d4 100644 --- a/tools/mock-service-host/package.json +++ b/tools/mock-service-host/package.json @@ -1,6 +1,6 @@ { "name": "@azure-tools/mock-service-host", - "version": "0.1.4", + "version": "0.1.5", "description": "Azure Mock Service Host", "main": "index.js", "scripts": { @@ -9,6 +9,7 @@ "build": "tsc && npm run copyfiles", "copyfiles": "copyfiles -a \"./test/testData/**/*.json\" ./dist", "unit-test": "npm run build && cross-env NODE_ENV=test jest --ci --reporters=default --reporters=jest-junit --config ./jest.unittest.config.js", + "unittest-update": "npm run build && cross-env NODE_ENV=test jest --ci --reporters=default --reporters=jest-junit --updateSnapshot --config ./jest.unittest.config.js", "test": "npm run build && cross-env NODE_ENV=test jest --ci --reporters=default --reporters=jest-junit --runInBand", "eslint-fix": "eslint . --ext .ts --ignore-pattern node_modules/ --ignore-pattern dist/ --fix", "eslint": "eslint . --ext .ts --ignore-pattern node_modules/ --ignore-pattern dist/ --quiet", diff --git a/tools/mock-service-host/src/mid/coordinator.ts b/tools/mock-service-host/src/mid/coordinator.ts index 9fa2d26d3d1..3ef7cbdf4c9 100644 --- a/tools/mock-service-host/src/mid/coordinator.ts +++ b/tools/mock-service-host/src/mid/coordinator.ts @@ -276,25 +276,33 @@ export class Coordinator { const [code, _ret] = this.findResponse(exampleResponses, HttpStatusCode.OK) let ret = _ret - // simplified paging - ret = lodash.omit(ret, 'nextLink') + if (typeof ret === 'object') { + if (!Array.isArray(ret)) { + // simplified paging + ret = lodash.omit(ret, 'nextLink') + } - // simplified LRO - ret = replacePropertyValue('provisioningState', 'Succeeded', ret) + // simplified LRO + ret = replacePropertyValue('provisioningState', 'Succeeded', ret) - if (code !== HttpStatusCode.OK && code !== HttpStatusCode.NO_CONTENT && code < 300) { - res.setHeader('Azure-AsyncOperation', await this.findLROGet(req)) - res.setHeader('Retry-After', 1) - } - if (req.query?.[LRO_CALLBACK] === 'true') { - ret.status = 'Succeeded' - } + if ( + code !== HttpStatusCode.OK && + code !== HttpStatusCode.NO_CONTENT && + code < 300 + ) { + res.setHeader('Azure-AsyncOperation', await this.findLROGet(req)) + res.setHeader('Retry-After', 1) + } + if (req.query?.[LRO_CALLBACK] === 'true') { + ret.status = 'Succeeded' + } - //set name - const path = getPath(getPureUrl(req.url)) - ret = replacePropertyValue('name', path[path.length - 1], ret, (v) => { - return typeof v === 'string' && v.match(/^a+$/) !== null - }) + //set name + const path = getPath(getPureUrl(req.url)) + ret = replacePropertyValue('name', path[path.length - 1], ret, (v) => { + return typeof v === 'string' && v.match(/^a+$/) !== null + }) + } res.set(code, ret) } diff --git a/tools/mock-service-host/src/mid/oav/swaggerMocker.ts b/tools/mock-service-host/src/mid/oav/swaggerMocker.ts index 47f1ee35a11..3bc4c5bcd44 100644 --- a/tools/mock-service-host/src/mid/oav/swaggerMocker.ts +++ b/tools/mock-service-host/src/mid/oav/swaggerMocker.ts @@ -80,7 +80,7 @@ export default class SwaggerMocker { } } // get(list) - if (responses[key]?.body?.value?.length) { + if (Array.isArray(responses[key]?.body?.value) && responses[key]?.body?.value?.length) { responses[key]?.body?.value?.forEach((item: any) => { if (item.id) { const resourceName = item.name || 'resourceName' @@ -369,7 +369,10 @@ export default class SwaggerMocker { example = this.mocker.mock(definitionSpec, objName, arrItem) } else { /** type === number or integer */ - example = example ? example : this.mocker.mock(definitionSpec, objName) + example = + example && typeof example !== 'object' + ? example + : this.mocker.mock(definitionSpec, objName) } // return value for primary type: string, number, integer, boolean // "aaaa" diff --git a/tools/mock-service-host/src/mid/responser.ts b/tools/mock-service-host/src/mid/responser.ts index e979ac73d51..39a8e82a39d 100644 --- a/tools/mock-service-host/src/mid/responser.ts +++ b/tools/mock-service-host/src/mid/responser.ts @@ -1,6 +1,7 @@ import * as _ from 'lodash' import * as fs from 'fs' import * as path from 'path' +import { AjvSchemaValidator } from 'oav/dist/lib/swaggerValidator/ajvSchemaValidator' import { Config } from '../common/config' import { ExampleNotFound, ExampleNotMatch } from '../common/errors' import { Headers, ParameterType, SWAGGER_ENCODING, useREF } from '../common/constants' @@ -8,9 +9,13 @@ import { JsonLoader } from 'oav/dist/lib/swagger/jsonLoader' import { LiveRequest } from 'oav/dist/lib/liveValidation/operationValidator' import { MockerCache, PayloadCache } from 'oav/dist/lib/generator/exampleCache' import { Operation, SwaggerExample, SwaggerSpec } from 'oav/dist/lib/swagger/swaggerTypes' +import { TransformContext, getTransformContext } from 'oav/dist/lib/transform/context' +import { applyGlobalTransformers, applySpecTransformers } from 'oav/dist/lib/transform/transformer' +import { discriminatorTransformer } from 'oav/dist/lib/transform/discriminatorTransformer' import { injectable } from 'inversify' import { inversifyGetInstance } from 'oav/dist/lib/inversifyUtils' import { isNullOrUndefined } from '../common/utils' +import { resolveNestedDefinitionTransformer } from 'oav/dist/lib/transform/resolveNestedDefinitionTransformer' import SwaggerMocker from './oav/swaggerMocker' export interface SwaggerExampleParameter { @@ -30,12 +35,18 @@ export class ResponseGenerator { private mockerCache: MockerCache private payloadCache: PayloadCache private swaggerMocker: SwaggerMocker + public readonly transformContext: TransformContext constructor() { this.jsonLoader = inversifyGetInstance(JsonLoader, {}) this.mockerCache = new MockerCache() this.payloadCache = new PayloadCache() this.swaggerMocker = new SwaggerMocker(this.jsonLoader, this.mockerCache, this.payloadCache) + const schemaValidator = new AjvSchemaValidator(this.jsonLoader) + this.transformContext = getTransformContext(this.jsonLoader, schemaValidator, [ + resolveNestedDefinitionTransformer, + discriminatorTransformer + ]) } private getSpecItem(spec: any, operationId: string): SpecItem | undefined { @@ -147,6 +158,9 @@ export class ResponseGenerator { public async generate(operation: Operation, config: Config, liveRequest: LiveRequest) { const specFile = this.getSpecFileByOperation(operation, config) const spec = (await (this.jsonLoader.load(specFile) as unknown)) as SwaggerSpec + applySpecTransformers(spec, this.transformContext) + applyGlobalTransformers(this.transformContext) + const specItem = this.getSpecItem(spec, operation.operationId as string) if (!specItem) { throw Error(`operation ${operation.operationId} can't be found in ${specFile}`) diff --git a/tools/mock-service-host/test/testData/payloads/valid_input_delete.json b/tools/mock-service-host/test/testData/payloads/valid_input_delete.json index 2491057df3f..7dd867ccf42 100644 --- a/tools/mock-service-host/test/testData/payloads/valid_input_delete.json +++ b/tools/mock-service-host/test/testData/payloads/valid_input_delete.json @@ -18,7 +18,6 @@ }, "liveResponse": { "statusCode": "200", - "headers": {}, - "body": {} + "headers": {} } } \ No newline at end of file diff --git a/tools/mock-service-host/test/testData/payloads/valid_input_delete.json.new b/tools/mock-service-host/test/testData/payloads/valid_input_delete.json.new new file mode 100644 index 00000000000..7dd867ccf42 --- /dev/null +++ b/tools/mock-service-host/test/testData/payloads/valid_input_delete.json.new @@ -0,0 +1,23 @@ +{ + "liveRequest": { + "headers": { + "strict-Transport-Security": "max-age=31536000; includeSubDomains", + "x-ms-request-id": "8e3485b6-c8a7-45c2-a9f5-59b826e42880", + "x-ms-correlation-request-id": "8e3485b6-c8a7-45c2-a9f5-59b826e42880", + "date": "Tue, 11 Sep 2018 18:45:41 GMT", + "eTag": "\"AAAAAAAAjAIAAAAAAACL/A==\"", + "server": "Microsoft-HTTPAPI/2.0", + "Content-Type": "application/json", + "If-Match": "dddd" + }, + "method": "DELETE", + "url": "/subscriptions/randomSub/resourceGroups/randomRG/providers/Microsoft.ApiManagement/service/randomService/users/randomUsers?api-version=2018-01-01", + "query": { + "api-version": "2018-01-01" + } + }, + "liveResponse": { + "statusCode": "200", + "headers": {} + } +} \ No newline at end of file diff --git a/tools/mock-service-host/test/tools.ts b/tools/mock-service-host/test/tools.ts index 0c0fb747895..bbe72f30aac 100644 --- a/tools/mock-service-host/test/tools.ts +++ b/tools/mock-service-host/test/tools.ts @@ -39,22 +39,6 @@ export function mockDefaultResponse(): VirtualServerResponse { return new VirtualServerResponse('500', createErrorBody(500, 'Default Response')) } -export function storeAndCompare( - pair: RequestResponsePair, - response: VirtualServerResponse, - path: string -) { - const expected = lodash.cloneDeep(pair) - pair.liveResponse.statusCode = response.statusCode - pair.liveResponse.body = response.body - pair.liveResponse.headers = response.headers - - const newFile = path + '.new' - fs.writeFileSync(newFile, JSON.stringify(pair, null, 2)) // save new response for trouble shooting - assert.deepStrictEqual(pair, expected) - fs.unlinkSync(newFile) // remove the new file if pass the assert -} - export function createLiveRequestForCreateRG(sub = 'randomSub', rg = 'randomRG'): LiveRequest { return { url: `/subscriptions/${sub}/resourceGroups/${rg}`, diff --git a/tools/mock-service-host/test/unittest/__snapshots__/testcoordinator.ts.snap b/tools/mock-service-host/test/unittest/__snapshots__/testcoordinator.ts.snap new file mode 100644 index 00000000000..524a378f84c --- /dev/null +++ b/tools/mock-service-host/test/unittest/__snapshots__/testcoordinator.ts.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`generateResponse() special rule of GET locations 1`] = ` +VirtualServerResponse { + "body": Object { + "id": "/subscriptions/randomSub/resourceGroups/randomRG", + "location": "eastus", + "managedBy": null, + "name": "randomRG", + "properties": Object { + "provisioningState": "Succeeded", + }, + "tags": Object {}, + "type": "Microsoft.Resources/resourceGroups", + }, + "headers": Object {}, + "statusCode": "200", +} +`; + +exports[`generateResponse() special rule of GET resourceGroup 1`] = ` +VirtualServerResponse { + "body": Object { + "id": "/subscriptions/randomSub/resourceGroups/randomRG", + "location": "eastus", + "managedBy": null, + "name": "randomRG", + "properties": Object { + "provisioningState": "Succeeded", + }, + "tags": Object {}, + "type": "Microsoft.Resources/resourceGroups", + }, + "headers": Object {}, + "statusCode": "200", +} +`; + +exports[`generateResponse() validate DELETE input 1`] = ` +VirtualServerResponse { + "body": undefined, + "headers": Object {}, + "statusCode": "200", +} +`; diff --git a/tools/mock-service-host/test/unittest/testcoordinator.ts b/tools/mock-service-host/test/unittest/testcoordinator.ts index c458e62d948..bd9d86f4a24 100644 --- a/tools/mock-service-host/test/unittest/testcoordinator.ts +++ b/tools/mock-service-host/test/unittest/testcoordinator.ts @@ -25,8 +25,7 @@ import { createLiveRequestForDeleteApiManagementService, genFakeResponses, mockDefaultResponse, - mockRequest, - storeAndCompare + mockRequest } from '../tools' const statefulProfile = { @@ -102,7 +101,7 @@ describe('generateResponse()', () => { const response = mockDefaultResponse() await coordinator.generateResponse(request, response, statelessProfile) assert.strictEqual(response.statusCode, pair.liveResponse.statusCode) - storeAndCompare(pair, response, fileName) + expect(response).toMatchSnapshot() pair.liveResponse.body = response.body pair.liveResponse.headers = response.headers pair.liveResponse.headers['Content-Type'] = 'application/json' @@ -134,7 +133,7 @@ describe('generateResponse()', () => { const response = mockDefaultResponse() await coordinator.generateResponse(request, response, statelessProfile) assert.strictEqual(response.statusCode, pair.liveResponse.statusCode) - storeAndCompare(pair, response, fileName) + expect(response).toMatchSnapshot() }) it('special rule of GET locations', async () => { @@ -150,7 +149,7 @@ describe('generateResponse()', () => { const response = mockDefaultResponse() await coordinator.generateResponse(request, response, statelessProfile) assert.strictEqual(response.statusCode, pair.liveResponse.statusCode) - storeAndCompare(pair, response, fileName) + expect(response).toMatchSnapshot() if (!pair.liveResponse.headers) pair.liveResponse.headers = {} const result = await coordinator.Validator.validateLiveRequestResponse(pair) assert.strictEqual(result.requestValidationResult.isSuccessful, undefined) // since this is a special URI not handled by oav