lib/apiScenario/apiScenarioLoader.ts (834 lines of code) (raw):
/* eslint-disable require-atomic-updates */
import { basename } from "path";
import { cloneDeep, pathDirName, pathJoin } from "@azure-tools/openapi-tools-common";
import { inject, injectable } from "inversify";
import { apply as jsonMergeApply, generate as jsonMergePatchGenerate } from "json-merge-patch";
import { findReadMe } from "@azure/openapi-markdown";
import { inversifyGetInstance, TYPES } from "../inversifyUtils";
import { FileLoader, FileLoaderOption } from "../swagger/fileLoader";
import { JsonLoader, JsonLoaderOption } from "../swagger/jsonLoader";
import { Loader, setDefaultOpts } from "../swagger/loader";
import { SwaggerLoader, SwaggerLoaderOption } from "../swagger/swaggerLoader";
import {
ApiKeySecurity,
BodyParameter,
Operation,
Parameter,
SwaggerExample,
SwaggerSpec,
} from "../swagger/swaggerTypes";
import { SchemaValidator } from "../swaggerValidator/schemaValidator";
import { allOfTransformer } from "../transform/allOfTransformer";
import { getTransformContext, TransformContext } from "../transform/context";
import { discriminatorTransformer } from "../transform/discriminatorTransformer";
import { noAdditionalPropertiesTransformer } from "../transform/noAdditionalPropertiesTransformer";
import { nullableTransformer } from "../transform/nullableTransformer";
import { pureObjectTransformer } from "../transform/pureObjectTransformer";
import { referenceFieldsTransformer } from "../transform/referenceFieldsTransformer";
import { resolveNestedDefinitionTransformer } from "../transform/resolveNestedDefinitionTransformer";
import { applyGlobalTransformers, applySpecTransformers } from "../transform/transformer";
import { traverseSwagger } from "../transform/traverseSwagger";
import { xmsPathsTransformer } from "../transform/xmsPathsTransformer";
import { getInputFiles } from "../util/utils";
import { xmsExamples } from "../util/constants";
import { logger } from "./logger";
import {
ArmDeploymentScriptResource,
ArmTemplate,
Authentication,
RawAuthentication,
RawScenario,
RawScenarioDefinition,
RawStep,
RawStepArmScript,
RawStepArmTemplate,
RawStepExample,
RawStepOperation,
RawStepRoleAssignment,
RawVariableScope,
ReadmeTag,
Scenario,
ScenarioDefinition,
Step,
StepArmTemplate,
StepRestCall,
StepRoleAssignment,
Variable,
VariableScope,
} from "./apiScenarioTypes";
import { ApiScenarioYamlLoader } from "./apiScenarioYamlLoader";
import { BodyTransformer } from "./bodyTransformer";
import { armDeploymentScriptTemplate, DEFAULT_ARM_ENDPOINT } from "./constants";
import { jsonPatchApply } from "./diffUtils";
import { TemplateGenerator } from "./templateGenerator";
const variableRegex = /\$\(([A-Za-z_$][A-Za-z0-9_]*)\)/;
export interface ApiScenarioLoaderOption
extends FileLoaderOption,
JsonLoaderOption,
SwaggerLoaderOption {
includeOperation?: boolean;
}
interface ApiScenarioContext {
stepTracking: Map<string, Step>;
scenarioDef: ScenarioDefinition;
scenario?: Scenario;
scenarioIndex?: number;
stepIndex?: number;
stage?: "prepare" | "scenario" | "cleanUp";
}
@injectable()
export class ApiScenarioLoader implements Loader<ScenarioDefinition> {
private transformContext: TransformContext;
private operationsMap = new Map<string, Operation>();
private apiVersionsMap = new Map<string, string>();
private exampleToOperation = new Map<string, { [operationId: string]: string }>();
private additionalMap = new Map<
string,
{
operationsMap: Map<string, Operation>;
apiVersionsMap: Map<string, string>;
}
>();
private initialized: boolean = false;
public constructor(
@inject(TYPES.opts) private opts: ApiScenarioLoaderOption,
private fileLoader: FileLoader,
public jsonLoader: JsonLoader,
private swaggerLoader: SwaggerLoader,
private apiScenarioYamlLoader: ApiScenarioYamlLoader,
private templateGenerator: TemplateGenerator,
private bodyTransformer: BodyTransformer,
@inject(TYPES.schemaValidator) private schemaValidator: SchemaValidator
) {
setDefaultOpts(opts, {
skipResolveRefKeys: [xmsExamples],
includeOperation: true,
});
this.transformContext = getTransformContext(this.jsonLoader, this.schemaValidator, [
xmsPathsTransformer,
resolveNestedDefinitionTransformer,
referenceFieldsTransformer,
discriminatorTransformer,
allOfTransformer,
noAdditionalPropertiesTransformer,
nullableTransformer,
pureObjectTransformer,
]);
}
public static create(opts: ApiScenarioLoaderOption) {
return inversifyGetInstance(ApiScenarioLoader, opts);
}
private async initialize(swaggerFilePaths: string[], additionalTags?: ReadmeTag[]) {
if (this.initialized) {
throw new Error("Already initialized");
}
await this.loadSwaggers(
swaggerFilePaths,
this.operationsMap,
this.apiVersionsMap,
this.exampleToOperation
);
if (additionalTags) {
for (const e of additionalTags) {
logger.verbose(`Additional readme tag: ${e.filePath}`);
const inputFiles = await getInputFiles(e.filePath, e.tag);
if (inputFiles) {
this.additionalMap.set(e.name, {
operationsMap: new Map<string, Operation>(),
apiVersionsMap: new Map<string, string>(),
});
const additionalSwaggerFiles: string[] = [];
inputFiles.forEach((f) => {
additionalSwaggerFiles.push(pathJoin(pathDirName(e.filePath), f));
});
logger.verbose("Additional input-file:");
logger.verbose(additionalSwaggerFiles);
await this.loadSwaggers(
additionalSwaggerFiles,
this.additionalMap.get(e.name)!.operationsMap,
this.additionalMap.get(e.name)!.apiVersionsMap
);
}
}
}
this.initialized = true;
}
private async loadSwaggers(
swaggerFilePaths: string[],
opsMap: Map<string, Operation>,
verMap: Map<string, string>,
egOpMap?: Map<string, { [operationId: string]: string }>
) {
const allSpecs: SwaggerSpec[] = [];
for (const swaggerFilePath of swaggerFilePaths ?? []) {
const swaggerSpec = await this.swaggerLoader.load(swaggerFilePath);
allSpecs.push(swaggerSpec);
applySpecTransformers(swaggerSpec, this.transformContext);
}
applyGlobalTransformers(this.transformContext);
for (const spec of allSpecs) {
traverseSwagger(spec, {
onOperation: (operation) => {
if (operation.operationId === undefined) {
throw new Error(
`OperationId is undefined for operation ${operation._method} ${operation._path._pathTemplate}`
);
}
if (opsMap.has(operation.operationId)) {
throw new Error(
`Duplicated operationId ${operation.operationId}: ${
operation._path._pathTemplate
}\nConflict with path: ${opsMap.get(operation.operationId)?._path._pathTemplate}`
);
}
opsMap.set(operation.operationId, operation);
verMap.set(operation.operationId, spec.info.version);
if (egOpMap) {
const xMsExamples = operation[xmsExamples] ?? {};
for (const exampleName of Object.keys(xMsExamples)) {
const example = xMsExamples[exampleName];
if (typeof example.$ref !== "string") {
throw new Error(`Example doesn't use $ref: ${exampleName}`);
}
const exampleFilePath = this.fileLoader.relativePath(
this.jsonLoader.getRealPath(example.$ref)
);
let opMap = egOpMap.get(exampleFilePath);
if (opMap === undefined) {
opMap = {};
egOpMap.set(exampleFilePath, opMap);
}
opMap[operation.operationId] = exampleName;
}
}
},
});
}
}
public async load(
scenarioFilePath: string,
swaggerFilePaths?: string[],
readmePath?: string
): Promise<ScenarioDefinition> {
const [rawDef, additionalTags] = await this.apiScenarioYamlLoader.load(scenarioFilePath);
if (!swaggerFilePaths || swaggerFilePaths.length === 0) {
swaggerFilePaths = [];
if (!readmePath) {
readmePath = await findReadMe(pathDirName(scenarioFilePath));
}
if (readmePath) {
const inputFile = await getInputFiles(readmePath);
for (const it of inputFile ?? []) {
if (swaggerFilePaths.indexOf(it) < 0) {
swaggerFilePaths.push(this.fileLoader.resolvePath(it));
}
}
}
}
await this.initialize(swaggerFilePaths, additionalTags);
rawDef.scope = rawDef.scope ?? "ResourceGroup";
const isArmScope = rawDef.scope !== "None";
const scenarioDef: ScenarioDefinition = {
name: basename(scenarioFilePath).substring(0, basename(scenarioFilePath).lastIndexOf(".")),
scope: rawDef.scope,
prepareSteps: [],
scenarios: [],
_filePath: this.fileLoader.relativePath(scenarioFilePath),
_swaggerFilePaths: swaggerFilePaths!,
cleanUpSteps: [],
...convertVariables(rawDef.variables),
authentication: this.loadAuthentication(rawDef.authentication),
};
if (isArmScope) {
if (!scenarioDef.authentication) {
scenarioDef.authentication = {
type: "AADToken",
scope: "$(armEndpoint)/.default",
};
}
if (!scenarioDef.variables.armEndpoint) {
scenarioDef.variables.armEndpoint = {
type: "string",
value: DEFAULT_ARM_ENDPOINT,
};
}
const requiredVariables = new Set(scenarioDef.requiredVariables);
if (["ResourceGroup", "Subscription"].indexOf(scenarioDef.scope) >= 0) {
requiredVariables.add("subscriptionId");
if (scenarioDef.scope === "ResourceGroup") {
requiredVariables.add("location");
}
}
scenarioDef.requiredVariables = [...requiredVariables];
}
const ctx: ApiScenarioContext = {
stepTracking: new Map(),
scenarioDef: scenarioDef,
stepIndex: 0,
};
await this.loadPrepareSteps(rawDef, ctx);
await this.loadCleanUpSteps(rawDef, ctx);
ctx.scenarioIndex = 0;
for (const rawScenario of rawDef.scenarios) {
ctx.stepTracking.clear();
const scenario = await this.loadScenario(rawScenario, ctx);
scenarioDef.scenarios.push(scenario);
ctx.scenarioIndex++;
}
// await this.writeTestDefinitionFile("./test.yaml", scenarioDef);
return scenarioDef;
}
private loadAuthentication(
rawAuthentication: RawAuthentication | undefined
): Authentication | undefined {
const authentication = rawAuthentication as Authentication | undefined;
switch (authentication?.type) {
case "AADToken":
authentication.scope = authentication.scope ?? "$(armEndpoint)/.default";
break;
case "AzureKey":
authentication.name = authentication.name ?? "Authorization";
authentication.in = authentication.in ?? "header";
break;
case "None":
default:
break;
}
return authentication;
}
private async loadPrepareSteps(rawDef: RawScenarioDefinition, ctx: ApiScenarioContext) {
ctx.stage = "prepare";
ctx.stepIndex = 0;
for (const rawStep of rawDef.prepareSteps ?? []) {
const step = await this.loadStep(rawStep, ctx);
step.isPrepareStep = true;
ctx.scenarioDef.prepareSteps.push(step);
}
}
private async loadCleanUpSteps(rawDef: RawScenarioDefinition, ctx: ApiScenarioContext) {
ctx.stage = "cleanUp";
ctx.stepIndex = 0;
for (const rawStep of rawDef.cleanUpSteps ?? []) {
const step = await this.loadStep(rawStep, ctx);
step.isCleanUpStep = true;
ctx.scenarioDef.cleanUpSteps.push(step);
}
}
private async loadScenario(rawScenario: RawScenario, ctx: ApiScenarioContext): Promise<Scenario> {
ctx.stage = "scenario";
const steps: Step[] = [];
const { scenarioDef } = ctx;
const variableScope = convertVariables(rawScenario.variables);
variableScope.requiredVariables = [
...new Set([...scenarioDef.requiredVariables, ...variableScope.requiredVariables]),
];
const scenario: Scenario = {
scenario: rawScenario.scenario ?? `scenario_${ctx.scenarioIndex}`,
description: rawScenario.description ?? "",
steps,
_scenarioDef: scenarioDef,
...variableScope,
authentication:
this.loadAuthentication(rawScenario.authentication) ?? scenarioDef.authentication,
};
ctx.scenario = scenario;
ctx.stepIndex = 0;
for (const rawStep of rawScenario.steps) {
const step = await this.loadStep(rawStep, ctx);
steps.push(step);
}
return scenario;
}
private async loadStep(rawStep: RawStep, ctx: ApiScenarioContext): Promise<Step> {
let step: Step;
try {
if ("operationId" in rawStep || "exampleFile" in rawStep) {
step = await this.loadStepRestCall(rawStep, ctx);
} else if ("armTemplate" in rawStep) {
step = await this.loadStepArmTemplate(rawStep, ctx);
} else if ("armDeploymentScript" in rawStep) {
step = await this.loadStepArmDeploymentScript(rawStep, ctx);
} else if ("roleAssignment" in rawStep) {
step = await this.loadStepRoleAssignment(rawStep, ctx);
} else {
throw new Error("Invalid step");
}
} catch (error) {
throw new Error(`Failed to load step ${JSON.stringify(rawStep)}: ${(error as any).message}`);
}
if (step.outputVariables) {
if (ctx.scenario !== undefined) {
declareOutputVariables(step.outputVariables, ctx.scenario);
} else {
declareOutputVariables(step.outputVariables, ctx.scenarioDef);
}
}
ctx.stepIndex!++;
return step;
}
private getVariableFunction(step: Step, ctx: ApiScenarioContext) {
return (name: string) => {
const variable =
step.variables[name] ?? ctx.scenario?.variables[name] ?? ctx.scenarioDef.variables[name];
return variable;
};
}
private async loadStepRestCall(
rawStep: RawStepOperation | RawStepExample,
ctx: ApiScenarioContext
): Promise<StepRestCall> {
if (rawStep.step !== undefined && ctx.stepTracking.has(rawStep.step)) {
throw new Error(`Duplicated step name: ${rawStep.step}`);
}
const step: StepRestCall = {
type: "restCall",
step: rawStep.step ?? `_${ctx.stepIndex}`,
description: rawStep.description,
operationId: "",
operation: {} as Operation,
parameters: {} as SwaggerExample["parameters"],
responses: {} as SwaggerExample["responses"],
outputVariables: rawStep.outputVariables ?? {},
...convertVariables(rawStep.variables),
authentication:
this.loadAuthentication(rawStep.authentication) ??
ctx.scenario?.authentication ??
ctx.scenarioDef.authentication,
};
const getVariable = (
name: string,
...scopes: Array<VariableScope | undefined>
): Variable | undefined => {
if (!scopes || scopes.length === 0) {
scopes = [step, ctx.scenario, ctx.scenarioDef];
}
for (const scope of scopes) {
if (scope && scope.variables[name]) {
return scope.variables[name];
}
}
for (const scope of scopes) {
if (scope && scope.requiredVariables.includes(name)) {
return {
type: "string",
value: `$(${name})`,
};
}
}
if (
(ctx.scenarioDef.scope === "ResourceGroup" &&
["subscriptionId", "resourceGroupName", "location"].includes(name)) ||
(ctx.scenarioDef.scope === "Subscription" && ["subscriptionId"].includes(name))
) {
return {
type: "string",
value: `$(${name})`,
};
}
return undefined;
};
const requireVariable = (name: string) => {
if (ctx.scenarioDef.scope === "ResourceGroup" && ["resourceGroupName"].includes(name)) {
return;
}
const requiredVariables =
ctx.scenario?.requiredVariables ?? ctx.scenarioDef.requiredVariables;
if (!requiredVariables.includes(name)) {
requiredVariables.push(`${name}`);
}
};
let operation: Operation | undefined;
if (!("exampleFile" in rawStep)) {
// load operation step
step.operationId = rawStep.operationId;
operation = rawStep.readmeTag
? this.additionalMap.get(rawStep.readmeTag)?.operationsMap.get(step.operationId)
: this.operationsMap.get(step.operationId);
if (operation === undefined) {
throw new Error(`Operation not found for ${step.operationId} in step ${step.step}`);
}
if (rawStep.readmeTag) {
step.externalReference = true;
}
step.isManagementPlane = this.isManagementPlane(operation);
if (this.opts.includeOperation) {
step.operation = operation;
}
if (rawStep.variables) {
for (const [name, value] of Object.entries(rawStep.variables)) {
if (typeof value === "string") {
step.variables[name] = { type: "string", value };
continue;
}
if (value.type === "object" || value.type === "secureObject" || value.type === "array") {
if (value.patches) {
const variable = ctx.scenario
? getVariable(name, ctx.scenario, ctx.scenarioDef)
: getVariable(name, ctx.scenarioDef);
if (!variable) {
throw new Error(`Variable ${name} not found in step ${step.step}`);
}
const obj = cloneDeep(variable);
if (typeof obj !== "object") {
// TODO dynamic json patch
throw new Error(`Can not Json Patch on ${name}, type of ${typeof obj}`);
}
jsonPatchApply(obj.value, value.patches);
step.variables[name] = obj;
continue;
}
}
step.variables[name] = value;
}
}
operation.parameters?.forEach((param) => {
param = this.jsonLoader.resolveRefObj(param);
if (param.name === "api-version") {
step.parameters["api-version"] = rawStep.readmeTag
? this.additionalMap.get(rawStep.readmeTag)?.apiVersionsMap.get(step.operationId)!
: this.apiVersionsMap.get(step.operationId)!;
} else if (rawStep.parameters?.[param.name]) {
step.parameters[param.name] = rawStep.parameters[param.name];
} else {
const v = getVariable(param.name);
if (v) {
if (param.in === "body") {
step.parameters[param.name] = v.value;
} else {
step.parameters[param.name] = `$(${param.name})`;
}
} else if (param.in === "path" || param.required) {
step.parameters[param.name] = `$(${param.name})`;
requireVariable(param.name);
}
}
});
const xHost = operation._path._spec["x-ms-parameterized-host"];
if (xHost) {
xHost.parameters.forEach((param) => {
param = this.jsonLoader.resolveRefObj(param);
if (rawStep.parameters?.[param.name]) {
step.variables[param.name] = {
type: "string",
value: rawStep.parameters[param.name] as string,
};
}
});
}
step.responseAssertion = rawStep.responses;
} else {
// load example step
step.exampleFile = rawStep.exampleFile;
const exampleFilePath = pathJoin(pathDirName(ctx.scenarioDef._filePath), step.exampleFile!);
// Load example file
const fileContent = await this.fileLoader.load(exampleFilePath);
const exampleFileContent = JSON.parse(fileContent) as SwaggerExample;
// Load Operation
if (rawStep.operationId || exampleFileContent.operationId) {
step.operationId = (rawStep.operationId ?? exampleFileContent.operationId)!;
operation = this.operationsMap.get(step.operationId);
if (operation === undefined) {
throw new Error(`Operation not found for ${step.operationId} in step ${step.step}`);
}
step.isManagementPlane = this.isManagementPlane(operation);
if (this.opts.includeOperation) {
step.operation = operation;
}
} else {
const opMap = this.exampleToOperation.get(exampleFilePath);
if (opMap === undefined) {
throw new Error(`Example file is not referenced by any operation: ${step.exampleFile}`);
}
const ops = Object.keys(opMap);
if (ops.length > 1) {
throw new Error(
`Example file is referenced by multiple operation: ${Object.keys(opMap)} ${
step.exampleFile
}`
);
}
step.operationId = ops[0];
const exampleName = opMap[step.operationId];
operation = this.operationsMap.get(step.operationId);
if (operation === undefined) {
throw new Error(`Operation not found for ${step.operationId} in step ${step.step}`);
}
step.isManagementPlane = this.isManagementPlane(operation);
if (this.opts.includeOperation) {
step.operation = operation;
}
step.description = step.description ?? exampleName;
}
step.parameters = exampleFileContent.parameters;
// force update api-version
if (step.parameters["api-version"]) {
step.parameters["api-version"] = this.apiVersionsMap.get(step.operationId)!;
}
step.responses = exampleFileContent.responses;
await this.applyPatches(step, rawStep, operation);
this.templateGenerator.exampleParameterConvention(step, getVariable, operation);
}
const security = operation?.security || operation?._path._spec.security;
if (!step.authentication && security) {
if (security.length > 1) {
logger.warn("Multiple security definitions found, only the first one will be used");
}
const security0 = security[0];
const key = Object.keys(security0)[0];
const securityDefinition = operation?._path._spec.securityDefinitions?.[key];
if (!securityDefinition) {
throw new Error(`Security definition not found for ${key}`);
}
if (securityDefinition.type === "oauth2") {
const value = security0[key];
if (value.length > 1) {
throw new Error(`Multiple scopes are not supported yet: ${JSON.stringify(value)}`);
}
const scope = value[0];
step.authentication = {
type: "AADToken",
scope: scope,
};
} else if (securityDefinition.type === "apiKey") {
const def = securityDefinition as ApiKeySecurity;
step.authentication = {
type: "AzureKey",
key: `$(${key})`,
in: def.in === "query" ? "query" : "header",
name: def.name ?? "Authorization",
};
}
}
if (!rawStep.step) {
step.step = step.operationId;
let i = 1;
while (ctx.stepTracking.has(step.step)) {
step.step += `_${i++}`;
}
}
ctx.stepTracking.set(step.step, step);
return step;
}
private isManagementPlane(operation: Operation): boolean {
const spec = operation._path._spec;
if (spec.host === "management.azure.com") {
return true;
}
if (spec._filePath.indexOf("/resource-manager/") >= 0) {
return true;
}
return false;
}
private async applyPatches(step: StepRestCall, rawStep: RawStepExample, operation: Operation) {
if (rawStep.requestUpdate) {
const bodyParam = getBodyParam(operation, this.jsonLoader);
let source;
if (bodyParam) {
source = cloneDeep(step.parameters[bodyParam.name]);
}
jsonPatchApply(step.parameters, rawStep.requestUpdate);
if (["put", "patch"].includes(operation._method) && bodyParam) {
const target = step.parameters[bodyParam.name];
const propertiesMergePatch = jsonMergePatchGenerate(source, target);
Object.keys(step.responses).forEach(async (statusCode) => {
if (statusCode >= "400") {
return;
}
const response = step.responses[statusCode];
if (response.body) {
jsonMergeApply(response.body, propertiesMergePatch);
await this.bodyTransformer.resourceToResponse(
response.body,
operation.responses[statusCode].schema!
);
}
});
}
}
if (rawStep.responseUpdate) {
jsonPatchApply(step.responses, rawStep.responseUpdate);
}
}
private async loadStepArmDeploymentScript(
rawStep: RawStepArmScript,
ctx: ApiScenarioContext
): Promise<StepArmTemplate> {
const step: StepArmTemplate = {
type: "armTemplateDeployment",
step: rawStep.step ?? `ArmDeploymentScript_${ctx.stepIndex}`,
outputVariables: rawStep.outputVariables ?? {},
armTemplate: "",
armTemplatePayload: {},
...convertVariables(rawStep.variables),
};
const { scenarioDef } = ctx;
const payload = cloneDeep(armDeploymentScriptTemplate) as ArmTemplate;
step.armTemplatePayload = payload;
const resource = payload.resources![0] as ArmDeploymentScriptResource;
resource.name = step.step;
if (rawStep.armDeploymentScript.endsWith(".ps1")) {
resource.kind = "AzurePowerShell";
resource.properties.azPowerShellVersion = "6.2";
} else {
resource.kind = "AzureCLI";
resource.properties.azCliVersion = "2.0.80";
}
const filePath = pathJoin(pathDirName(scenarioDef._filePath), rawStep.armDeploymentScript);
const scriptContent = await this.fileLoader.load(filePath);
resource.properties.scriptContent = scriptContent;
resource.properties.arguments = rawStep.arguments;
for (const variable of rawStep.environmentVariables ?? []) {
if (this.isSecretVariable(variable.value, step, ctx)) {
resource.properties.environmentVariables!.push({
name: variable.name,
secureValue: variable.value,
});
} else {
resource.properties.environmentVariables!.push({
name: variable.name,
value: variable.value,
});
}
}
this.templateGenerator.armTemplateParameterConvention(
step,
this.getVariableFunction(step, ctx)
);
return step;
}
private async loadStepRoleAssignment(
rawStep: RawStepRoleAssignment,
ctx: ApiScenarioContext
): Promise<StepRoleAssignment> {
const step: StepRoleAssignment = {
type: "armRoleAssignment",
step: rawStep.step ?? `RoleAssignment_${ctx.stepIndex}`,
outputVariables: rawStep.outputVariables ?? {},
roleAssignment: rawStep.roleAssignment,
...convertVariables(rawStep.variables),
authentication: ctx.scenario?.authentication ?? ctx.scenarioDef.authentication,
};
return step;
}
private isSecretVariable(variable: string, step: Step, ctx: ApiScenarioContext): boolean {
const { scenarioDef, scenario } = ctx;
if (variableRegex.test(variable)) {
const globalRegex = new RegExp(variableRegex, "g");
let match;
while ((match = globalRegex.exec(variable))) {
const refKey = match[1];
if (
step.secretVariables.includes(refKey) ||
scenario?.secretVariables.includes(refKey) ||
scenarioDef.secretVariables.includes(refKey)
) {
return true;
}
}
}
return false;
}
private async loadStepArmTemplate(
rawStep: RawStepArmTemplate,
ctx: ApiScenarioContext
): Promise<StepArmTemplate> {
const step: StepArmTemplate = {
type: "armTemplateDeployment",
step: rawStep.step ?? `${ctx.scenarioIndex ?? ctx.stage}_${ctx.stepIndex}_ArmTemplate`,
outputVariables: rawStep.outputVariables ?? {},
armTemplate: rawStep.armTemplate,
armTemplatePayload: {},
...convertVariables(rawStep.variables),
};
const { scenarioDef, scenario } = ctx;
const variableScope: VariableScope = scenario ?? scenarioDef;
const filePath = pathJoin(pathDirName(scenarioDef._filePath), step.armTemplate);
const armTemplateContent = await this.fileLoader.load(filePath);
step.armTemplatePayload = JSON.parse(armTemplateContent);
const params = step.armTemplatePayload.parameters;
if (params !== undefined) {
for (const paramName of Object.keys(params)) {
if (
params[paramName].defaultValue !== undefined ||
step.variables[paramName] !== undefined ||
variableScope.variables[paramName] !== undefined ||
scenarioDef.variables[paramName] !== undefined
) {
continue;
}
if (
params[paramName].type !== "string" &&
params[paramName].type !== "securestring" &&
!variableScope.requiredVariables.includes(paramName)
) {
throw new Error(
`Only string and securestring type is supported in arm template params, please specify defaultValue for: ${paramName}`
);
}
variableScope.requiredVariables.push(paramName);
}
}
const outputs = step.armTemplatePayload.outputs;
if (outputs !== undefined) {
declareOutputVariables(outputs, variableScope);
}
this.templateGenerator.armTemplateParameterConvention(
step,
this.getVariableFunction(step, ctx)
);
return step;
}
}
export const getBodyParam = (operation: Operation, jsonLoader: JsonLoader) => {
const bodyParams = pickParams(operation, "body", jsonLoader) as BodyParameter[] | undefined;
return bodyParams?.[0];
};
const pickParams = (operation: Operation, location: Parameter["in"], jsonLoader: JsonLoader) => {
const params = operation.parameters
?.map((param) => jsonLoader.resolveRefObj(param))
.filter((resolvedObj) => resolvedObj.in === location);
return params;
};
const convertVariables = (rawVariables: RawVariableScope["variables"]) => {
const result: VariableScope = {
variables: {},
requiredVariables: [],
secretVariables: [],
};
for (const [key, val] of Object.entries(rawVariables ?? {})) {
if (typeof val === "string") {
result.variables[key] = {
type: "string",
value: val,
};
} else {
result.variables[key] = val;
if (val.value === undefined) {
if (val.type === "string" || val.type === "secureString") {
if (val.value === undefined && val.prefix === undefined) {
result.requiredVariables.push(key);
}
} else if (
(val.type === "object" || val.type === "secureObject" || val.type === "array") &&
val.patches !== undefined
) {
// ok
} else {
throw new Error(
`Only string and secureString type is supported in environment variables, please specify value for: ${key}`
);
}
}
if (val.type === "secureString" || val.type === "secureObject") {
result.secretVariables.push(key);
}
}
}
return result;
};
const declareOutputVariables = (
outputVariables: { [key: string]: { type?: any } },
scope: VariableScope
) => {
for (const [key, val] of Object.entries(outputVariables)) {
if (scope.variables[key] === undefined) {
scope.variables[key] = {
type: val.type ?? "string",
};
}
if (val.type === "secureString" || val.type === "securestring" || val.type === "secureObject") {
scope.secretVariables.push(key);
}
}
};