diff --git a/lib/constants.js b/lib/constants.js index 1848ef4d..96f9f039 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -1,6 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.defaultYaml = void 0; +exports.APIRoute = exports.testmanagerApiVersion = exports.defaultYaml = void 0; exports.defaultYaml = { version: 'v0.1', testId: 'SampleTest', @@ -48,3 +48,13 @@ exports.defaultYaml = { } ] }; +exports.testmanagerApiVersion = "2024-07-01-preview"; +var BaseAPIRoute; +(function (BaseAPIRoute) { + BaseAPIRoute.featureFlag = "featureFlags"; +})(BaseAPIRoute || (BaseAPIRoute = {})); +var APIRoute; +(function (APIRoute) { + const latestVersion = "api-version=" + exports.testmanagerApiVersion; + APIRoute.FeatureFlags = (flag) => `${BaseAPIRoute.featureFlag}/${flag}?${latestVersion}`; +})(APIRoute || (exports.APIRoute = APIRoute = {})); diff --git a/lib/main.js b/lib/main.js index 32f247be..f024a3d7 100644 --- a/lib/main.js +++ b/lib/main.js @@ -39,6 +39,8 @@ const util = __importStar(require("./util")); const TestKind_1 = require("./engine/TestKind"); const fs = __importStar(require("fs")); const util_1 = require("util"); +const FeatureFlagService_1 = require("./services/FeatureFlagService"); +const FeatureFlags_1 = require("./services/FeatureFlags"); const resultFolder = 'loadTest'; const reportZipFileName = 'report.zip'; const resultZipFileName = 'results.zip'; @@ -74,8 +76,8 @@ function run() { } }); } -function getTestAPI(validate) { - return __awaiter(this, void 0, void 0, function* () { +function getTestAPI(validate_1) { + return __awaiter(this, arguments, void 0, function* (validate, returnTestObj = false) { var _a, _b; var urlSuffix = "tests/" + testId + "?api-version=" + util.apiConstants.latestVersion; urlSuffix = baseURL + urlSuffix; @@ -113,6 +115,9 @@ function getTestAPI(validate) { } var inputScriptFileInfo = testObj.kind == TestKind_1.TestKind.URL ? testObj.inputArtifacts.urlTestConfigFileInfo : testObj.inputArtifacts.testScriptFileInfo; if (validate) { + if (returnTestObj) { + return [inputScriptFileInfo.validationStatus, testObj]; + } return inputScriptFileInfo.validationStatus; } else { @@ -223,9 +228,10 @@ function uploadTestPlan() { var startTime = new Date(); var maxAllowedTime = new Date(startTime.getTime() + minutesToAdd * 60000); var validationStatus = "VALIDATION_INITIATED"; + var testObj; while (maxAllowedTime > (new Date()) && (validationStatus == "VALIDATION_INITIATED" || validationStatus == "NOT_VALIDATED" || validationStatus == null)) { try { - validationStatus = yield getTestAPI(true); + [validationStatus, testObj] = yield getTestAPI(true, true); } catch (e) { retry--; @@ -235,8 +241,18 @@ function uploadTestPlan() { } yield util.sleep(5000); } + console.log("Validation status of the test plan: " + validationStatus); if (validationStatus == null || validationStatus == "VALIDATION_SUCCESS") { console.log(`Validated test plan for the test successfully.`); + // Get errors from all files + var fileErrors = util.getAllFileErrors(testObj); + if (Object.keys(fileErrors).length > 0) { + console.log("Validation failed for the following files:"); + for (const [file, error] of Object.entries(fileErrors)) { + console.log(`File: ${file}, Error: ${error}`); + } + throw new Error("Validation of one or more files failed. Please correct the errors and try again."); + } yield createTestRun(); } else if (validationStatus == "VALIDATION_INITIATED" || validationStatus == "NOT_VALIDATED") @@ -252,7 +268,7 @@ function uploadConfigFile() { if (configFiles != undefined && configFiles.length > 0) { for (let filepath of configFiles) { let filename = map.getFileName(filepath); - var urlSuffix = "tests/" + testId + "/files/" + filename + "?api-version=" + util.apiConstants.latestVersion; + var urlSuffix = "tests/" + testId + "/files/" + filename + "?api-version=" + util.apiConstants.latestVersion + ("&fileType=" + FileType.ADDITIONAL_ARTIFACTS); urlSuffix = baseURL + urlSuffix; let headers = yield map.UploadAndValidateHeader(); let uploadresult = yield util.httpClientRetries(urlSuffix, headers, 'put', 3, filepath, true); @@ -291,6 +307,13 @@ function uploadZipArtifacts() { let flagValidationPending = true; let zipInvalid = false; let zipFailureReason = ""; + // TODO(harshanb): Remove this check once the feature flag is enabled by default. + let isTestScriptFragmentEnabled = yield FeatureFlagService_1.FeatureFlagService.isFeatureEnabledAsync(FeatureFlags_1.FeatureFlags.enableTestScriptFragments, baseURL); + let zipValidationTerminateStates = ["VALIDATION_SUCCESS", "VALIDATION_FAILURE"]; + if (isTestScriptFragmentEnabled) { + // NOT_VALIDATED is a terminal state for the file validation and actual validation will be performed during test script upload + zipValidationTerminateStates.push("NOT_VALIDATED"); + } while (maxAllowedTime > (new Date()) && flagValidationPending) { var urlSuffix = "tests/" + testId + "?api-version=" + util.apiConstants.latestVersion; urlSuffix = baseURL + urlSuffix; @@ -304,7 +327,7 @@ function uploadZipArtifacts() { flagValidationPending = false; if (testObj && testObj.inputArtifacts && testObj.inputArtifacts.additionalFileInfo) { for (const file of testObj.inputArtifacts.additionalFileInfo) { - if (file.fileType == FileType.ZIPPED_ARTIFACTS && (file.validationStatus != "VALIDATION_SUCCESS" && file.validationStatus != "VALIDATION_FAILURE")) { + if (file.fileType == FileType.ZIPPED_ARTIFACTS && (file.validationStatus in zipValidationTerminateStates)) { flagValidationPending = true; break; } @@ -329,7 +352,12 @@ function uploadZipArtifacts() { else if (flagValidationPending) { throw new Error("Validation of one or more zip artifacts timed out. Please retry."); } - console.log(`Uploaded and validated ${zipFiles.length} zip artifact(s) for the test successfully.`); + if (isTestScriptFragmentEnabled) { + console.log(`Uploaded ${zipFiles.length} zip artifact(s) for the test successfully.`); + } + else { + console.log(`Uploaded and validated ${zipFiles.length} zip artifact(s) for the test successfully.`); + } } var statuscode = yield uploadPropertyFile(); if (statuscode == 201) { diff --git a/lib/mappers.js b/lib/mappers.js index 578260a3..585aef27 100644 --- a/lib/mappers.js +++ b/lib/mappers.js @@ -32,7 +32,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.getARMEndpoint = exports.getTenantId = exports.getFileName = exports.getTestId = exports.getZipFiles = exports.getConfigFiles = exports.getPropertyFile = exports.getTestFile = exports.getYamlPath = exports.getTestKind = exports.getDefaultTestRunName = exports.getDefaultTestName = exports.getSubName = exports.getInputParams = exports.getResourceId = exports.getTestHeader = exports.getTestRunHeader = exports.startTestData = exports.armTokenHeader = exports.UploadAndValidateHeader = exports.createTestHeader = exports.createTestData = void 0; +exports.getARMEndpoint = exports.getTenantId = exports.getFileName = exports.getTestId = exports.getZipFiles = exports.getConfigFiles = exports.getPropertyFile = exports.getTestFile = exports.getYamlPath = exports.getTestKind = exports.getDefaultTestRunName = exports.getDefaultTestName = exports.getSubName = exports.getInputParams = exports.getResourceId = exports.getTestHeader = exports.getTestRunHeader = exports.startTestData = exports.armTokenHeader = exports.UploadAndValidateHeader = exports.createTestHeader = exports.createTestData = exports.getToken = void 0; const core = __importStar(require("@actions/core")); const yaml = require("js-yaml"); const jwt_decode = __importStar(require("jwt-decode")); @@ -100,6 +100,10 @@ function getExistingData() { envYaml[key] = null; } } +function getToken() { + return token; +} +exports.getToken = getToken; function createTestData() { getExistingData(); var data = { diff --git a/lib/models/APIResponseModel.js b/lib/models/APIResponseModel.js new file mode 100644 index 00000000..c8ad2e54 --- /dev/null +++ b/lib/models/APIResponseModel.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/services/FeatureFlagService.js b/lib/services/FeatureFlagService.js new file mode 100644 index 00000000..2b014428 --- /dev/null +++ b/lib/services/FeatureFlagService.js @@ -0,0 +1,73 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.FeatureFlagService = void 0; +const constants_1 = require("../constants"); +const map = __importStar(require("../mappers")); +const util = __importStar(require("../util")); +class FeatureFlagService { + static getFeatureFlagAsync(flag_1, baseUrl_1) { + return __awaiter(this, arguments, void 0, function* (flag, baseUrl, useCache = true) { + if (useCache && flag in this.featureFlagCache) { + return { featureFlag: flag, enabled: this.featureFlagCache[flag.toString()] }; + } + let uri = baseUrl + constants_1.APIRoute.FeatureFlags(flag.toString()); + let headers = { + 'content-type': 'application/json', + 'Authorization': 'Bearer ' + map.getToken() + }; + let flagResponse = yield util.httpClientRetries(uri, headers, 'get', 3, "", false, false); + try { + let flagObj = (yield util.getResultObj(flagResponse)); + this.featureFlagCache[flag.toString()] = flagObj.enabled; + return flagObj; + } + catch (error) { + // remove item from dict + // handle in case getFlag was called with cache true once and then with cache false, and failed during second call + // remove the item from cache so that it can be fetched again rather than using old value + delete this.featureFlagCache[flag.toString()]; + return null; + } + }); + } + static isFeatureEnabledAsync(flag_1, baseUrl_1) { + return __awaiter(this, arguments, void 0, function* (flag, baseUrl, useCache = true) { + let flagObj = yield this.getFeatureFlagAsync(flag, baseUrl, useCache); + return flagObj ? flagObj.enabled : false; + }); + } +} +exports.FeatureFlagService = FeatureFlagService; +FeatureFlagService.featureFlagCache = {}; diff --git a/lib/services/FeatureFlags.js b/lib/services/FeatureFlags.js new file mode 100644 index 00000000..5c24cf5f --- /dev/null +++ b/lib/services/FeatureFlags.js @@ -0,0 +1,7 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.FeatureFlags = void 0; +var FeatureFlags; +(function (FeatureFlags) { + FeatureFlags["enableTestScriptFragments"] = "enableTestScriptFragments"; +})(FeatureFlags || (exports.FeatureFlags = FeatureFlags = {})); diff --git a/lib/util.js b/lib/util.js index c96e57d0..6969a4a1 100644 --- a/lib/util.js +++ b/lib/util.js @@ -32,7 +32,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.ErrorCorrection = exports.getResultObj = exports.validCriteria = exports.isTerminalTestStatus = exports.removeUnits = exports.indexOfFirstDigit = exports.deleteFile = exports.getReportFolder = exports.getResultFolder = exports.checkValidityYaml = exports.invalidDescription = exports.invalidDisplayName = exports.invalidName = exports.getUniqueId = exports.sleep = exports.printClientMetrics = exports.uploadFileToResultsFolder = exports.printCriteria = exports.printTestDuration = exports.checkFileTypes = exports.checkFileType = exports.httpClientRetries = exports.uploadFileData = exports.ManagedIdentityType = exports.apiConstants = void 0; +exports.getAllFileErrors = exports.ErrorCorrection = exports.getResultObj = exports.validCriteria = exports.isTerminalTestStatus = exports.removeUnits = exports.indexOfFirstDigit = exports.deleteFile = exports.getReportFolder = exports.getResultFolder = exports.checkValidityYaml = exports.invalidDescription = exports.invalidDisplayName = exports.invalidName = exports.getUniqueId = exports.sleep = exports.printClientMetrics = exports.uploadFileToResultsFolder = exports.printCriteria = exports.printTestDuration = exports.checkFileTypes = exports.checkFileType = exports.httpClientRetries = exports.uploadFileData = exports.ManagedIdentityType = exports.apiConstants = void 0; const fs = __importStar(require("fs")); var path = require('path'); var AdmZip = require("adm-zip"); @@ -87,7 +87,7 @@ function uploadFileData(filepath) { exports.uploadFileData = uploadFileData; const correlationHeader = 'x-ms-correlation-request-id'; function httpClientRetries(urlSuffix_1, header_1, method_1) { - return __awaiter(this, arguments, void 0, function* (urlSuffix, header, method, retries = 1, data, isUploadCall = true) { + return __awaiter(this, arguments, void 0, function* (urlSuffix, header, method, retries = 1, data, isUploadCall = true, log = true) { let httpResponse; try { let correlationId = `gh-actions-${getUniqueId()}`; @@ -118,7 +118,9 @@ function httpClientRetries(urlSuffix_1, header_1, method_1) { if (retries) { let sleeptime = (5 - retries) * 1000 + Math.floor(Math.random() * 5001); yield sleep(sleeptime); - console.log(`Failed to connect to ${urlSuffix} due to ${err.message}, retrying in ${sleeptime / 1000} seconds`); + if (log) { + console.log(`Failed to connect to ${urlSuffix} due to ${err.message}, retrying in ${sleeptime / 1000} seconds`); + } return httpClientRetries(urlSuffix, header, method, retries - 1, data); } else @@ -542,3 +544,23 @@ function ErrorCorrection(result) { return "Unable to fetch the response. Please re-run or contact support if the issue persists. " + "Status code: " + result.message.statusCode; } exports.ErrorCorrection = ErrorCorrection; +function getAllFileErrors(testObj) { + var allArtifacts = []; + for (var key in testObj.inputArtifacts) { + var artifacts = testObj.inputArtifacts[key]; + if (artifacts instanceof Array) { + allArtifacts = allArtifacts.concat(artifacts.filter((artifact) => artifact !== null && artifact !== undefined)); + } + else if (artifacts !== null && artifacts !== undefined) { + allArtifacts.push(artifacts); + } + } + var fileErrors = {}; + for (const file of allArtifacts) { + if (file.validationStatus === "VALIDATION_FAILURE") { + fileErrors[file.fileName] = file.validationFailureDetails; + } + } + return fileErrors; +} +exports.getAllFileErrors = getAllFileErrors; diff --git a/src/constants.ts b/src/constants.ts index 8522ed4c..7b93ab26 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -46,3 +46,14 @@ export const defaultYaml: any = } ] } + +export const testmanagerApiVersion = "2024-07-01-preview"; + +namespace BaseAPIRoute { + export const featureFlag = "featureFlags"; +} + +export namespace APIRoute { + const latestVersion = "api-version="+testmanagerApiVersion; + export const FeatureFlags = (flag: string) => `${BaseAPIRoute.featureFlag}/${flag}?${latestVersion}`; +} diff --git a/src/main.ts b/src/main.ts index e19fc9b4..fba3c7c3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,6 +6,8 @@ import * as Util from './engine/Util'; import { TestKind } from "./engine/TestKind"; import * as fs from 'fs'; import { isNullOrUndefined } from 'util'; +import { FeatureFlagService } from './services/FeatureFlagService'; +import { FeatureFlags } from './services/FeatureFlags'; const resultFolder = 'loadTest'; const reportZipFileName = 'report.zip'; @@ -39,7 +41,7 @@ async function run() { core.setFailed(err.message); } } -async function getTestAPI(validate:boolean) { +async function getTestAPI(validate:boolean, returnTestObj:boolean = false) { var urlSuffix = "tests/"+testId+"?api-version="+util.apiConstants.latestVersion; urlSuffix = baseURL+urlSuffix; let header = await map.getTestHeader(); @@ -74,7 +76,11 @@ async function getTestAPI(validate:boolean) { testObj.kind = testObj.testType; } var inputScriptFileInfo = testObj.kind == TestKind.URL ? testObj.inputArtifacts.urlTestConfigFileInfo :testObj.inputArtifacts.testScriptFileInfo; + if(validate){ + if (returnTestObj) { + return [inputScriptFileInfo.validationStatus, testObj]; + } return inputScriptFileInfo.validationStatus; } else @@ -185,9 +191,10 @@ async function uploadTestPlan() var startTime = new Date(); var maxAllowedTime = new Date(startTime.getTime() + minutesToAdd*60000); var validationStatus = "VALIDATION_INITIATED"; + var testObj; while(maxAllowedTime>(new Date()) && (validationStatus == "VALIDATION_INITIATED" || validationStatus == "NOT_VALIDATED" || validationStatus == null)) { try{ - validationStatus = await getTestAPI(true); + [validationStatus, testObj] = await getTestAPI(true, true); } catch(e:any) { retry--; @@ -197,8 +204,21 @@ async function uploadTestPlan() } await util.sleep(5000); } + console.log("Validation status of the test plan: "+ validationStatus); if(validationStatus == null || validationStatus == "VALIDATION_SUCCESS" ){ console.log(`Validated test plan for the test successfully.`); + + // Get errors from all files + var fileErrors = util.getAllFileErrors(testObj); + + if (Object.keys(fileErrors).length > 0) { + console.log("Validation failed for the following files:"); + for (const [file, error] of Object.entries(fileErrors)) { + console.log(`File: ${file}, Error: ${error}`); + } + throw new Error("Validation of one or more files failed. Please correct the errors and try again."); + } + await createTestRun(); } else if(validationStatus == "VALIDATION_INITIATED" || validationStatus == "NOT_VALIDATED") @@ -213,7 +233,7 @@ async function uploadConfigFile() if(configFiles != undefined && configFiles.length > 0) { for (let filepath of configFiles) { let filename = map.getFileName(filepath); - var urlSuffix = "tests/"+testId+"/files/"+filename+"?api-version="+util.apiConstants.latestVersion; + var urlSuffix = "tests/"+testId+"/files/"+filename+"?api-version="+util.apiConstants.latestVersion + ("&fileType=" + FileType.ADDITIONAL_ARTIFACTS); urlSuffix = baseURL+urlSuffix; let headers = await map.UploadAndValidateHeader(); let uploadresult = await util.httpClientRetries(urlSuffix,headers,'put',3,filepath, true); @@ -250,6 +270,15 @@ async function uploadZipArtifacts() let flagValidationPending = true; let zipInvalid = false; let zipFailureReason = ""; + + // TODO(harshanb): Remove this check once the feature flag is enabled by default. + let isTestScriptFragmentEnabled = await FeatureFlagService.isFeatureEnabledAsync(FeatureFlags.enableTestScriptFragments, baseURL); + let zipValidationTerminateStates = ["VALIDATION_SUCCESS", "VALIDATION_FAILURE"]; + if (isTestScriptFragmentEnabled) { + // NOT_VALIDATED is a terminal state for the file validation and actual validation will be performed during test script upload + zipValidationTerminateStates.push("NOT_VALIDATED"); + } + while(maxAllowedTime>(new Date()) && flagValidationPending) { var urlSuffix = "tests/"+testId+"?api-version="+util.apiConstants.latestVersion; urlSuffix = baseURL+urlSuffix; @@ -263,7 +292,7 @@ async function uploadZipArtifacts() flagValidationPending = false; if (testObj && testObj.inputArtifacts && testObj.inputArtifacts.additionalFileInfo) { for(const file of testObj.inputArtifacts.additionalFileInfo){ - if (file.fileType == FileType.ZIPPED_ARTIFACTS && (file.validationStatus != "VALIDATION_SUCCESS" && file.validationStatus != "VALIDATION_FAILURE")) { + if (file.fileType == FileType.ZIPPED_ARTIFACTS && (file.validationStatus !in zipValidationTerminateStates)) { flagValidationPending = true; break; } else if(file.fileType == FileType.ZIPPED_ARTIFACTS && file.validationStatus == "VALIDATION_FAILURE"){ @@ -285,7 +314,12 @@ async function uploadZipArtifacts() } else if(flagValidationPending) { throw new Error("Validation of one or more zip artifacts timed out. Please retry."); } - console.log(`Uploaded and validated ${zipFiles.length} zip artifact(s) for the test successfully.`); + if (isTestScriptFragmentEnabled) { + console.log(`Uploaded ${zipFiles.length} zip artifact(s) for the test successfully.`); + } + else { + console.log(`Uploaded and validated ${zipFiles.length} zip artifact(s) for the test successfully.`); + } } var statuscode = await uploadPropertyFile(); if(statuscode== 201){ diff --git a/src/mappers.ts b/src/mappers.ts index e392e298..05862530 100644 --- a/src/mappers.ts +++ b/src/mappers.ts @@ -101,6 +101,11 @@ function getExistingData() { if (!envYaml.hasOwnProperty(key)) envYaml[key] = null; } } + +export function getToken() { + return token; +} + export function createTestData() { getExistingData(); var data = { diff --git a/src/models/APIResponseModel.ts b/src/models/APIResponseModel.ts new file mode 100644 index 00000000..453d37f1 --- /dev/null +++ b/src/models/APIResponseModel.ts @@ -0,0 +1,9 @@ +export interface Definitions { + /** + * Response for a feature flag query + */ + FeatureFlagResponse: { + featureFlag: string; + enabled: boolean; + }; +} diff --git a/src/services/FeatureFlagService.ts b/src/services/FeatureFlagService.ts new file mode 100644 index 00000000..125b8933 --- /dev/null +++ b/src/services/FeatureFlagService.ts @@ -0,0 +1,40 @@ +import { FeatureFlags } from "./FeatureFlags"; +import { Definitions } from "../models/APIResponseModel"; +import { APIRoute } from "../constants"; +import * as map from '../mappers'; +import * as util from '../util'; +import { IHeaders } from "typed-rest-client/Interfaces"; + +export class FeatureFlagService { + private static featureFlagCache: { [key: string]: boolean } = {}; + + public static async getFeatureFlagAsync(flag: FeatureFlags, baseUrl: string, useCache: boolean = true): Promise { + if (useCache && flag in this.featureFlagCache) { + return {featureFlag: flag, enabled: this.featureFlagCache[flag.toString()]}; + } + + let uri: string = baseUrl + APIRoute.FeatureFlags(flag.toString()); + let headers: IHeaders = { + 'content-type': 'application/json', + 'Authorization': 'Bearer '+ map.getToken() + }; + let flagResponse = await util.httpClientRetries(uri, headers, 'get', 3, "", false, false); + try { + let flagObj = (await util.getResultObj(flagResponse)) as Definitions["FeatureFlagResponse"]; + this.featureFlagCache[flag.toString()] = flagObj.enabled; + return flagObj; + } + catch (error) { + // remove item from dict + // handle in case getFlag was called with cache true once and then with cache false, and failed during second call + // remove the item from cache so that it can be fetched again rather than using old value + delete this.featureFlagCache[flag.toString()]; + return null; + } + } + + public static async isFeatureEnabledAsync(flag: FeatureFlags, baseUrl: string, useCache: boolean = true): Promise { + let flagObj = await this.getFeatureFlagAsync(flag, baseUrl, useCache); + return flagObj ? flagObj.enabled : false; + } +} \ No newline at end of file diff --git a/src/services/FeatureFlags.ts b/src/services/FeatureFlags.ts new file mode 100644 index 00000000..2c977c9f --- /dev/null +++ b/src/services/FeatureFlags.ts @@ -0,0 +1,3 @@ +export enum FeatureFlags { + enableTestScriptFragments = "enableTestScriptFragments", +} \ No newline at end of file diff --git a/src/util.ts b/src/util.ts index a083f49f..0bf3b258 100644 --- a/src/util.ts +++ b/src/util.ts @@ -54,7 +54,7 @@ export function uploadFileData(filepath: string) { } const correlationHeader = 'x-ms-correlation-request-id' -export async function httpClientRetries(urlSuffix : string, header : IHeaders, method : 'get' | 'del' | 'patch' | 'put', retries : number = 1,data : string, isUploadCall : boolean = true ) : Promise{ +export async function httpClientRetries(urlSuffix : string, header : IHeaders, method : 'get' | 'del' | 'patch' | 'put', retries : number = 1, data : string, isUploadCall : boolean = true, log: boolean = true) : Promise{ let httpResponse : IHttpClientResponse; try { let correlationId = `gh-actions-${getUniqueId()}`; @@ -85,7 +85,9 @@ export async function httpClientRetries(urlSuffix : string, header : IHeaders, m if(retries){ let sleeptime = (5-retries)*1000 + Math.floor(Math.random() * 5001); await sleep(sleeptime); - console.log(`Failed to connect to ${urlSuffix} due to ${err.message}, retrying in ${sleeptime/1000} seconds`); + if (log) { + console.log(`Failed to connect to ${urlSuffix} due to ${err.message}, retrying in ${sleeptime/1000} seconds`); + } return httpClientRetries(urlSuffix,header,method,retries-1,data); } else @@ -490,4 +492,25 @@ export async function getResultObj(data:any) { } export function ErrorCorrection(result : IHttpClientResponse){ return "Unable to fetch the response. Please re-run or contact support if the issue persists. " + "Status code: " + result.message.statusCode ; -} \ No newline at end of file +} + +export function getAllFileErrors(testObj:any): { [key: string]: string } { + var allArtifacts:any[] = []; + for (var key in testObj.inputArtifacts) { + var artifacts = testObj.inputArtifacts[key]; + if (artifacts instanceof Array ) { + allArtifacts = allArtifacts.concat(artifacts.filter((artifact:any) => artifact !== null && artifact !== undefined)); + } + else if (artifacts !== null && artifacts !== undefined) { + allArtifacts.push(artifacts); + } + } + + var fileErrors: { [key: string]: string } = {}; + for (const file of allArtifacts) { + if (file.validationStatus === "VALIDATION_FAILURE") { + fileErrors[file.fileName] = file.validationFailureDetails; + } + } + return fileErrors; +} diff --git a/test/Utils/checkForValidationOfYaml.test.ts b/test/Utils/checkForValidationOfYaml.test.ts index e28ad1b5..2ae7369f 100644 --- a/test/Utils/checkForValidationOfYaml.test.ts +++ b/test/Utils/checkForValidationOfYaml.test.ts @@ -1,4 +1,4 @@ -import { checkValidityYaml } from '../../src/util' +import { checkValidityYaml, getAllFileErrors } from '../../src/util' import * as constants from './testYamls'; describe('invalid Yaml tests', () =>{ @@ -148,4 +148,160 @@ describe('valid yaml tests', () => { test('subnet id and PIP is true', () => { expect(checkValidityYaml(constants.subnetIdPIPDisabledTrue)).toStrictEqual({valid : true, error : ""}); }); +}) + +describe('file errors', () => { + test('Test object with no file validation errors', () => { + // https://learn.microsoft.com/en-us/rest/api/loadtesting/dataplane/load-test-administration/get-test?view=rest-loadtesting-dataplane-2022-11-01&tabs=HTTP + let testObj = { + "testId": "12345678-1234-1234-1234-123456789012", + "description": "sample description", + "displayName": "Performance_LoadTest", + "loadTestConfiguration": { + "engineInstances": 6, + "splitAllCSVs": true + }, + "passFailCriteria": { + "passFailMetrics": { + "fefd759d-7fe8-4f83-8b6d-aeebe0f491fe": { + "clientMetric": "response_time_ms", + "aggregate": "percentage", + "condition": ">", + "value": 20, + "action": "continue", + "actualValue": 0, + "result": null + } + } + }, + "createdDateTime": "2021-12-05T16:43:46.072Z", + "createdBy": "user@contoso.com", + "lastModifiedDateTime": "2021-12-05T16:43:46.072Z", + "lastModifiedBy": "user@contoso.com", + "inputArtifacts": { + "configFileInfo": { + "url": "https://dummyurl.com/configresource", + "fileName": "config.yaml", + "fileType": "ADDITIONAL_ARTIFACTS", + "expireDateTime": "2021-12-05T16:43:46.072Z", + "validationStatus": "" + }, + "testScriptFileInfo": { + "url": "https://dummyurl.com/testscriptresource", + "fileName": "sample.jmx", + "fileType": "JMX_FILE", + "expireDateTime": "2021-12-05T16:43:46.072Z", + "validationStatus": "VALIDATION_SUCCESS" + }, + "userPropFileInfo": { + "url": "https://dummyurl.com/userpropresource", + "fileName": "user.properties", + "fileType": "USER_PROPERTIES", + "expireDateTime": "2021-12-05T16:43:46.072Z", + "validationStatus": "" + }, + "inputArtifactsZipFileInfo": { + "url": "https://dummyurl.com/inputartifactzipresource", + "fileName": "inputartifacts.zip", + "fileType": "ADDITIONAL_ARTIFACTS", + "expireDateTime": "2021-12-05T16:43:46.072Z", + "validationStatus": "" + }, + "additionalFileInfo": [] + }, + "secrets": { + "secret1": { + "value": "https://samplevault.vault.azure.net/secrets/samplesecret/f113f91fd4c44a368049849c164db827", + "type": "AKV_SECRET_URI" + } + }, + "environmentVariables": { + "envvar1": "sampletext" + }, + "subnetId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/samplerg/providers/Microsoft.Network/virtualNetworks/samplenetworkresource/subnets/AAAAA0A0A0", + "keyvaultReferenceIdentityType": "UserAssigned", + "keyvaultReferenceIdentityId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/samplerg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/identity1" + } + expect(getAllFileErrors(testObj)).toStrictEqual({}); + }); + + test('Test object with file validation errors', () => { + // https://learn.microsoft.com/en-us/rest/api/loadtesting/dataplane/load-test-administration/get-test?view=rest-loadtesting-dataplane-2022-11-01&tabs=HTTP + let testObj = { + "testId": "12345678-1234-1234-1234-123456789012", + "description": "sample description", + "displayName": "Performance_LoadTest", + "loadTestConfiguration": { + "engineInstances": 6, + "splitAllCSVs": true + }, + "passFailCriteria": { + "passFailMetrics": { + "fefd759d-7fe8-4f83-8b6d-aeebe0f491fe": { + "clientMetric": "response_time_ms", + "aggregate": "percentage", + "condition": ">", + "value": 20, + "action": "continue", + "actualValue": 0, + "result": null + } + } + }, + "createdDateTime": "2021-12-05T16:43:46.072Z", + "createdBy": "user@contoso.com", + "lastModifiedDateTime": "2021-12-05T16:43:46.072Z", + "lastModifiedBy": "user@contoso.com", + "inputArtifacts": { + "testScriptFileInfo": { + "url": "https://dummyurl.com/testscriptresource", + "fileName": "sample.jmx", + "fileType": "JMX_FILE", + "expireDateTime": "2021-12-05T16:43:46.072Z", + "validationStatus": "VALIDATION_SUCCESS" + }, + "userPropFileInfo": { + "url": "https://dummyurl.com/userpropresource", + "fileName": "user.properties", + "fileType": "USER_PROPERTIES", + "expireDateTime": "2021-12-05T16:43:46.072Z", + "validationStatus": "" + }, + "additionalFileInfo": [ + { + "url": "https://dummyurl.com/inputartifactzipresource", + "fileName": "inputartifacts.zip", + "fileType": "ZIPPED_ARTIFACTS", + "expireDateTime": "2021-12-05T16:43:46.072Z", + "validationStatus": "VALIDATION_FAILURE", + "validationFailureDetails": "Error in zip" + }, + { + "url": "https://dummyurl.com/inputartifactresource", + "fileName": "fragment.jmx", + "fileType": "ADDITIONAL_ARTIFACTS", + "expireDateTime": "2021-12-05T16:43:46.072Z", + "validationStatus": "VALIDATION_FAILURE", + "validationFailureDetails": "Error in fragment" + }, + ] + }, + "secrets": { + "secret1": { + "value": "https://samplevault.vault.azure.net/secrets/samplesecret/f113f91fd4c44a368049849c164db827", + "type": "AKV_SECRET_URI" + } + }, + "environmentVariables": { + "envvar1": "sampletext" + }, + "subnetId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/samplerg/providers/Microsoft.Network/virtualNetworks/samplenetworkresource/subnets/AAAAA0A0A0", + "keyvaultReferenceIdentityType": "UserAssigned", + "keyvaultReferenceIdentityId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/samplerg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/identity1" + } + expect(getAllFileErrors(testObj)).toStrictEqual({ + "inputartifacts.zip": "Error in zip", + "fragment.jmx": "Error in fragment" + }); + }) }) \ No newline at end of file