lib/apiScenario/postmanCollectionRunnerClient.ts (972 lines of code) (raw):

import * as path from "path"; import { Collection, Item, ItemDefinition, ItemGroup, Request, RequestAuth, RequestBody, RequestBodyDefinition, Url, Variable, VariableScope, } from "postman-collection"; import { urlParse } from "@azure-tools/openapi-tools-common"; import { isArray } from "lodash"; import { xmsLongRunningOperation, xmsLongRunningOperationOptions, xmsLongRunningOperationOptionsField, xmsSkipUrlEncoding, } from "../util/constants"; import { JsonLoader } from "../swagger/jsonLoader"; import { usePseudoRandom } from "../util/utils"; import { ApiScenarioClientRequest, ApiScenarioRunnerClient, ArmDeployment, Scope, } from "./apiScenarioRunner"; import { ArmTemplate, Authentication, DelayItemMetadata, FinalGetItemMetadata, LroItemMetadata, PollerItemMetadata, Scenario, ScenarioDefinition, SimpleItemMetadata, StepArmTemplate, StepResponseAssertion, StepRestCall, } from "./apiScenarioTypes"; import { DEFAULT_ARM_API_VERSION } from "./constants"; import * as PostmanHelper from "./postmanHelper"; import { VariableEnv } from "./variableEnv"; import { CallType, postmanArmRules } from "./postmanAssertionRules"; export interface PostmanCollectionRunnerClientOption { scenarioFile: string; collectionName?: string; runId: string; testProxy?: string; testProxyAssets?: string; verbose?: boolean; skipAuth?: boolean; skipArmCall?: boolean; skipLroPoll?: boolean; jsonLoader: JsonLoader; } interface PostmanAADTokenAuthOption { type: "AADToken"; tokenName: string; } interface PostmanAzureKeyAuthOption { type: "AzureKey"; keyName: string; } type PostmanAuthOption = PostmanAADTokenAuthOption | PostmanAzureKeyAuthOption; function generatePostmanAssertion(parameter: { step: StepRestCall | StepArmTemplate; item: Item; type: CallType; opts?: any; }) { const scripts: string[] = []; if (parameter.step.type === "armTemplateDeployment") { return; } const rules = postmanArmRules; for (const rule of rules) { const conditions = rule.conditions; const httpMethods = isArray(conditions.httpMethods) ? conditions.httpMethods : [conditions.httpMethods]; const openapiTypes = isArray(conditions.openapiTypes) ? conditions.openapiTypes : [conditions.openapiTypes]; const callTypes = isArray(conditions.callTypes) ? conditions.callTypes : [conditions.callTypes]; if ( !!(parameter.step as StepRestCall).isManagementPlane === openapiTypes.includes("Management") && httpMethods.includes(parameter.step.operation?._method as any) && callTypes.includes(parameter.type) && (conditions.isAsync === undefined || !!parameter.step.operation?.[xmsLongRunningOperation] === conditions.isAsync) && (!conditions.match || conditions.match(parameter)) ) { if (rule.assertion?.stepAssertion) { PostmanHelper.appendScripts(scripts, { name: rule.name!, types: ["ResponseDataAssertion", "StatusCodeAssertion"], responseAssertion: rule.assertion.stepAssertion, }); } if (rule.assertion?.postmanTestScript) { scripts.push(`pm.test("${rule.name}", function() {`); rule.assertion.postmanTestScript.forEach((s) => scripts.push(s)); scripts.push("});"); } } } if (scripts.length) { PostmanHelper.addEvent(parameter.item.events, "test", scripts); } } export class PostmanCollectionRunnerClient implements ApiScenarioRunnerClient { private opts: PostmanCollectionRunnerClientOption; private collection: Collection; private prepareStepsFolder: ItemGroup<Item>; private cleanUpStepsFolder: ItemGroup<Item>; private scenarioFolder: ItemGroup<Item>; private runtimeEnv: VariableScope; private authOptionMap = new Map<string, PostmanAuthOption>(); // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility constructor(opts: PostmanCollectionRunnerClientOption) { this.opts = opts; } private checkAuthOption( authentication: Authentication, preScripts: string[], env: VariableEnv ): RequestAuth | undefined { if (authentication.type === "AADToken") { authentication.scope = env.resolveString(authentication.scope); let option: PostmanAADTokenAuthOption; if (!this.authOptionMap.has(authentication.scope)) { option = { type: authentication.type, tokenName: `x_bearer_token_${this.authOptionMap.size}`, }; this.authOptionMap.set(authentication.scope, option); preScripts.push(PostmanHelper.generateAuthScript(authentication.scope, option.tokenName)); } else { option = this.authOptionMap.get(authentication.scope)! as PostmanAADTokenAuthOption; } return new RequestAuth({ type: "bearer", bearer: [ { key: "token", value: `{{${option.tokenName}}}`, type: "string", }, ], }); } else if (authentication.type === "AzureKey") { return new RequestAuth({ type: "apikey", apikey: [ { key: "in", value: authentication.in, type: "string", }, { key: "key", value: authentication.name, type: "string", }, { key: "value", value: covertToPostmanVariable(authentication.key), type: "string", }, ], }); } return undefined; } public async provisionScope(scenarioDef: ScenarioDefinition, scope: Scope): Promise<void> { this.collection = new Collection({ info: { id: this.opts.runId, name: this.opts.collectionName, }, }); // TODO: figure out what's this for this.collection.describe( JSON.stringify({ apiScenarioFilePath: scenarioDef._filePath, // apiScenarioName: scenario.scenario, swaggerFilePaths: scenarioDef._swaggerFilePaths, }) ); const preScripts: string[] = []; if (scenarioDef.authentication) { this.collection.auth = this.checkAuthOption( scenarioDef.authentication, preScripts, scope.env ); } if (preScripts.length > 0) { PostmanHelper.addEvent(this.collection.events, "prerequest", preScripts); } scope.env.resolve(); this.runtimeEnv = new VariableScope({}); this.runtimeEnv.set("tenantId", scope.env.get("tenantId")?.value, "string"); this.runtimeEnv.set("client_id", scope.env.get("client_id")?.value, "string"); this.runtimeEnv.set("client_secret", scope.env.get("client_secret")?.value, "string"); this.runtimeEnv.set("armEndpoint", scope.env.get("armEndpoint")?.value, "string"); this.runtimeEnv.set("subscriptionId", scope.env.get("subscriptionId")?.value, "string"); this.runtimeEnv.set("resourceGroupName", scope.env.get("resourceGroupName")?.value, "string"); this.runtimeEnv.set("location", scope.env.get("location")?.value, "string"); for (const [name, variable] of scope.env.getVariables()) { if (!this.runtimeEnv.has(name) && !this.collection.variables.has(name)) { if (variable.type === "secureString" || variable.type === "secureObject") { this.runtimeEnv.set(name, variable.value, "secret"); this.collection.variables.add(new Variable({ key: name, type: "secret" })); } else { this.collection.variables.add( new Variable({ key: name, value: variable.value, }) ); } } } PostmanHelper.reservedCollectionVariables.forEach((variable) => { if (!this.collection.variables.has(variable.key)) { if (this.opts.skipAuth && variable.key === "x_enable_auth") { this.collection.variables.add( new Variable({ key: variable.key, value: "false", }) ); } else { this.collection.variables.add(new Variable(variable)); } } }); if (this.opts.testProxy) { this.startTestProxyRecording(); } } public async prepareScenario(scenario: Scenario, env: VariableEnv): Promise<void> { this.scenarioFolder = PostmanHelper.addItemGroup(this.collection, { name: scenario.scenario, description: scenario.description, }); const preScripts: string[] = []; if (scenario.authentication) { this.scenarioFolder.auth = this.checkAuthOption(scenario.authentication, preScripts, env); } env.resolve(); if (Object.keys(scenario.variables).length > 0) { Object.entries(scenario.variables).forEach(([key, value]) => { if (value.value) { preScripts.push( `pm.variables.set("${key}", ${JSON.stringify(env.resolveObjectValues(value.value))});` ); } }); } if (preScripts.length > 0) { PostmanHelper.addEvent(this.scenarioFolder.events, "prerequest", preScripts); } // TODO output variables } public outputCollection(): [Collection, VariableScope] { if (this.opts.testProxy) { this.stopTestProxyRecording(); } return [this.collection, this.runtimeEnv]; } private startTestProxyRecording() { const startRecordRequest: any = { "x-recording-file": `${path.dirname(this.opts.scenarioFile)}/recordings/${ this.opts.collectionName }.json`, }; if (this.opts.testProxyAssets) { startRecordRequest["x-recording-assets-file"] = this.opts.testProxyAssets; } const { item } = this.addNewItem("Prepare", { name: "startTestProxyRecording", request: { url: `${this.opts.testProxy}/record/start`, method: "POST", body: { mode: "raw", raw: `${JSON.stringify(startRecordRequest, null, 2)}`, }, }, }); item.request.addHeader({ key: "Content-Type", value: "application/json" }); PostmanHelper.addEvent( item.events, "test", ` pm.test("Started TestProxy recording", function() { pm.response.to.be.success; pm.response.to.have.header("x-recording-id"); pm.environment.set("x_recording_id", pm.response.headers.get("x-recording-id")); });` ); } private stopTestProxyRecording() { const { item } = this.addNewItem("CleanUp", { name: "stopTestProxyRecording", request: { url: `${this.opts.testProxy}/record/stop`, method: "POST", body: { mode: "raw", raw: `${JSON.stringify( { TIMESTAMP: "{{$isoTimestamp}}", scenarioFile: this.opts.scenarioFile, armEndpoint: this.runtimeEnv.get("armEndpoint"), subscriptionId: this.runtimeEnv.get("subscriptionId"), resourceGroup: this.runtimeEnv.get("resourceGroupName"), location: this.runtimeEnv.get("location"), runId: this.opts.runId, randomSeed: usePseudoRandom.seed.toString(), }, null, 2 )}`, }, }, }); item.request.addHeader({ key: "Content-Type", value: "application/json" }); item.request.addHeader({ key: "x-recording-id", value: "{{x_recording_id}}", }); PostmanHelper.addEvent( item.events, "test", ` pm.test("Stopped TestProxy recording", function() { pm.response.to.be.success; }); ` ); } private addNewItem( itemType: "Prepare" | "CleanUp" | "Scenario" | "Blank", definition?: ItemDefinition, baseUriForTestProxy?: string ): { item: Item; itemGroup?: ItemGroup<Item> } { const item = PostmanHelper.createItem(definition); let itemGroup: ItemGroup<Item> | undefined; if (this.opts.testProxy && baseUriForTestProxy) { item.request.addHeader({ key: "x-recording-upstream-base-uri", value: baseUriForTestProxy, }); item.request.addHeader({ key: "x-recording-id", value: "{{x_recording_id}}" }); item.request.addHeader({ key: "x-recording-mode", value: "record" }); } switch (itemType) { case "Prepare": if (this.prepareStepsFolder === undefined) { this.prepareStepsFolder = PostmanHelper.addItemGroup(this.collection, { name: PostmanHelper.PREPARE_FOLDER, }); } itemGroup = this.prepareStepsFolder; break; case "CleanUp": if (this.cleanUpStepsFolder === undefined) { this.cleanUpStepsFolder = PostmanHelper.addItemGroup(this.collection, { name: PostmanHelper.CLEANUP_FOLDER, }); } itemGroup = this.cleanUpStepsFolder; break; case "Scenario": if (this.scenarioFolder === undefined) { throw new Error("Scenario folder is not initialized"); } itemGroup = this.scenarioFolder; break; case "Blank": break; } if (itemGroup) { itemGroup.items.add(item); } return { item, itemGroup, }; } public async createResourceGroup( armEndpoint: string, subscriptionId: string, resourceGroupName: string, location: string ): Promise<void> { if (this.opts.skipArmCall) return; const { item } = this.addNewItem( "Prepare", { name: "createResourceGroup", }, armEndpoint ); item.request.method = "PUT"; item.request.url = new Url({ host: this.opts.testProxy ?? armEndpoint, path: "/subscriptions/:subscriptionId/resourcegroups/:resourceGroupName", variable: [ { key: "subscriptionId", value: "{{subscriptionId}}", }, { key: "resourceGroupName", value: "{{resourceGroupName}}", }, ], query: [ { key: "api-version", value: DEFAULT_ARM_API_VERSION, }, ], }); item.request.body = new RequestBody({ mode: "raw", raw: '{"location":"{{location}}"}', }); item.request.addHeader({ key: "Content-Type", value: "application/json" }); const postScripts = this.generatePostScripts(); if (postScripts.length > 0) { PostmanHelper.addEvent(item.events, "test", postScripts); } this.runtimeEnv.set("subscriptionId", subscriptionId, "string"); this.runtimeEnv.set("resourceGroupName", resourceGroupName, "string"); this.runtimeEnv.set("location", location, "string"); } public async deleteResourceGroup( armEndpoint: string, _subscriptionId: string, _resourceGroupName: string ): Promise<void> { if (this.opts.skipArmCall) return; const { item } = this.addNewItem( "CleanUp", { name: "deleteResourceGroup", }, armEndpoint ); item.request.method = "DELETE"; item.request.url = new Url({ host: this.opts.testProxy ?? armEndpoint, path: "/subscriptions/:subscriptionId/resourcegroups/:resourceGroupName", variable: [ { key: "subscriptionId", value: "{{subscriptionId}}", }, { key: "resourceGroupName", value: "{{resourceGroupName}}", }, ], query: [ { key: "api-version", value: DEFAULT_ARM_API_VERSION, }, ], }); item.request.addHeader({ key: "Content-Type", value: "application/json" }); const postScripts: string[] = []; PostmanHelper.appendScripts(postScripts, { name: "response code should be 2xx", types: ["StatusCodeAssertion"], }); // Do not poll deleteResourceGroup // this.lroPoll(this.cleanUpStepsFolder, item, armEndpoint, postScripts); if (postScripts.length > 0) { PostmanHelper.addEvent(item.events, "test", postScripts); } } private resolveFilePath(filePath: string): string { const url = urlParse(filePath); if (url) { throw new Error(`File path should be a local file path but got ${filePath}`); } return path.resolve(this.opts.scenarioFile, "..", filePath); } public async sendRestCallRequest( clientRequest: ApiScenarioClientRequest, step: StepRestCall, env: VariableEnv ): Promise<void> { env.resolve(); const baseUri = convertPostmanFormat(env.tryResolveString(clientRequest.host)); const { item, itemGroup } = this.addNewItem( step.isPrepareStep ? "Prepare" : step.isCleanUpStep ? "CleanUp" : "Scenario", { name: step.step, request: { method: clientRequest.method, url: clientRequest.path, body: clientRequest.body ? { mode: "raw", raw: JSON.stringify(convertPostmanFormat(clientRequest.body), null, 2), } : clientRequest.formData ? { mode: "formdata", formdata: Object.entries(clientRequest.formData).map(([key, value]) => { if (value.type === "file") { return { key, type: "file", src: this.resolveFilePath(value.value), }; } else { return { key, type: value.type, value: value.value, }; } }), } : clientRequest.file ? { mode: "file", file: { src: this.resolveFilePath(clientRequest.file) }, } : undefined, }, }, baseUri ); // pre scripts const preScripts: string[] = []; if (step.authentication) { item.request.auth = this.checkAuthOption(step.authentication, preScripts, env); } item.description = step.operationId; item.request.url = new Url({ host: this.opts.testProxy ?? baseUri, path: covertToPostmanVariable(clientRequest.path, true), variable: Object.entries(clientRequest.pathParameters ?? {}).map(([key, value]) => ({ key, value: convertPostmanFormat(value), })), query: Object.entries(clientRequest.query).map(([key, value]) => ({ key, value: convertPostmanFormat(value)?.toString(), })), }); if (clientRequest.body) { item.request.addHeader({ key: "Content-Type", value: step.operation?.consumes?.[0] ?? step.operation?._path?._spec?.consumes?.[0] ?? "application/json", }); } Object.entries(clientRequest.headers).forEach(([key, value]) => { item.request.addHeader({ key, value: convertPostmanFormat(value) }); }); step._resolvedParameters = env.resolveObjectValues(step.parameters); if (Object.keys(step.variables).length > 0) { Object.entries(step.variables).forEach(([key, value]) => preScripts.push( `pm.variables.set("${key}", ${JSON.stringify(env.resolveObjectValues(value.value))});` ) ); } const replaceKey = new Set<string>(); const jsonLoader = this.opts.jsonLoader; const encodeVariable = function (variable: { key?: string | null; value: string | null }) { let skipEncode = false; step.operation?.parameters?.forEach((p) => { p = jsonLoader.resolveRefObj(p); if (p.name === variable.key && p.in === "path" && p[xmsSkipUrlEncoding]) { skipEncode = true; } }); if (skipEncode) { return; } const regex = /\{\{([A-Za-z_$][A-Za-z0-9_]*)\}\}/g; const replaceArray: Array<[number, number, string]> = []; let match, index = variable.value!.length; while ((match = regex.exec(variable.value!))) { replaceKey.add(match[1]); replaceArray.push([match.index, match.index + match[0].length, `{{${match[1]}_encoded}}`]); index = match.index + match[0].length; } replaceArray.push([index, variable.value!.length, ""]); let r, value = ""; index = 0; while ((r = replaceArray.shift())) { value += encodeURIComponent(variable.value!.substring(index, r[0])) + r[2]; index = r[1]; } variable.value = value; }; item.request.url.variables.each(encodeVariable); item.request.url.query.each(encodeVariable); replaceKey.forEach((key) => { preScripts.push( `pm.variables.set("${key}_encoded", encodeURIComponent(pm.variables.get("${key}")));` ); }); if (preScripts.length > 0) { PostmanHelper.addEvent(item.events, "prerequest", preScripts); } // post scripts const postScripts: string[] = []; const getOverwriteVariables = () => { if (step.outputVariables !== undefined && Object.keys(step.outputVariables).length > 0) { const ret = new Map<string, string>(); for (const k of Object.keys(step.outputVariables)) { ret.set(k, step.outputVariables[k].fromResponse); } return ret; } return undefined; }; for (const outputName of Object.keys(step.outputVariables ?? {})) { env.output(outputName, { type: "string", value: `{{${outputName}}}`, }); } const scriptTypes: PostmanHelper.TestScriptType[] = ["StatusCodeAssertion"]; if (step.responseAssertion) { step.responseAssertion = env.resolveObjectValues(step.responseAssertion); scriptTypes.push("ResponseDataAssertion"); } this.generatePostScripts( scriptTypes, getOverwriteVariables(), undefined, step.responseAssertion ).forEach((s) => postScripts.push(s)); if (step.operation && step.operation[xmsLongRunningOperation]) { const metadata: LroItemMetadata = { type: "LRO", poller_item_name: `_${item.name}_poller`, operationId: step.operation.operationId || "", exampleName: step.exampleFile!, itemName: item.name, step: item.name, }; item.description = JSON.stringify(metadata); this.lroPoll(itemGroup!, item, step, clientRequest.host, postScripts); // generate final get if (step.operation?._method !== "post") { itemGroup!.items.add( this.generateFinalGetItem( item.name, baseUri, step, step.operation._method, undefined, undefined ) ); } } else { const metadata: SimpleItemMetadata = { type: "simple", operationId: step.operation?.operationId || "", exampleName: step.exampleFile!, itemName: item.name, step: item.name, }; item.description = JSON.stringify(metadata); } if (postScripts.length > 0) { PostmanHelper.addEvent(item.events, "test", postScripts); } generatePostmanAssertion({ step, type: "stepCall", item, opts: this.opts }); } private lroPoll( itemGroup: ItemGroup<Item>, item: Item, step: StepRestCall | StepArmTemplate, baseUri: string, postScripts: string[] ) { const finalStateVia = step.type === "restCall" ? step?.operation?.[xmsLongRunningOperationOptions]?.[xmsLongRunningOperationOptionsField] : undefined; const isManagementPlane = step.type === "armTemplateDeployment" || step.isManagementPlane; if (this.opts.skipLroPoll) return; const url = item.request.url; const urlStr = `${baseUri}${url.getPathWithQuery()}`; // For ARM put or patch operations , by default the final get is original url. const isArmResourceCreate = isManagementPlane && ["put", "patch"].includes(item.request.method.toLowerCase()); postScripts.push( ` // RPC-Async-V1-06 x-ms-long-running-operation-options should indicate the type of response header to track the async operation. function getLroFinalGetUrl(finalStateVia) { if (!finalStateVia) { // by default, the final url header is Location for ARM, Operation-Location for dataplane. return ${ isArmResourceCreate ? `'${urlStr}'` : `pm.response.headers.get("Location") || pm.response.headers.get("Operation-Location")` } } switch (finalStateVia) { case "location": { return pm.response.headers.get("Location"); } case "azure-async-operation": { return pm.response.headers.get("Azure-AsyncOperation"); } case "original-uri": { return "${urlStr}"; } case "operation-location": { return pm.response.headers.get("Operation-Location"); } default: return ""; } } function getProxyUrl(url) { return "${this.opts.testProxy ?? ""}" ? url.replace("${baseUri}","${ this.opts.testProxy ?? "" }") : url } const pollingUrl = pm.response.headers.get("Azure-AsyncOperation") || pm.response.headers.get("Location") || pm.response.headers.get("Operation-Location") || ${ isArmResourceCreate ? `'${urlStr}'` : "''" } if (pollingUrl) { pm.variables.set("x_polling_url", getProxyUrl(pollingUrl)); pm.variables.set("x_final_get_url", getProxyUrl(getLroFinalGetUrl("${finalStateVia ?? ""}"))) pm.variables.set("x_retry_after", "3"); }` ); const { item: delayItem } = this.addNewItem( "Blank", { name: `_${item.name}_delay`, request: { url: "https://postman-echo.com/delay/{{x_retry_after}}", method: "GET", }, }, baseUri ); const delayItemMetadata: DelayItemMetadata = { type: "delay", lro_item_name: item.name, }; delayItem.description = JSON.stringify(delayItemMetadata); itemGroup.items.add(delayItem); const { item: pollerItem } = this.addNewItem( "Blank", { name: `_${item.name}_poller`, request: { url: `{{x_polling_url}}`, method: "GET", }, }, baseUri ); const pollerItemMetadata: PollerItemMetadata = { type: "poller", lro_item_name: item.name, }; pollerItem.description = JSON.stringify(pollerItemMetadata); const pollerPostScripts: string[] = []; pollerPostScripts.push( ` try { if (pm.response.code === 202) { postman.setNextRequest("${delayItem.name}"); if (pm.response.headers.has("Retry-After")) { pm.variables.set("x_retry_after", pm.response.headers.get("Retry-After")); } } else if (pm.response.size().body > 0) { const terminalStatus = ["succeeded", "failed", "canceled", "cancelled", "aborted", "deleted", "completed"]; const json = pm.response.json(); if (json.status !== undefined && terminalStatus.indexOf(json.status.toLowerCase()) === -1) { postman.setNextRequest("${delayItem.name}") if (pm.response.headers.has("Retry-After")) { pm.variables.set("x_retry_after", pm.response.headers.get("Retry-After")); } } } } catch (err) { console.error(err); } ` ); if (postScripts.length > 0) { PostmanHelper.addEvent(pollerItem.events, "test", pollerPostScripts); } generatePostmanAssertion({ step, type: "lroPolling", item: pollerItem, opts: this.opts }); itemGroup.items.add(pollerItem); } private generatePostScripts( types: PostmanHelper.TestScriptType[] = ["StatusCodeAssertion"], overwriteVariables?: Map<string, string>, armTemplate?: ArmTemplate, responseAssertion?: StepResponseAssertion, name?: string ): string[] { const scripts: string[] = []; if (overwriteVariables !== undefined) { types.push("OverwriteVariables"); } // TODO For post request do not output response log. // if (item.request.method === "POST") { // types = types.filter((it) => it !== "DetailResponseLog"); // } if (types.length > 0) { // generate assertion from example PostmanHelper.appendScripts(scripts, { name: name || "response status code assertion.", types: types, variables: overwriteVariables, armTemplate, responseAssertion, }); } return scripts; } public async sendArmTemplateDeployment( armEndpoint: string, armTemplate: ArmTemplate, _armDeployment: ArmDeployment, step: StepArmTemplate, env: VariableEnv ): Promise<void> { if (this.opts.skipArmCall) return; const { item, itemGroup } = this.addNewItem( step.isPrepareStep ? "Prepare" : step.isCleanUpStep ? "CleanUp" : "Scenario", { name: step.step, }, armEndpoint ); item.request = new Request({ name: step.step, method: "PUT", url: "", body: { mode: "raw" } as RequestBodyDefinition, }); item.request.url = new Url({ host: this.opts.testProxy ?? armEndpoint, path: `/subscriptions/:subscriptionId/resourcegroups/:resourceGroupName/providers/Microsoft.Resources/deployments/:deploymentName`, variable: [ { key: "subscriptionId", value: `{{subscriptionId}}` }, { key: "resourceGroupName", value: `{{resourceGroupName}}` }, { key: "deploymentName", value: `${step.step}` }, ], query: [{ key: "api-version", value: DEFAULT_ARM_API_VERSION }], }); const body = { properties: { mode: "Incremental", template: convertPostmanFormat(env.resolveObjectValues(armTemplate)), }, }; for (const outputName of Object.keys(step.armTemplatePayload.outputs || {})) { env.output(outputName, { type: "string", value: `{{${outputName}}}`, }); } item.request.body = new RequestBody({ mode: "raw", raw: JSON.stringify(body, null, 2), }); item.request.addHeader({ key: "Content-Type", value: "application/json" }); const scriptTypes: PostmanHelper.TestScriptType[] = ["StatusCodeAssertion"]; const postScripts: string[] = []; PostmanHelper.appendScripts(postScripts, { name: "response status code assertion.", types: scriptTypes, variables: undefined, }); this.lroPoll(itemGroup!, item, step, armEndpoint, postScripts); if (postScripts.length > 0) { // to be improved PostmanHelper.addEvent(item.events, "test", postScripts); } generatePostmanAssertion({ step, type: "armTemplateCall", item: item, opts: this.opts }); const generatedGetScriptTypes: PostmanHelper.TestScriptType[] = ["ExtractARMTemplateOutput"]; const generatedGetOperationItem = this.generateFinalGetItem( item.name, armEndpoint, step, "put", generatedGetScriptTypes, armTemplate ); itemGroup!.items.add(generatedGetOperationItem); } private generateFinalGetItem( name: string, baseUri: string, step: StepRestCall | StepArmTemplate, prevMethod: string = "put", scriptTypes: PostmanHelper.TestScriptType[] = [], armTemplate?: ArmTemplate, finalStateVia?: string ): Item { const { item } = this.addNewItem( "Blank", { name: `_${name}_final_get`, request: { method: "GET", url: `{{x_final_get_url}}`, }, }, baseUri ); const metadata: FinalGetItemMetadata = { type: "finalGet", lro_item_name: name, step: step.step, }; item.description = JSON.stringify(metadata); item.request.addHeader({ key: "Content-Type", value: "application/json" }); if (prevMethod !== "delete") { scriptTypes.push("StatusCodeAssertion"); } const postScripts = this.generatePostScripts(scriptTypes, undefined, armTemplate, undefined); if (postScripts.length > 0) { // to be improved PostmanHelper.addEvent(item.events, "test", postScripts); } if (finalStateVia && finalStateVia !== "original-uri") { PostmanHelper.addEvent(item.events, "prerequest", [ `pm.test("LRO final-state-via is valid", () => { pm.expect(pm.variables.get("x_final_get_url")).to.be.not.undefined; })`, ]); } generatePostmanAssertion({ step, type: "lroFinalGet", item: item, opts: this.opts }); return item; } } const convertPostmanFormat = <T>(obj: T): T => { if (typeof obj === "string") { return covertToPostmanVariable(obj) as unknown as T; } if (typeof obj !== "object") { return obj; } if (obj === null || obj === undefined) { return obj; } if (Array.isArray(obj)) { return (obj as any[]).map((v) => convertPostmanFormat(v)) as unknown as T; } const result: any = {}; for (const key of Object.keys(obj)) { const newKey = covertToPostmanVariable(key); result[newKey] = convertPostmanFormat((obj as any)[key]); } return result; }; const covertToPostmanVariable = (value: string, isPath: boolean = false): string => { return value.replace(/\$\(([a-z0-9-_$]+)\)/gi, (_, p1) => (isPath ? `:${p1}` : `{{${p1}}}`)); };