diff --git a/extensions/resource-deployment/package.json b/extensions/resource-deployment/package.json index 00d68fc773..3ce70b968f 100644 --- a/extensions/resource-deployment/package.json +++ b/extensions/resource-deployment/package.json @@ -492,7 +492,7 @@ "tags": ["SQL Server", "Cloud"], "providers": [ { - "azureSQLVMWizard":{ + "azureSQLVMWizard": { "notebook": "./notebooks/azurevm/create-sqlvm.ipynb" }, "requiredTools": [ @@ -534,11 +534,13 @@ "devDependencies": { "@types/mocha": "^5.2.5", "@types/semver": "^7.3.1", + "@types/sinon": "^9.0.8", "@types/yamljs": "0.2.30", "mocha": "^5.2.0", "mocha-junit-reporter": "^1.17.0", "mocha-multi-reporters": "^1.1.7", "should": "^13.2.3", + "sinon": "^9.2.0", "typemoq": "^2.1.0", "vscodetestcover": "^1.1.0" } diff --git a/extensions/resource-deployment/src/localizedConstants.ts b/extensions/resource-deployment/src/localizedConstants.ts index 2f530ad8c6..7d6b352233 100644 --- a/extensions/resource-deployment/src/localizedConstants.ts +++ b/extensions/resource-deployment/src/localizedConstants.ts @@ -38,7 +38,8 @@ export const acceptEulaAndSelect = localize('deploymentDialog.RecheckEulaButton' export const resourceTypePickerDialogTitle = localize('resourceTypePickerDialog.title', "Select the deployment options"); export const resourceTypeSearchBoxDescription = localize('resourceTypePickerDialog.resourceSearchPlaceholder', "Filter resources..."); -export const resoucrceTypeCategoryListViewTitle = localize('resourceTypePickerDialog.tagsListViewTitle', 'Categories'); +export const resourceTypeCategoryListViewTitle = localize('resourceTypePickerDialog.tagsListViewTitle', "Categories"); +export const multipleValidationErrors = localize("validation.multipleValidationErrors", "There are some errors on this page, click 'Show Details' to view the errors."); export const scriptToNotebook = localize('ui.ScriptToNotebookButton', "Script"); export const deployNotebook = localize('ui.DeployButton', "Run"); diff --git a/extensions/resource-deployment/src/test/ui/validation/validations.test.ts b/extensions/resource-deployment/src/test/ui/validation/validations.test.ts new file mode 100644 index 0000000000..0c1d19aab4 --- /dev/null +++ b/extensions/resource-deployment/src/test/ui/validation/validations.test.ts @@ -0,0 +1,223 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import 'mocha'; +import * as should from 'should'; +import * as sinon from 'sinon'; +import { createValidation, GreaterThanOrEqualsValidation, IntegerValidation, LessThanOrEqualsValidation, RegexValidation, validateInputBoxComponent, Validation, ValidationType, ValidationValueType } from '../../../ui/validation/validations'; + +const inputBox = { + updateProperty(key: string, value: any) {} +}; +let inputBoxStub: sinon.SinonStub; +const testValidations = [ + { + type: ValidationType.IsInteger, + description: 'field was not an integer' + }, + { + type: ValidationType.Regex, + description: 'field must contain only alphabetic characters', + regex: '^[a-z]+$' + }, + { + type: ValidationType.LessThanOrEqualsTo, + description: 'field value must be <= field2\'s value', + target: 'field2' + }, + { + type: ValidationType.GreaterThanOrEqualsTo, + description: 'field value must be >= field1\'s value', + target: 'field1' + } +]; + +suite('Validation', () => { + suite('createValidation and validate input Box', () => { + setup(() => { + sinon.restore(); //cleanup all previously defined sinon mocks + inputBoxStub = sinon.stub(inputBox, 'updateProperty' ).resolves(); + }); + testValidations.forEach(testObj => { + test(`validationType: ${testObj.type}`, async () => { + const validation = createValidation(testObj, async () => undefined, async (_varName: string) => undefined); + switch(testObj.type) { + case ValidationType.IsInteger: should(validation).be.instanceOf(IntegerValidation); break; + case ValidationType.Regex: should(validation).be.instanceOf(RegexValidation); break; + case ValidationType.LessThanOrEqualsTo: should(validation).be.instanceOf(LessThanOrEqualsValidation); break; + case ValidationType.GreaterThanOrEqualsTo: should(validation).be.instanceOf(GreaterThanOrEqualsValidation); break; + default: console.log(`unexpected validation type: ${testObj.type}`); break; + } + should(await validateInputBoxComponent(inputBox, [validation])).be.false(); + should(inputBoxStub.calledOnce).be.true(); + should(inputBoxStub.getCall(0).args[0]).equal('validationErrorMessage'); + should(inputBoxStub.getCall(0).args[1]).equal(testObj.description); + }); + }); + }); + + suite('IntegerValidation', () => { + // all the below test values are arbitrary representative values or sentinel values for integer validation + [ + { value: '342520596781', expected: true }, + { value: 342520596781, expected: true }, + { value: '3.14', expected: false }, + { value: 3.14, expected: false }, + { value: '3.14e2', expected: true }, + { value: 3.14e2, expected: true }, + { value: undefined, expected: false }, + { value: NaN, expected: false }, + ].forEach((testObj) => { + const displayTestValue = getDisplayString(testObj.value); + test(`testValue:${displayTestValue}`, async () => { + const validationDescription = `value: ${displayTestValue} was not an integer`; + const validation = new IntegerValidation( + { type: ValidationType.IsInteger, description: validationDescription }, + async () => testObj.value + ); + await testValidation(validation, testObj, validationDescription); + }); + }); + }); + + suite('RegexValidation', () => { + const testRegex = '^[0-9]+$'; + // tests + [ + { value: '3425205616179816', expected: true }, + { value: 3425205616179816, expected: true }, + { value: '3.14', expected: false }, + { value: 3.14, expected: false }, + { value: '3.14e2', expected: false }, + { value: 3.14e2, expected: true }, // value of 3.14e2 literal is 342 which in string matches the testRegex + { value: 'arbitraryString', expected: false }, + { value: undefined, expected: false }, + ].forEach(testOb => { + const displayTestValue = getDisplayString(testOb.value); + test(`regex: /${testRegex}/, testValue:${displayTestValue}, expect result: ${testOb.expected}`, async () => { + const validationDescription = `value:${displayTestValue} did not match the regex:/${testRegex}/`; + const validation = new RegexValidation( + { type: ValidationType.IsInteger, description: validationDescription, regex: testRegex }, + async () => testOb.value + ); + await testValidation(validation, testOb, validationDescription); + }); + }); + }); + + suite('LessThanOrEqualsValidation', () => { + const targetVariableName = 'comparisonTarget'; + // tests - when operands are mix of string and number then number comparison is performed + [ + // integer values + { value: '342', targetValue: '42', expected: true }, + + { value: 342, targetValue: '42', expected: false }, + { value: '342', targetValue: 42, expected: false }, + + { value: 42, targetValue: '342', expected: true }, + { value: '42', targetValue: 342, expected: true }, + { value: 42, targetValue: '42', expected: true }, + + { value: 342, targetValue: 42, expected: false }, + + // floating pt values + { value: '342.15e-1', targetValue: '42.15e-1', expected: true }, + { value: 342.15e-1, targetValue: '42.15e-1', expected: false }, + { value: '342.15e-1', targetValue: 42.15e-1, expected: false }, + { value: 342.15e-1, targetValue: 42.15e-1, expected: false }, + + // equal values + { value: '342.15', targetValue: '342.15', expected: true }, + { value: 342.15, targetValue: '342.15', expected: true }, + { value: '342.15', targetValue: 342.15, expected: true }, + { value: 342.15, targetValue: 342.15, expected: true }, + + + // undefined values - if one operand is undefined result is always false + { value: undefined, targetValue: '42', expected: false }, + { value: undefined, targetValue: 42, expected: false }, + { value: '42', targetValue: undefined, expected: false }, + { value: 42, targetValue: undefined, expected: false }, + { value: undefined, targetValue: undefined, expected: false }, + + ].forEach(testObj => { + const displayTestValue = getDisplayString(testObj.value); + const displayTargetValue = getDisplayString(testObj.targetValue); + test(`testValue:${displayTestValue}, targetValue:${displayTargetValue}`, async () => { + const validationDescription = `${displayTestValue} did not test as <= ${displayTargetValue}`; + const validation = new LessThanOrEqualsValidation( + { type: ValidationType.IsInteger, description: validationDescription, target: targetVariableName }, + async () => testObj.value, + async (_variableName: string) => testObj.targetValue + ); + await testValidation(validation, testObj, validationDescription); + }); + }); + }); + + suite('GreaterThanOrEqualsValidation', () => { + const targetVariableName = 'comparisonTarget'; + // tests - when operands are mix of string and number then number comparison is performed + [ + // integer values + { value: '342', targetValue: '42', expected: false }, + { value: 342, targetValue: '42', expected: true }, + { value: '342', targetValue: 42, expected: true }, + { value: 342, targetValue: 42, expected: true }, + + // floating pt values + { value: '342.15e-1', targetValue: '42.15e-1', expected: false }, + { value: 342.15e-1, targetValue: '42.15e-1', expected: true }, + { value: '342.15e-1', targetValue: 42.15e-1, expected: true }, + { value: 342.15e-1, targetValue: 42.15e-1, expected: true }, + + // equal values + { value: '342.15', targetValue: '342.15', expected: true }, + { value: 342.15, targetValue: '342.15', expected: true }, + { value: '342.15', targetValue: 342.15, expected: true }, + { value: 342.15, targetValue: 342.15, expected: true }, + + // undefined values - if one operand is undefined result is always false + { value: undefined, targetValue: '42', expected: false }, + { value: undefined, targetValue: 42, expected: false }, + { value: '42', targetValue: undefined, expected: false }, + { value: 42, targetValue: undefined, expected: false }, + { value: undefined, targetValue: undefined, expected: false }, + ].forEach(testObj => { + const displayTestValue = getDisplayString(testObj.value); + const displayTargetValue = getDisplayString(testObj.targetValue); + test(`testValue:${displayTestValue}, targetValue:${displayTargetValue}`, async () => { + const validationDescription = `${displayTestValue} did not test as >= ${displayTargetValue}`; + const validation = new GreaterThanOrEqualsValidation( + { type: ValidationType.IsInteger, description: validationDescription, target: targetVariableName }, + async () => testObj.value, + async (_variableName: string) => testObj.targetValue + ); + await testValidation(validation, testObj, validationDescription); + }); + }); + }); +}); + +interface TestObject { + value: ValidationValueType; + targetValue?: ValidationValueType; + expected: boolean; +} + +async function testValidation(validation: Validation, test: TestObject, validationDescription: string) { + const validationResult = await validation.validate(); + should(validationResult.valid).be.equal(test.expected, validationDescription); + validationResult.valid + ? should(validationResult.message).be.undefined() + : should(validationResult.message).be.equal(validationDescription); +} + +function getDisplayString(value: ValidationValueType) { + return typeof value === 'string' ? `"${value}"` : value; +} + diff --git a/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts b/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts index 81470d4d5d..7747606cad 100644 --- a/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts +++ b/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts @@ -218,7 +218,7 @@ export class ResourceTypePickerDialog extends DialogBase { }); const listView = this._view.modelBuilder.listView().withProps({ title: { - text: loc.resoucrceTypeCategoryListViewTitle + text: loc.resourceTypeCategoryListViewTitle }, CSSStyles: { 'width': '140px', diff --git a/extensions/resource-deployment/src/ui/validation/validations.ts b/extensions/resource-deployment/src/ui/validation/validations.ts new file mode 100644 index 0000000000..7fbf18ca7a --- /dev/null +++ b/extensions/resource-deployment/src/ui/validation/validations.ts @@ -0,0 +1,169 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import { throwUnless } from '../../common/utils'; + +export interface ValidationResult { + valid: boolean; + message?: string; +} + +export type Validator = () => Promise; +export type ValidationValueType = string | number | undefined; + +export type VariableValueGetter = (variable: string) => Promise; +export type ValueGetter = () => Promise; + +export const enum ValidationType { + IsInteger = 'is_integer', + Regex = 'regex_match', + LessThanOrEqualsTo = '<=', + GreaterThanOrEqualsTo = '>=' +} + +export type ValidationInfo = RegexValidationInfo | IntegerValidationInfo | ComparisonValidationInfo; + +export interface ValidationInfoBase { + readonly type: ValidationType, + readonly description: string, +} + +export type IntegerValidationInfo = ValidationInfoBase; + +export interface RegexValidationInfo extends ValidationInfoBase { + readonly regex: string | RegExp +} + +export interface ComparisonValidationInfo extends ValidationInfoBase { + readonly target: string +} + +export abstract class Validation { + private _description: string; + protected readonly _target?: string; + + get target(): string | undefined { + return this._target; + } + get description(): string { + return this._description; + } + + // gets the validation result for this validation object + abstract validate(): Promise; + + protected getValue(): Promise { + return this._valueGetter(); + } + + protected getVariableValue(variable: string): Promise { + return this._variableValueGetter!(variable); + } + + constructor(validation: ValidationInfo, protected _valueGetter: ValueGetter, protected _variableValueGetter?: VariableValueGetter) { + this._description = validation.description; + } +} + +export class IntegerValidation extends Validation { + constructor(validation: IntegerValidationInfo, valueGetter: ValueGetter) { + super(validation, valueGetter); + } + + private async isInteger(): Promise { + const value = await this.getValue(); + return (typeof value === 'string') ? Number.isInteger(parseFloat(value)) : Number.isInteger(value); + } + + async validate(): Promise { + const isValid = await this.isInteger(); + return { + valid: isValid, + message: isValid ? undefined: this.description + }; + } +} + +export class RegexValidation extends Validation { + private _regex: RegExp; + + get regex(): RegExp { + return this._regex; + } + + constructor(validation: RegexValidationInfo, valueGetter: ValueGetter) { + super(validation, valueGetter); + throwUnless(validation.regex !== undefined); + this._regex = (typeof validation.regex === 'string') ? new RegExp(validation.regex) : validation.regex; + } + + async validate(): Promise { + const value = await this.getValue(); + const isValid = value === undefined + ? false + : this.regex.test(value.toString()); + return { + valid: isValid, + message: isValid ? undefined: this.description + }; + } +} + +export abstract class Comparison extends Validation { + protected _target: string; // comparison object require a target so override the base optional setting. + + get target(): string { + return this._target; + } + + constructor(validation: ComparisonValidationInfo, valueGetter: ValueGetter, variableValueGetter: VariableValueGetter) { + super(validation, valueGetter, variableValueGetter); + throwUnless(validation.target !== undefined); + this._target = validation.target; + } + + abstract isComparisonSuccessful(): Promise; + async validate(): Promise { + const isValid = await this.isComparisonSuccessful(); + return { + valid: isValid, + message: isValid ? undefined: this.description + }; + } +} + +export class LessThanOrEqualsValidation extends Comparison { + async isComparisonSuccessful() { + return (await this.getValue())! <= ((await this.getVariableValue(this.target))!); + } +} + +export class GreaterThanOrEqualsValidation extends Comparison { + async isComparisonSuccessful() { + return (await this.getValue())! >= ((await this.getVariableValue(this.target))!); + } +} + +export function createValidation(validation: ValidationInfo, valueGetter: ValueGetter, variableValueGetter?: VariableValueGetter): Validation { + switch (validation.type) { + case ValidationType.Regex: return new RegexValidation(validation, valueGetter); + case ValidationType.IsInteger: return new IntegerValidation(validation, valueGetter); + case ValidationType.LessThanOrEqualsTo: return new LessThanOrEqualsValidation(validation, valueGetter, variableValueGetter!); + case ValidationType.GreaterThanOrEqualsTo: return new GreaterThanOrEqualsValidation(validation, valueGetter, variableValueGetter!); + default: throw new Error(`unknown validation type:${validation.type}`); //dev error + } +} + +export async function validateInputBoxComponent(component: azdata.InputBoxComponent, validations: Validation[] = []): Promise { + for (const validation of validations) { + const result = await validation.validate(); + if (!result.valid) { + component.updateProperty('validationErrorMessage', result.message); + return false; + } + } + return true; +} diff --git a/extensions/resource-deployment/yarn.lock b/extensions/resource-deployment/yarn.lock index 4e6d54ff71..b89dce2527 100644 --- a/extensions/resource-deployment/yarn.lock +++ b/extensions/resource-deployment/yarn.lock @@ -189,6 +189,42 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== +"@sinonjs/commons@^1", "@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.8.1": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.1.tgz#e7df00f98a203324f6dc7cc606cad9d4a8ab2217" + integrity sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^6.0.0", "@sinonjs/fake-timers@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40" + integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA== + dependencies: + "@sinonjs/commons" "^1.7.0" + +"@sinonjs/formatio@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-5.0.1.tgz#f13e713cb3313b1ab965901b01b0828ea6b77089" + integrity sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ== + dependencies: + "@sinonjs/commons" "^1" + "@sinonjs/samsam" "^5.0.2" + +"@sinonjs/samsam@^5.0.2", "@sinonjs/samsam@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-5.2.0.tgz#fcff83ab86f83b5498f4a967869c079408d9b5eb" + integrity sha512-CaIcyX5cDsjcW/ab7HposFWzV1kC++4HNsfnEdFJa7cP1QIuILAKV+BgfeqRXhcnSAc76r/Rh/O5C+300BwUIw== + dependencies: + "@sinonjs/commons" "^1.6.0" + lodash.get "^4.4.2" + type-detect "^4.0.8" + +"@sinonjs/text-encoding@^0.7.1": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" + integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== + "@types/mocha@^5.2.5": version "5.2.7" resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea" @@ -199,6 +235,18 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.4.tgz#43d7168fec6fa0988bb1a513a697b29296721afb" integrity sha512-+nVsLKlcUCeMzD2ufHEYuJ9a2ovstb6Dp52A5VsoKxDXgvE051XgHI/33I1EymwkRGQkwnA0LkhnUzituGs4EQ== +"@types/sinon@^9.0.8": + version "9.0.8" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.8.tgz#1ed0038d356784f75b086104ef83bfd4130bb81b" + integrity sha512-IVnI820FZFMGI+u1R+2VdRaD/82YIQTdqLYC9DLPszZuynAJDtCvCtCs3bmyL66s7FqRM3+LPX7DhHnVTaagDw== + dependencies: + "@types/sinonjs__fake-timers" "*" + +"@types/sinonjs__fake-timers@*": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.2.tgz#3a84cf5ec3249439015e14049bd3161419bf9eae" + integrity sha512-dIPoZ3g5gcx9zZEszaxLSVTvMReD3xxyyDnQUjA6IYDG9Ba2AV0otMPs+77sG9ojB4Qr2N2Vk5RnKeuA0X/0bg== + "@types/yamljs@0.2.30": version "0.2.30" resolved "https://registry.yarnpkg.com/@types/yamljs/-/yamljs-0.2.30.tgz#d034e1d329e46e8d0f737c9a8db97f68f81b5382" @@ -353,6 +401,11 @@ diff@3.5.0: resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== +diff@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -452,6 +505,11 @@ is-buffer@~1.1.1: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + istanbul-lib-coverage@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" @@ -527,11 +585,21 @@ json5@^2.1.2: dependencies: minimist "^1.2.5" +just-extend@^4.0.2: + version "4.1.1" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.1.1.tgz#158f1fdb01f128c411dc8b286a7b4837b3545282" + integrity sha512-aWgeGFW67BP3e5181Ep1Fv2v8z//iBJfrvyTnq8wG86vEESwmonn1zPBJ0VfmT9CJq2FIT0VsETtrNFm2a+SHA== + linux-release-info@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/linux-release-info/-/linux-release-info-2.0.0.tgz#bdd4743a3ac49151ce612dbfe063f5eec116aeab" integrity sha512-w0RoUAZOQvnwuypQT+kwiDRNrMrEyrNWC8OOQH4e0Chii/BqB2tq7yeUHKo9brPDymY0Iz7EKe0TtwsflAlOnA== +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= + lodash@^4.16.4, lodash@^4.17.13, lodash@^4.17.4: version "4.17.19" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" @@ -631,6 +699,17 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== +nise@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/nise/-/nise-4.0.4.tgz#d73dea3e5731e6561992b8f570be9e363c4512dd" + integrity sha512-bTTRUNlemx6deJa+ZyoCUTRvH3liK5+N6VQZ4NIw90AgDXY6iPnsqplNFf6STcj+ePk0H/xqxnP75Lr0J0Fq3A== + dependencies: + "@sinonjs/commons" "^1.7.0" + "@sinonjs/fake-timers" "^6.0.0" + "@sinonjs/text-encoding" "^0.7.1" + just-extend "^4.0.2" + path-to-regexp "^1.7.0" + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -648,6 +727,13 @@ path-parse@^1.0.6: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== +path-to-regexp@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + dependencies: + isarray "0.0.1" + pify@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" @@ -748,6 +834,19 @@ should@^13.2.3: should-type-adaptors "^1.0.1" should-util "^1.0.0" +sinon@^9.2.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-9.2.0.tgz#1d333967e30023609f7347351ebc0dc964c0f3c9" + integrity sha512-eSNXz1XMcGEMHw08NJXSyTHIu6qTCOiN8x9ODACmZpNQpr0aXTBXBnI4xTzQzR+TEpOmLiKowGf9flCuKIzsbw== + dependencies: + "@sinonjs/commons" "^1.8.1" + "@sinonjs/fake-timers" "^6.0.1" + "@sinonjs/formatio" "^5.0.1" + "@sinonjs/samsam" "^5.2.0" + diff "^4.0.2" + nise "^4.0.4" + supports-color "^7.1.0" + source-map@^0.5.0: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -806,6 +905,11 @@ to-fast-properties@^2.0.0: resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= +type-detect@4.0.8, type-detect@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + typemoq@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/typemoq/-/typemoq-2.1.0.tgz#4452ce360d92cf2a1a180f0c29de2803f87af1e8"