azure-sdk-for-node/test/framework/suite-base.js

719 строки
26 KiB
JavaScript

//
// Copyright (c) Microsoft and contributors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
var fs = require('fs');
var os = require('os');
var path = require('path');
var sinon = require('sinon');
var _ = require('underscore');
var util = require('util');
var uuid = require('uuid');
var msRest = require('ms-rest');
var msRestAzure = require('ms-rest-azure');
var FileTokenCache = require('../../lib/util/fileTokenCache');
var MockTokenCache = require('./mock-token-cache');
var nockHelper = require('./nock-helper');
var ResourceManagementClient = require('../../lib/services/resourceManagement/lib/resource/resourceManagementClient');
var async = require('async');
var adal = require('adal-node');
var DEFAULT_ADAL_CLIENT_ID = '04b07795-8ddb-461a-bbee-02f9e1bf7b46';
/**
* @class
* Initializes a new instance of the SuiteBase class.
* @constructor
*
* @param {object} mochaSuiteObject - The mocha suite object
*
* @param {string} testPrefix - The prefix to use for the test suite
*
* @param {Array} env - (Optional) Array of environment variables required by the test
* Example:
* [
* { requiresToken: true },
* { name: 'AZURE_ARM_TEST_LOCATION', defaultValue: 'West US' },
* { name: 'AZURE_AD_TEST_PASSWORD'},
* ];
*/
function SuiteBase(mochaSuiteObject, testPrefix, env) {
this.mochaSuiteObject = mochaSuiteObject;
this.testPrefix = this.normalizeTestName(testPrefix);
this.mockServerClient;
this.currentTest = '';
//Recording info
this.setRecordingsDirectory(__dirname + '/../recordings/' + this.testPrefix + '/');
this.suiteRecordingsFile = this.getRecordingsDirectory() + 'suite.' + this.testPrefix + '.nock.js';
this.recordingsFile = __dirname + '/../recordings/' + this.testPrefix + '.nock.js';
//test modes
this.isMocked = !process.env.NOCK_OFF;
this.isRecording = process.env.AZURE_NOCK_RECORD;
this.isPlayback = !process.env.NOCK_OFF && !process.env.AZURE_NOCK_RECORD;
//authentication info
this.subscriptionId = process.env['AZURE_SUBSCRIPTION_ID'] || 'subscription-id';
this.clientId = process.env['CLIENT_ID'] || DEFAULT_ADAL_CLIENT_ID;
this.domain = process.env['DOMAIN'] || 'domain';
this.username = process.env['AZURE_USERNAME'] || 'username@example.com';
this.password = process.env['AZURE_PASSWORD'] || 'dummypassword';
this.secret = process.env['APPLICATION_SECRET'] || 'dummysecret';
this.tokenCache = new adal.MemoryCache();
this._setCredentials();
//subscriptionId should be recorded for playback
if (!env) {
env = [];
}
env.push('AZURE_SUBSCRIPTION_ID');
// Normalize environment
this.normalizeEnvironment(env);
this.validateEnvironment();
//track & restore generated uuids to be used as part of request url, like a RBAC role assignment name
this.uuidsGenerated = [];
this.currentUuid = 0;
this.randomTestIdsGenerated = [];
this.numberOfRandomTestIdGenerated = 0;
this.mockVariables = {};
//stub necessary methods if in playback mode
this._stubMethods();
}
_.extend(SuiteBase.prototype, {
_setCredentials: function() {
if (!this.isPlayback) {
if (process.env['SKIP_CREDENTIAL_CHECK']) {
let token = process.env['AZURE_ACCESS_TOKEN'] || 'token';
this.credentials = new msRest.TokenCredentials('token');
} else if ((process.env['AZURE_PASSWORD'] && process.env['APPLICATION_SECRET']) ||
(!process.env['AZURE_PASSWORD'] && !process.env['APPLICATION_SECRET'])) {
throw new Error('You must either set the envt. variables \'AZURE_USERNAME\' ' +
'and \'AZURE_PASSWORD\' for running tests as a user or set the ' +
'envt. variable \'CLIENT_ID\' and \'APPLICATION_SECRET\' ' +
'for running tests as a service-principal, but not both.');
}
if (process.env['AZURE_PASSWORD']) {
this.credentials = this._createUserCredentials();
} else if (process.env['APPLICATION_SECRET']) {
this.credentials = this._createApplicationCredentials();
}
} else {
//The type of credential object does not matter in playback mode as authentication
//header is not recorded. Hence we always default to UsertokenCredentials.
this.credentials = this._createUserCredentials();
}
},
/**
* Creates the UserTokenCredentials object.
*
* @returns {ms-rest-azure.UserTokenCredentials} The user token credentials object.
*/
_createUserCredentials: function() {
if(process.env['AZURE_ENVIRONMENT'] && process.env['AZURE_ENVIRONMENT'].toUpperCase() === 'DOGFOOD') {
var df = {
name: 'Dogfood',
portalUrl: 'https://windows.azure-test.net/',
activeDirectoryEndpointUrl: 'https://login.windows-ppe.net/',
activeDirectoryResourceId: 'https://management.core.windows.net/',
managementEndpointUrl: 'https://management-preview.core.windows-int.net/',
resourceManagerEndpointUrl: 'https://api-dogfood.resources.windows-int.net/'
};
var env = msRestAzure.AzureEnvironment.add(df);
return new msRestAzure.UserTokenCredentials(this.clientId, this.domain, this.username,
this.password, { 'tokenCache': this.tokenCache, 'environment': env });
}
return new msRestAzure.UserTokenCredentials(this.clientId, this.domain, this.username,
this.password, { 'tokenCache': this.tokenCache });
},
/**
* Creates the ApplicationTokenCredentials object.
*
* @returns {ms-rest-azure.ApplicationTokenCredentials} The application token credentials object.
*/
_createApplicationCredentials: function() {
if(process.env['AZURE_ENVIRONMENT'] && process.env['AZURE_ENVIRONMENT'].toUpperCase() === 'DOGFOOD') {
var df = {
name: 'Dogfood',
portalUrl: 'https://windows.azure-test.net/',
activeDirectoryEndpointUrl: 'https://login.windows-ppe.net/',
activeDirectoryResourceId: 'https://management.core.windows.net/',
managementEndpointUrl: 'https://management-preview.core.windows-int.net/',
resourceManagerEndpointUrl: 'https://api-dogfood.resources.windows-int.net/'
};
var env = msRestAzure.AzureEnvironment.add(df);
return new msRestAzure.ApplicationTokenCredentials(this.clientId, this.domain, this.secret, {
'tokenCache': this.tokenCache,
'environment': env
});
}
return new msRestAzure.ApplicationTokenCredentials(this.clientId, this.domain, this.secret, {
'tokenCache': this.tokenCache
});
},
/**
* Creates a ResourceManagementClient and sets it as a property of the suite.
*/
_setupResourceManagementClient: function() {
if (!this.resourceManagement) {
this.resourceManagement = new ResourceManagementClient(this.credentials, this.subscriptionId);
}
if (this.isPlayback) {
this.resourceManagement.longRunningOperationRetryTimeout = 0;
}
},
/**
* Creates a new ResourceGroup with the specified name and location.
*
* @param {string} groupName - The resourcegroup name
* @param {string} location - The location
*
* @returns {function} callback(err, result) - It contains error and result for the create request.
*/
createResourcegroup: function(groupName, location, callback) {
console.log('Creating Resource Group: \'' + groupName + '\' at location: \'' + location + '\'');
this._setupResourceManagementClient();
return this.resourceManagement.resourceGroups.createOrUpdate(groupName, {
'location': location
}, callback);
},
/**
* Deletes the specified ResourceGroup.
*
* @param {string} groupName - The resourcegroup name
*
* @returns {function} callback(err, result) - It contains error and result for the delete request.
*/
deleteResourcegroup: function(groupName, callback) {
console.log('Deleting Resource Group: ' + groupName);
if (!this.resourceManagement) {
this._setupResourceManagementClient();
}
return this.resourceManagement.resourceGroups.deleteMethod(groupName, callback);
},
/**
* Provides the recordings directory for the test suite
*
* @returns {string} The test recordings directory
*/
getRecordingsDirectory: function() {
return this.recordingsDirectory;
},
/**
* Sets the recordings directory for the test suite
*
* @param {string} dir The test recordings directory
*/
setRecordingsDirectory: function(dir) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
this.recordingsDirectory = dir;
},
/**
* Provides the curent test recordings file
*
* @returns {string} The curent test recordings file
*/
getTestRecordingsFile: function() {
this.testRecordingsFile = this.getRecordingsDirectory() +
this.normalizeTestName(this.currentTest) + ".nock.js";
return this.testRecordingsFile;
},
normalizeTestName: function(str) {
return str.replace(/[{}\[\]'";\(\)#@~`!%&\^\$\+=,\/\\?<>\|\*:]/ig, '').replace(/(\s+)/ig, '_');
},
normalizeEnvironment: function(env) {
this.requiredEnvironment = env.map(function(env) {
if (typeof(env) === 'string') {
return {
name: env,
secure: false
};
} else {
return env;
}
});
},
validateEnvironment: function() {
if (this.isPlayback) {
return;
}
var messages = [];
var missing = [];
this.requiredEnvironment.forEach(function(e) {
if (!process.env[e.name] && !e.defaultValue) {
missing.push(e.name);
}
});
if (missing.length > 0) {
messages.push('This test requires the following environment variables which are not set: ' +
missing.join(', '));
}
if (messages.length > 0) {
throw new Error(messages.join(os.EOL));
}
},
setEnvironmentDefaults: function() {
this.requiredEnvironment.forEach(function(env) {
if (env.defaultValue && !process.env[env.name]) {
process.env[env.name] = env.defaultValue;
}
});
},
/**
* Provides the curent suite recordings file
*
* @returns {string} The curent suite recordings file
*/
getSuiteRecordingsFile: function() {
return this.suiteRecordingsFile;
},
/**
* Performs the specified actions before executing the suite. Records the random test ids and uuids generated during the
* suite setup and restores them in playback
*
* @param {function} callback A hook to provide the steps to execute during setup suite
*/
setupSuite: function(callback, isAsyncSetUp) {
if (this.isMocked) {
process.env.AZURE_ENABLE_STRICT_SSL = false;
}
if (this.isPlayback) {
// retrive suite level recorded testids and uuids if any
var nocked = require(this.getSuiteRecordingsFile());
if (nocked.randomTestIdsGenerated) {
this.randomTestIdsGenerated = nocked.randomTestIdsGenerated();
}
if (nocked.uuidsGenerated) {
this.uuidsGenerated = nocked.uuidsGenerated();
}
if (nocked.mockVariables) {
this.mockVariables = nocked.mockVariables();
}
if (nocked.setEnvironment) {
nocked.setEnvironment();
}
this.subscriptionId = process.env['AZURE_SUBSCRIPTION_ID'];
this.originalTokenCache = this.tokenCache;
this.tokenCache = new MockTokenCache();
} else {
this.setEnvironmentDefaults();
}
var self = this;
async.series([
function(firstCallback) {
if (isAsyncSetUp) {
callback(function() {
firstCallback(null);
});
} else {
callback();
firstCallback(null);
}
},
function(secondCallback) {
//write the suite level testids and uuids to a suite recordings file
if (self.isMocked && self.isRecording) {
self.writeRecordingHeader(self.getSuiteRecordingsFile());
fs.appendFileSync(self.getSuiteRecordingsFile(), '];\n');
self.writeGeneratedUuids(self.getSuiteRecordingsFile());
self.writeGeneratedRandomTestIds(self.getSuiteRecordingsFile());
self.writeMockVariables(self.getSuiteRecordingsFile());
}
secondCallback(null);
}
],
function(err, results) {
if (err) {
throw err;
}
});
},
/**
* Performs the specified async actions before executing the suite. Records the random test ids and uuids generated during the
* suite setup and restores them in playback
*
* @param {function} callback A hook to provide the steps to execute during setup suite
*/
setupSuiteAsync: function(callback) {
this.setupSuite(callback, true);
},
/**
* Performs the specified actions after executing the suite.
*
* @param {function} callback A hook to provide the steps to execute after the suite has completed execution
*/
teardownSuite: function(callback) {
if (this.isMocked) {
delete process.env.AZURE_ENABLE_STRICT_SSL;
}
callback();
},
/**
* Performs the specified actions before executing the test. Restores the random test ids and uuids in
* playback mode. Creates a new recording file for every test.
*
* @param {function} callback A hook to provide the steps to execute before the test starts execution
*/
setupTest: function(callback) {
this.currentTest = this.mochaSuiteObject.currentTest.fullTitle();
this.numberOfRandomTestIdGenerated = 0;
this.currentUuid = 0;
nockHelper.nockHttp();
if (this.isMocked && this.isRecording) {
// nock recording
this.writeRecordingHeader();
nockHelper.nock.recorder.rec(true);
}
if (this.isPlayback) {
// nock playback
var nocked = require(this.getTestRecordingsFile());
if (nocked.randomTestIdsGenerated) {
this.randomTestIdsGenerated = nocked.randomTestIdsGenerated();
}
if (nocked.uuidsGenerated) {
this.uuidsGenerated = nocked.uuidsGenerated();
}
if (nocked.mockVariables) {
this.mockVariables = nocked.mockVariables();
}
if (nocked.setEnvironment) {
nocked.setEnvironment();
}
this.originalTokenCache = this.tokenCache;
this.tokenCache = new MockTokenCache();
if (nocked.scopes.length === 1) {
nocked.scopes[0].forEach(function(createScopeFunc) {
createScopeFunc(nockHelper.nock);
});
} else {
throw new Error('It appears the ' + this.getTestRecordingsFile() + ' file has more tests than there are mocked tests. ' +
'You may need to re-generate it.');
}
}
callback();
},
/**
* Performs the specified actions after executing the test. Writes the generated uuids and test ids during
* the test to the recorded file.
*
* @param {function} callback A hook to provide the steps to execute after the test has completed execution
*/
baseTeardownTest: function(callback) {
if (this.isMocked) {
if (this.isRecording) {
// play nock recording
var scope = '[';
var lineWritten;
nockHelper.nock.recorder.play().forEach(function(line) {
if (line.indexOf('nock') >= 0) {
// apply fixups of nock generated mocks
// do not filter on body as they usual have time related stamps
line = line.replace(/(\.post\('.*?')\s*,\s*"[^]+[^\\]"\)/, '.filteringRequestBody(function (path) { return \'*\';})\n$1, \'*\')');
line = line.replace(/(\.get\('.*?')\s*,\s*"[^]+[^\\]"\)/, '.filteringRequestBody(function (path) { return \'*\';})\n$1, \'*\')');
line = line.replace(/(\.put\('.*?')\s*,\s*"[^]+[^\\]"\)/, '.filteringRequestBody(function (path) { return \'*\';})\n$1, \'*\')');
line = line.replace(/(\.delete\('.*?')\s*,\s*"[^]+[^\\]"\)/, '.filteringRequestBody(function (path) { return \'*\';})\n$1, \'*\')');
line = line.replace(/(\.merge\('.*?')\s*,\s*"[^]+[^\\]"\)/, '.filteringRequestBody(function (path) { return \'*\';})\n$1, \'*\')');
line = line.replace(/(\.patch\('.*?')\s*,\s*"[^]+[^\\]"\)/, '.filteringRequestBody(function (path) { return \'*\';})\n$1, \'*\')');
// put deployment have a timestamp in the url
line = line.replace(/(\.put\('\/deployment-templates\/\d{8}T\d{6}')/,
'.filteringPath(/\\/deployment-templates\\/\\d{8}T\\d{6}/, \'/deployment-templates/timestamp\')\n.put(\'/deployment-templates/timestamp\'');
// Requests to logging service contain timestamps in url query params, filter them out too
line = line.replace(/(\.get\('.*\/microsoft.insights\/eventtypes\/management\/values\?api-version=[0-9-]+)[^)]+\)/,
'.filteringPath(function (path) { return path.slice(0, path.indexOf(\'&\')); })\n$1\')');
if (line.match(/\/oauth2\/token\//ig) === null &&
line.match(/login\.windows\.net/ig) === null &&
line.match(/login\.windows-ppe\.net/ig) === null &&
line.match(/login\.microsoftonline\.com/ig) === null &&
line.match(/login\.chinacloudapi\.cn/ig) === null &&
line.match(/login\.microsoftonline\.de/ig) === null) {
scope += (lineWritten ? ',\n' : '') + 'function (nock) { \n' +
'var result = ' + line + ' return result; }';
lineWritten = true;
}
}
});
scope += ']];';
fs.appendFileSync(this.getTestRecordingsFile(), scope);
this.writeGeneratedUuids();
this.writeGeneratedRandomTestIds();
this.writeMockVariables();
nockHelper.nock.recorder.clear();
} else {
//playback mode
this.tokenCache = this.originalTokenCache;
nockHelper.nock.cleanAll();
}
}
nockHelper.unNockHttp();
callback();
},
/**
* Writes the generated uuids to the specified file.
*
* @param {string} filename (Optional) The file name to which the uuids need to be added
* If the filename is not provided then it will get the current test recording file.
*/
writeGeneratedUuids: function(filename) {
if (this.uuidsGenerated.length > 0) {
var uuids = this.uuidsGenerated.map(function(uuid) {
return '\'' + uuid + '\'';
}).join(',');
var content = util.format('\n exports.uuidsGenerated = function() { return [%s];};', uuids);
filename = filename || this.getTestRecordingsFile();
fs.appendFileSync(filename, content);
this.uuidsGenerated.length = 0;
}
},
/**
* Writes the generated random test ids to the specified file.
*
* @param {string} filename (Optional) The file name to which the random test ids need to be added
* If the filename is not provided then it will get the current test recording file.
*/
writeGeneratedRandomTestIds: function(filename) {
if (this.randomTestIdsGenerated.length > 0) {
var ids = this.randomTestIdsGenerated.map(function(id) {
return '\'' + id + '\'';
}).join(',');
var content = util.format('\n exports.randomTestIdsGenerated = function() { return [%s];};', ids);
filename = filename || this.getTestRecordingsFile();
fs.appendFileSync(filename, content);
this.randomTestIdsGenerated.length = 0;
}
},
/**
* Writes the mock variables to the specified file
*
* @param {string} filename (Optional) The file name to which the mock variables need to be added
* If the filename is not provided then it will get the current test recording file.
*/
writeMockVariables: function(filename) {
if (this.mockVariables && Object.keys(this.mockVariables).length > 0) {
var mockVariablesObject = JSON.stringify(this.mockVariables);
var content = util.format('\n exports.mockVariables = function() { return %s; };', mockVariablesObject);
filename = filename || this.getTestRecordingsFile();
fs.appendFileSync(filename, content);
this.mockVariables = {};
}
},
/**
* Writes the recording header to the specified file.
*
* @param {string} filename (Optional) The file name to which the recording header needs to be added
* If the filename is not provided then it will get the current test recording file.
*/
writeRecordingHeader: function(filename) {
var template = fs.readFileSync(path.join(__dirname, 'preamble.template'), {
encoding: 'utf8'
});
filename = filename || this.getTestRecordingsFile();
let compiledTemplateFunction = _.template(template);
let data = compiledTemplateFunction({ requiredEnvironment: this.requiredEnvironment });
fs.writeFileSync(filename, data);
},
/**
* Generates an unique identifier using a prefix, based on a currentList and repeatable or not depending on the isMocked flag.
*
* @param {string} prefix The prefix to use in the identifier.
* @param {array} currentList The current list of identifiers.
* @return {string} A new unique identifier.
*/
generateId: function(prefix, currentList) {
if (!currentList) {
currentList = [];
}
var newNumber;
//record or live
if (!this.isPlayback) {
newNumber = this.generateRandomId(prefix, currentList);
//record
if (this.isMocked) {
this.randomTestIdsGenerated[this.numberOfRandomTestIdGenerated++] = newNumber;
}
} else {
//playback
if (this.randomTestIdsGenerated && this.randomTestIdsGenerated.length > 0) {
newNumber = this.randomTestIdsGenerated[this.numberOfRandomTestIdGenerated++];
} else {
//some test might not have recorded generated ids, so we fall back to the old sequential logic
newNumber = prefix + (currentList.length + 1);
}
}
currentList.push(newNumber);
return newNumber;
},
/**
* Generates a Guid. It will save the Guid to the recording file if in 'Record' mode or
* retrieve the guid from the recording file if in 'Playback' mode.
* @return {string} A new Guid.
*/
generateGuid: function() {
var newGuid;
//record or live
if (!this.isPlayback) {
newGuid = uuid.v4();
//record
if (this.isMocked) {
this.uuidsGenerated[this.currentUuid++] = newGuid;
}
} else {
//playback
if (this.uuidsGenerated && this.uuidsGenerated.length > 0) {
newGuid = this.uuidsGenerated[this.currentUuid++];
}
}
return newGuid;
},
/**
* Saves the mock variable with the specified name to the recording file when the test is run
* in 'Record' mode or keeps it in memory when the test is run in 'Live' mode.
*/
saveMockVariable: function(mockVariableName, mockVariable) {
//record or live
if (!this.isPlayback) {
this.mockVariables[mockVariableName] = mockVariable;
}
},
/**
* Gets the mock variable with the specified name. Returns undefined if the variable name is not present.
* @return {object} A mock variable.
*/
getMockVariable: function(mockVariableName) {
return this.mockVariables[mockVariableName];
},
/**
* A helper function to handle wrapping an existing method in sinon.
*
* @param {ojbect} sinonObj either sinon or a sinon sandbox instance
* @param {object} object The object containing the method to wrap
* @param {string} property property name of method to wrap
* @param {function (function)} setup function that receives the original function,
* returns new function that runs when method is called.
* @return {object} The created stub.
*/
wrap: function(sinonObj, object, property, setup) {
var original = object[property];
return sinonObj.stub(object, property, setup(original));
},
/**
* A helper function to generate a random id.
*
* @param {string} prefix A prefix for the generated random id
* @param {array} currentList The list that contains the generated random ids
* (This ensures that there are no duplicates in the list)
* @return {string} The generated random nmumber.
*/
generateRandomId: function(prefix, currentList) {
var newNumber;
while (true) {
newNumber = prefix + Math.floor(Math.random() * 10000);
if (!currentList || currentList.indexOf(newNumber) === -1) {
break;
}
}
return newNumber;
},
/**
* Stubs certain methods to make them work in playback mode.
*/
_stubMethods: function() {
if (this.isPlayback) {
if (msRestAzure.UserTokenCredentials.prototype.signRequest.restore) {
msRestAzure.UserTokenCredentials.prototype.signRequest.restore();
}
sinon.stub(msRestAzure.UserTokenCredentials.prototype, 'signRequest').callsFake(function(webResource, callback) {
return callback(null);
});
if (msRestAzure.ApplicationTokenCredentials.prototype.signRequest.restore) {
msRestAzure.ApplicationTokenCredentials.prototype.signRequest.restore();
}
sinon.stub(msRestAzure.ApplicationTokenCredentials.prototype, 'signRequest').callsFake(function(webResource, callback) {
return callback(null);
});
if (this.createResourcegroup.restore) {
this.createResourcegroup.restore();
}
sinon.stub(this, 'createResourcegroup').callsFake(function(groupName, location, callback) {
return callback(null);
});
if (this.deleteResourcegroup.restore) {
this.deleteResourcegroup.restore();
}
sinon.stub(this, 'deleteResourcegroup').callsFake(function(groupName, callback) {
return callback(null);
});
}
}
});
exports = module.exports = SuiteBase;