diff --git a/sdk/keyvault/keyvault-certificates/karma.conf.js b/sdk/keyvault/keyvault-certificates/karma.conf.js new file mode 100644 index 000000000000..e13440815542 --- /dev/null +++ b/sdk/keyvault/keyvault-certificates/karma.conf.js @@ -0,0 +1,128 @@ +// https://github.com/karma-runner/karma-chrome-launcher +process.env.CHROME_BIN = require("puppeteer").executablePath(); +require("dotenv").config({ path: "../.env" }); + +module.exports = function(config) { + config.set({ + basePath: "./", + frameworks: ["mocha"], + + plugins: [ + "karma-mocha", + "karma-mocha-reporter", + "karma-chrome-launcher", + "karma-edge-launcher", + "karma-firefox-launcher", + "karma-ie-launcher", + "karma-env-preprocessor", + "karma-coverage", + "karma-remap-coverage", + "karma-junit-reporter", + "karma-json-to-file-reporter", + "karma-json-preprocessor" + ], + + files: [ + // polyfill service supporting IE11 missing features + // Promise,String.prototype.startsWith,String.prototype.endsWith,String.prototype.repeat,String.prototype.includes,Array.prototype.includes,Object.keys + "https://cdn.polyfill.io/v2/polyfill.js?features=Promise,String.prototype.startsWith,String.prototype.endsWith,String.prototype.repeat,String.prototype.includes,Array.prototype.includes,Object.keys|always", + "dist-test/index.browser.js", + "recordings/browsers/**/*.json" + ], + + exclude: [], + + preprocessors: { + "**/*.js": ["env"], + "dist-test/index.browser.js": ["coverage"], + "recordings/browsers/**/*.json": ["json"] + }, + + envPreprocessor: [ + "AZURE_CLIENT_ID", + "AZURE_CLIENT_SECRET", + "AZURE_TENANT_ID", + "KEYVAULT_NAME", + "TEST_MODE" + ], + + reporters: ["mocha", "coverage", "remap-coverage", "junit", "json-to-file"], + + coverageReporter: { type: "in-memory" }, + + remapCoverageReporter: { + "text-summary": null, + html: "./coverage-browser", + cobertura: "./coverage-browser/cobertura-coverage.xml" + }, + + remapOptions: { + exclude: /node_modules|tests/g + }, + + junitReporter: { + outputDir: "", + outputFile: "test-results.browser.xml", + suite: "", + useBrowserName: false, + nameFormatter: undefined, + classNameFormatter: undefined, + properties: {} + }, + + jsonToFileReporter: { + filter: function(obj) { + if (obj.writeFile) { + const fs = require("fs-extra"); + try { + // Stripping away the filename from the file path and retaining the directory structure + fs.ensureDirSync(obj.path.substring(0, obj.path.lastIndexOf("/") + 1)); + } catch (err) { + if (err.code !== "EEXIST") throw err; + } + fs.writeFile(obj.path, JSON.stringify(obj.content, null, " "), (err) => { + if (err) { + throw err; + } + }); + } + return false; + }, + outputPath: "." + }, + + port: 9328, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: false, + + // --no-sandbox allows our tests to run in Linux without having to change the system. + // --disable-web-security allows us to authenticate from the browser without having to write tests using interactive auth, which would be far more complex. + browsers: ["ChromeHeadlessNoSandbox"], + customLaunchers: { + ChromeHeadlessNoSandbox: { + base: "ChromeHeadless", + flags: ["--no-sandbox", "--disable-web-security"] + } + }, + + singleRun: false, + concurrency: 1, + + browserNoActivityTimeout: 600000, + browserDisconnectTimeout: 10000, + browserDisconnectTolerance: 3, + browserConsoleLogOptions: { + // IMPORTANT: COMMENT the following line if you want to print debug logs in your browsers in record mode!! + terminal: process.env.TEST_MODE !== "record" + }, + + client: { + mocha: { + // change Karma's debug.html to the mocha web reporter + reporter: "html", + timeout: "600000" + } + } + }); +}; diff --git a/sdk/keyvault/keyvault-certificates/package.json b/sdk/keyvault/keyvault-certificates/package.json index 59046ea04c32..9a7cef42396f 100644 --- a/sdk/keyvault/keyvault-certificates/package.json +++ b/sdk/keyvault/keyvault-certificates/package.json @@ -47,51 +47,92 @@ "build:samples": "tsc samples/helloWorld.ts", "build:es6": "tsc -p tsconfig.json", "build:nodebrowser": "rollup -c 2>&1", - "build:test": "echo skipped", + "build:test": "npm run build:es6 && rollup -c rollup.test.config.js 2>&1", "build": "npm run extract-api && npm run build:samples && npm run build:es6 && npm run build:nodebrowser", "check-format": "prettier --list-different --config ../../.prettierrc.json \"src/**/*.ts\" \"*.{js,json}\"", "clean": "rimraf dist esm test-dist typings *.tgz *.log", "extract-api": "tsc -p . && api-extractor run --local", "format": "prettier --write --config ../../.prettierrc.json \"src/**/*.ts\" \"*.{js,json}\"", - "integration-test:browser": "echo skipped", - "integration-test:node": "echo skipped", + "integration-test:browser": "karma start --single-run", + "integration-test:node": "nyc mocha --require source-map-support/register --reporter mocha-multi --timeout 1200000 --reporter-options spec=-,mocha-junit-reporter=- --full-trace dist-test/index.node.js", "integration-test": "npm run integration-test:node && npm run integration-test:browser", "lint:fix": "eslint \"src/**/*.ts\" -c ../../.eslintrc.json --fix --fix-type [problem,suggestion]", - "lint": "eslint -c ../../.eslintrc.json src --ext .ts -f html -o keyvault-certificates-lintReport.html || exit 0", + "lint": "eslint -c ../../.eslintrc.json src tests samples --ext .ts -f html -o keyvault-certificates-lintReport.html || exit 0", + "lint:terminal": "eslint -c ../../.eslintrc.json src tests samples --ext .ts", "pack": "npm pack 2>&1", "prebuild": "npm run clean", "test:browser": "npm run build:test && npm run unit-test:browser && npm run integration-test:browser", "test:node": "npm run build:test && npm run unit-test:node && npm run integration-test:node", "test": "npm run build:test && npm run unit-test && npm run integration-test", - "unit-test:browser": "echo skipped", - "unit-test:node": "echo skipped", + "unit-test:browser": "cross-env TEST_MODE=playback npm run integration-test:browser", + "unit-test:node": "cross-env TEST_MODE=playback npm run integration-test:node", "unit-test": "npm run unit-test:node && npm run unit-test:browser" }, "sideEffects": false, "dependencies": { - "@azure/core-paging": "1.0.0-preview.1", "@azure/core-arm": "1.0.0-preview.2", "@azure/core-http": "1.0.0-preview.2", + "@azure/core-paging": "1.0.0-preview.1", + "@azure/core-tracing": "1.0.0-preview.1", + "@azure/identity": "1.0.0-preview.2", "tslib": "^1.9.3" }, "devDependencies": { - "@azure/identity": "1.0.0-preview.2", "@microsoft/api-extractor": "^7.1.5", "@types/chai": "^4.1.6", + "@types/dotenv": "^6.1.0", + "@types/fs-extra": "^8.0.0", + "@types/mocha": "^5.2.5", + "@types/nise": "^1.4.0", + "@types/nock": "^10.0.1", "@types/node": "^8.0.0", + "@types/query-string": "6.2.0", "@typescript-eslint/eslint-plugin": "^2.0.0", "@typescript-eslint/parser": "^2.0.0", + "assert": "^1.4.1", "chai": "^4.2.0", + "cross-env": "^5.2.0", + "dotenv": "^8.0.0", "eslint": "^6.1.0", "eslint-config-prettier": "^6.0.0", "eslint-plugin-no-null": "^1.0.2", "eslint-plugin-no-only-tests": "^2.3.0", "eslint-plugin-promise": "^4.1.1", + "fs-extra": "^8.1.0", + "karma": "^4.0.1", + "karma-chrome-launcher": "^3.0.0", + "karma-coverage": "^1.1.2", + "karma-edge-launcher": "^0.4.2", + "karma-env-preprocessor": "^0.1.1", + "karma-firefox-launcher": "^1.1.0", + "karma-ie-launcher": "^1.0.0", + "karma-json-preprocessor": "^0.3.3", + "karma-json-to-file-reporter": "^1.0.1", + "karma-junit-reporter": "^1.2.0", + "karma-mocha": "^1.3.0", + "karma-mocha-reporter": "^2.2.5", + "karma-remap-coverage": "^0.1.5", + "mocha": "^5.2.0", + "mocha-junit-reporter": "^1.18.0", + "mocha-multi": "^1.0.1", + "nise": "^1.4.10", + "nock": "^10.0.6", + "nyc": "^14.0.0", "prettier": "^1.16.4", + "puppeteer": "^1.11.0", + "query-string": "^5.0.0", "rimraf": "^2.6.2", "rollup": "^1.16.3", + "rollup": "^1.16.3", "rollup-plugin-commonjs": "^10.0.0", + "rollup-plugin-multi-entry": "^2.1.0", "rollup-plugin-node-resolve": "^5.0.2", + "rollup-plugin-replace": "^2.1.0", + "rollup-plugin-shim": "^1.0.0", + "rollup-plugin-sourcemaps": "^0.4.2", + "rollup-plugin-terser": "^5.1.1", + "rollup-plugin-visualizer": "^2.0.0", + "source-map-support": "^0.5.9", "typescript": "^3.2.2", "uglify-js": "^3.4.9", "url": "^0.11.0" diff --git a/sdk/keyvault/keyvault-certificates/recordings/browsers/certificates_client__create_read_update_and_delete_operations/recording_can_create_a_certificate.json b/sdk/keyvault/keyvault-certificates/recordings/browsers/certificates_client__create_read_update_and_delete_operations/recording_can_create_a_certificate.json new file mode 100644 index 000000000000..aba891374f46 --- /dev/null +++ b/sdk/keyvault/keyvault-certificates/recordings/browsers/certificates_client__create_read_update_and_delete_operations/recording_can_create_a_certificate.json @@ -0,0 +1,400 @@ +{ + "recordings": [ + { + "method": "POST", + "url": "https://keyvault_name.vault.azure.net/certificates/recoverCertificateName-cancreateacertificate-/create", + "query": { + "api-version": "7.0" + }, + "requestBody": "", + "status": 401, + "response": "{\"error\":{\"code\":\"Unauthorized\",\"message\":\"Request is missing a Bearer or PoP token.\"}}", + "responseHeaders": { + "strict-transport-security": "max-age=31536000;includeSubDomains", + "www-authenticate": "Bearer authorization=\"https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47\", resource=\"https://vault.azure.net\"", + "x-ms-keyvault-network-info": "addr=40.121.43.168;act_addr_fam=InterNetwork;", + "x-aspnet-version": "4.0.30319", + "x-powered-by": "ASP.NET", + "status": "401", + "x-ms-keyvault-region": "westus", + "content-length": "87", + "pragma": "no-cache", + "server": "Microsoft-IIS/10.0", + "x-ms-keyvault-service-version": "1.1.0.875", + "date": "Tue, 13 Aug 2019 19:37:13 GMT", + "content-type": "application/json; charset=utf-8", + "x-ms-request-id": "36c10ec0-cdcf-4406-b174-fa82e70d4ff8", + "cache-control": "no-cache", + "x-content-type-options": "nosniff", + "expires": "-1" + } + }, + { + "method": "POST", + "url": "https://login.microsoftonline.com/azure_tenant_id/oauth2/v2.0/token", + "query": {}, + "requestBody": "response_type=token&grant_type=client_credentials&client_id=azure_client_id&client_secret=azure_client_secret&scope=https%3A%2F%2Fvault.azure.net%2F.default", + "status": 200, + "response": "{\"token_type\":\"Bearer\",\"expires_in\":3600,\"ext_expires_in\":3600,\"access_token\":\"access_token\"}", + "responseHeaders": { + "pragma": "no-cache", + "strict-transport-security": "max-age=31536000; includeSubDomains", + "x-content-type-options": "nosniff", + "date": "Tue, 13 Aug 2019 19:37:13 GMT", + "p3p": "CP=\"DSP CUR OTPi IND OTRi ONL FIN\"", + "x-ms-request-id": "1e8fa1dd-40d2-4ed9-957d-5531b6019400", + "cache-control": "no-cache, no-store", + "content-type": "application/json; charset=utf-8", + "content-length": "1231", + "x-ms-ests-server": "2.1.9228.13 - EST ProdSlices", + "referrer-policy": "strict-origin-when-cross-origin", + "expires": "-1" + } + }, + { + "method": "POST", + "url": "https://keyvault_name.vault.azure.net/certificates/recoverCertificateName-cancreateacertificate-/create", + "query": { + "api-version": "7.0" + }, + "requestBody": "{\"policy\":{\"x509_props\":{\"subject\":\"cn=MyCert\"},\"issuer\":{\"name\":\"Self\"}}}", + "status": 202, + "response": "{\"id\":\"https://keyvault_name.vault.azure.net/certificates/recoverCertificateName-cancreateacertificate-/pending\",\"issuer\":{\"name\":\"Self\"},\"csr\":\"MIICoTCCAYkCAQAwETEPMA0GA1UEAxMGTXlDZXJ0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsdPm69c0UNMuk7Anb5zANNZ7DuZMwhIFvSjhCWlhg5OaKja5MxLetIFRpJ8GGzBNpHXQoLb/gQYqx9Bq/PF2aJsBBrht6XCwZVQPAxEoHF7G25++eGdQUYT95JWgFJd66CzrYnFXKmSua1hdYlNhZalPQ2sV39CwU112mIq3Ejy87x+DuVX5FoXO1J8BNM/edDr19iOImoTBgMLSv9qUG7GrF7kz0TU+90HOpvoS6X6CXAMNe93xFZMNCyvtFDvSaQTTNB7NCGguLp8qGEBKnXzY1xqjSAOdlgr0Ij3B2JPBRC9G8cui8qD7dbBBc1iUk5lSQsenrMHwjqvB7B8JTwIDAQABoEswSQYJKoZIhvcNAQkOMTwwOjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEBAD/1IjNFYViCyiaBtAF2kXskY6VB2tU0PCmNYDAnWIrfdPGCkuIPr5bj8VjjMi0b1R81b/6kKMOsEkci//xIf9ClEFlWHAuUuUxyFSgztMWkARmlHxhEAKdslDpjQzecvjhV1iA2oPT8J7UC/xNIpbsTfxaLQ0A1HQXDmYI3JlAkdOAdSEdwqi+FL4sbkacV4lMCJ8NOw6ntb6+kLx3wyr9BG+A36ZEcpLyPsV1iLGh4w9MXvA3QBX80goHhDmyunn1USREpSBCpBDmGxgXvt14yDpE1/WV6eNFWsUGil+3XOXa9+69MsftbcZHVP0nLl1S/2cafPfTKnlT30NodicE=\",\"cancellation_requested\":false,\"status\":\"inProgress\",\"status_details\":\"Pending certificate created. Certificate request is in progress. This may take some time based on the issuer provider. Please check again later.\",\"request_id\":\"12201cad1be44d2c91c4a9eface4e3ed\"}", + "responseHeaders": { + "strict-transport-security": "max-age=31536000;includeSubDomains", + "x-content-type-options": "nosniff", + "x-ms-keyvault-network-info": "addr=40.121.43.168;act_addr_fam=InterNetwork;", + "x-aspnet-version": "4.0.30319", + "x-powered-by": "ASP.NET", + "status": "202", + "x-ms-keyvault-region": "westus", + "content-length": "1331", + "pragma": "no-cache", + "server": "Microsoft-IIS/10.0", + "x-ms-keyvault-service-version": "1.1.0.875", + "date": "Tue, 13 Aug 2019 19:37:14 GMT", + "content-type": "application/json; charset=utf-8", + "location": "https://danrodri-kv.vault.azure.net/certificates/recoverCertificateName-cancreateacertificate-32807286539187697/pending?api-version=7.0&request_id=12201cad1be44d2c91c4a9eface4e3ed", + "x-ms-request-id": "9bcf73ed-98ca-4328-b7b0-e6321d895567", + "cache-control": "no-cache", + "retry-after": "10", + "expires": "-1" + } + }, + { + "method": "DELETE", + "url": "https://keyvault_name.vault.azure.net/certificates/recoverCertificateName-cancreateacertificate-", + "query": { + "api-version": "7.0" + }, + "requestBody": "", + "status": 401, + "response": "{\"error\":{\"code\":\"Unauthorized\",\"message\":\"Request is missing a Bearer or PoP token.\"}}", + "responseHeaders": { + "strict-transport-security": "max-age=31536000;includeSubDomains", + "www-authenticate": "Bearer authorization=\"https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47\", resource=\"https://vault.azure.net\"", + "x-ms-keyvault-network-info": "addr=40.121.43.168;act_addr_fam=InterNetwork;", + "x-aspnet-version": "4.0.30319", + "x-powered-by": "ASP.NET", + "status": "401", + "x-ms-keyvault-region": "westus", + "content-length": "87", + "pragma": "no-cache", + "server": "Microsoft-IIS/10.0", + "x-ms-keyvault-service-version": "1.1.0.875", + "date": "Tue, 13 Aug 2019 19:37:14 GMT", + "content-type": "application/json; charset=utf-8", + "x-ms-request-id": "69d7e566-3ea1-4069-b44f-7f75278f95e8", + "cache-control": "no-cache", + "x-content-type-options": "nosniff", + "expires": "-1" + } + }, + { + "method": "POST", + "url": "https://login.microsoftonline.com/azure_tenant_id/oauth2/v2.0/token", + "query": {}, + "requestBody": "response_type=token&grant_type=client_credentials&client_id=azure_client_id&client_secret=azure_client_secret&scope=https%3A%2F%2Fvault.azure.net%2F.default", + "status": 200, + "response": "{\"token_type\":\"Bearer\",\"expires_in\":3600,\"ext_expires_in\":3600,\"access_token\":\"access_token\"}", + "responseHeaders": { + "pragma": "no-cache", + "strict-transport-security": "max-age=31536000; includeSubDomains", + "x-content-type-options": "nosniff", + "date": "Tue, 13 Aug 2019 19:37:14 GMT", + "p3p": "CP=\"DSP CUR OTPi IND OTRi ONL FIN\"", + "x-ms-request-id": "5382df34-f60d-46c0-a0fa-e0e4e17c9b00", + "cache-control": "no-cache, no-store", + "content-type": "application/json; charset=utf-8", + "content-length": "1231", + "x-ms-ests-server": "2.1.9228.13 - EST ProdSlices", + "referrer-policy": "strict-origin-when-cross-origin", + "expires": "-1" + } + }, + { + "method": "DELETE", + "url": "https://keyvault_name.vault.azure.net/certificates/recoverCertificateName-cancreateacertificate-", + "query": { + "api-version": "7.0" + }, + "requestBody": null, + "status": 200, + "response": "{\"recoveryId\":\"https://keyvault_name.vault.azure.net/deletedcertificates/recoverCertificateName-cancreateacertificate-\",\"deletedDate\":1565725035,\"scheduledPurgeDate\":1573501035,\"id\":\"https://keyvault_name.vault.azure.net/certificates/recoverCertificateName-cancreateacertificate-/1608acd6ebb44d49b808f97b81fae25b\",\"attributes\":{\"enabled\":false,\"nbf\":1565724434,\"exp\":1597347434,\"created\":1565725034,\"updated\":1565725034,\"recoveryLevel\":\"Recoverable+Purgeable\"},\"policy\":{\"id\":\"https://keyvault_name.vault.azure.net/certificates/recoverCertificateName-cancreateacertificate-/policy\",\"key_props\":{\"exportable\":true,\"kty\":\"RSA\",\"key_size\":2048,\"reuse_key\":false},\"secret_props\":{\"contentType\":\"application/x-pkcs12\"},\"x509_props\":{\"subject\":\"cn=MyCert\",\"ekus\":[\"1.3.6.1.5.5.7.3.1\",\"1.3.6.1.5.5.7.3.2\"],\"key_usage\":[\"digitalSignature\",\"keyEncipherment\"],\"validity_months\":12,\"basic_constraints\":{\"ca\":false}},\"lifetime_actions\":[{\"trigger\":{\"lifetime_percentage\":80},\"action\":{\"action_type\":\"AutoRenew\"}}],\"issuer\":{\"name\":\"Self\"},\"attributes\":{\"enabled\":true,\"created\":1565725034,\"updated\":1565725034}},\"pending\":{\"id\":\"https://keyvault_name.vault.azure.net/certificates/recoverCertificateName-cancreateacertificate-/pending\"}}", + "responseHeaders": { + "strict-transport-security": "max-age=31536000;includeSubDomains", + "x-content-type-options": "nosniff", + "x-ms-keyvault-network-info": "addr=40.121.43.168;act_addr_fam=InterNetwork;", + "x-aspnet-version": "4.0.30319", + "x-powered-by": "ASP.NET", + "status": "200", + "x-ms-keyvault-region": "westus", + "content-length": "1284", + "pragma": "no-cache", + "server": "Microsoft-IIS/10.0", + "x-ms-keyvault-service-version": "1.1.0.875", + "date": "Tue, 13 Aug 2019 19:37:14 GMT", + "content-type": "application/json; charset=utf-8", + "x-ms-request-id": "25ae7ae9-4af5-4041-b68b-fc89f4d4a5a0", + "cache-control": "no-cache", + "expires": "-1" + } + }, + { + "method": "DELETE", + "url": "https://keyvault_name.vault.azure.net/deletedcertificates/recoverCertificateName-cancreateacertificate-", + "query": { + "api-version": "7.0" + }, + "requestBody": "", + "status": 401, + "response": "{\"error\":{\"code\":\"Unauthorized\",\"message\":\"Request is missing a Bearer or PoP token.\"}}", + "responseHeaders": { + "strict-transport-security": "max-age=31536000;includeSubDomains", + "www-authenticate": "Bearer authorization=\"https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47\", resource=\"https://vault.azure.net\"", + "x-ms-keyvault-network-info": "addr=40.121.43.168;act_addr_fam=InterNetwork;", + "x-aspnet-version": "4.0.30319", + "x-powered-by": "ASP.NET", + "status": "401", + "x-ms-keyvault-region": "westus", + "content-length": "87", + "pragma": "no-cache", + "server": "Microsoft-IIS/10.0", + "x-ms-keyvault-service-version": "1.1.0.875", + "date": "Tue, 13 Aug 2019 19:37:14 GMT", + "content-type": "application/json; charset=utf-8", + "x-ms-request-id": "85b94637-dcd8-4324-8c0a-320d9fc05e4f", + "cache-control": "no-cache", + "x-content-type-options": "nosniff", + "expires": "-1" + } + }, + { + "method": "POST", + "url": "https://login.microsoftonline.com/azure_tenant_id/oauth2/v2.0/token", + "query": {}, + "requestBody": "response_type=token&grant_type=client_credentials&client_id=azure_client_id&client_secret=azure_client_secret&scope=https%3A%2F%2Fvault.azure.net%2F.default", + "status": 200, + "response": "{\"token_type\":\"Bearer\",\"expires_in\":3599,\"ext_expires_in\":3599,\"access_token\":\"access_token\"}", + "responseHeaders": { + "pragma": "no-cache", + "strict-transport-security": "max-age=31536000; includeSubDomains", + "x-content-type-options": "nosniff", + "date": "Tue, 13 Aug 2019 19:37:15 GMT", + "p3p": "CP=\"DSP CUR OTPi IND OTRi ONL FIN\"", + "x-ms-request-id": "1e7dadf6-782f-45ef-9852-597352c63f00", + "cache-control": "no-cache, no-store", + "content-type": "application/json; charset=utf-8", + "content-length": "1231", + "x-ms-ests-server": "2.1.9228.13 - EST ProdSlices", + "referrer-policy": "strict-origin-when-cross-origin", + "expires": "-1" + } + }, + { + "method": "DELETE", + "url": "https://keyvault_name.vault.azure.net/deletedcertificates/recoverCertificateName-cancreateacertificate-", + "query": { + "api-version": "7.0" + }, + "requestBody": null, + "status": 409, + "response": "{\"error\":{\"code\":\"Conflict\",\"message\":\"Certificate is currently being deleted.\",\"innererror\":{\"code\":\"ObjectIsBeingDeleted\"}}}", + "responseHeaders": { + "strict-transport-security": "max-age=31536000;includeSubDomains", + "x-content-type-options": "nosniff", + "x-ms-keyvault-network-info": "addr=40.121.43.168;act_addr_fam=InterNetwork;", + "x-aspnet-version": "4.0.30319", + "x-powered-by": "ASP.NET", + "status": "409", + "x-ms-keyvault-region": "westus", + "content-length": "126", + "pragma": "no-cache", + "server": "Microsoft-IIS/10.0", + "x-ms-keyvault-service-version": "1.1.0.875", + "date": "Tue, 13 Aug 2019 19:37:15 GMT", + "content-type": "application/json; charset=utf-8", + "x-ms-request-id": "68253122-b87a-48a0-9ade-4e79e7c15a7d", + "cache-control": "no-cache", + "expires": "-1" + } + }, + { + "method": "DELETE", + "url": "https://keyvault_name.vault.azure.net/deletedcertificates/recoverCertificateName-cancreateacertificate-", + "query": { + "api-version": "7.0" + }, + "requestBody": "", + "status": 401, + "response": "{\"error\":{\"code\":\"Unauthorized\",\"message\":\"Request is missing a Bearer or PoP token.\"}}", + "responseHeaders": { + "strict-transport-security": "max-age=31536000;includeSubDomains", + "www-authenticate": "Bearer authorization=\"https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47\", resource=\"https://vault.azure.net\"", + "x-ms-keyvault-network-info": "addr=40.121.43.168;act_addr_fam=InterNetwork;", + "x-aspnet-version": "4.0.30319", + "x-powered-by": "ASP.NET", + "status": "401", + "x-ms-keyvault-region": "westus", + "content-length": "87", + "pragma": "no-cache", + "server": "Microsoft-IIS/10.0", + "x-ms-keyvault-service-version": "1.1.0.875", + "date": "Tue, 13 Aug 2019 19:37:25 GMT", + "content-type": "application/json; charset=utf-8", + "x-ms-request-id": "a93143e1-b474-45fb-b808-f759686156b9", + "cache-control": "no-cache", + "x-content-type-options": "nosniff", + "expires": "-1" + } + }, + { + "method": "POST", + "url": "https://login.microsoftonline.com/azure_tenant_id/oauth2/v2.0/token", + "query": {}, + "requestBody": "response_type=token&grant_type=client_credentials&client_id=azure_client_id&client_secret=azure_client_secret&scope=https%3A%2F%2Fvault.azure.net%2F.default", + "status": 200, + "response": "{\"token_type\":\"Bearer\",\"expires_in\":3600,\"ext_expires_in\":3600,\"access_token\":\"access_token\"}", + "responseHeaders": { + "pragma": "no-cache", + "strict-transport-security": "max-age=31536000; includeSubDomains", + "x-content-type-options": "nosniff", + "date": "Tue, 13 Aug 2019 19:37:25 GMT", + "p3p": "CP=\"DSP CUR OTPi IND OTRi ONL FIN\"", + "x-ms-request-id": "9d120307-e7d2-47b1-9ccb-bdcac9269700", + "cache-control": "no-cache, no-store", + "content-type": "application/json; charset=utf-8", + "content-length": "1231", + "x-ms-ests-server": "2.1.9228.13 - EST ProdSlices", + "referrer-policy": "strict-origin-when-cross-origin", + "expires": "-1" + } + }, + { + "method": "DELETE", + "url": "https://keyvault_name.vault.azure.net/deletedcertificates/recoverCertificateName-cancreateacertificate-", + "query": { + "api-version": "7.0" + }, + "requestBody": null, + "status": 409, + "response": "{\"error\":{\"code\":\"Conflict\",\"message\":\"Certificate is currently being deleted.\",\"innererror\":{\"code\":\"ObjectIsBeingDeleted\"}}}", + "responseHeaders": { + "strict-transport-security": "max-age=31536000;includeSubDomains", + "x-content-type-options": "nosniff", + "x-ms-keyvault-network-info": "addr=40.121.43.168;act_addr_fam=InterNetwork;", + "x-aspnet-version": "4.0.30319", + "x-powered-by": "ASP.NET", + "status": "409", + "x-ms-keyvault-region": "westus", + "content-length": "126", + "pragma": "no-cache", + "server": "Microsoft-IIS/10.0", + "x-ms-keyvault-service-version": "1.1.0.875", + "date": "Tue, 13 Aug 2019 19:37:26 GMT", + "content-type": "application/json; charset=utf-8", + "x-ms-request-id": "37c4caa8-96d2-45cd-965b-e510b98a6351", + "cache-control": "no-cache", + "expires": "-1" + } + }, + { + "method": "DELETE", + "url": "https://keyvault_name.vault.azure.net/deletedcertificates/recoverCertificateName-cancreateacertificate-", + "query": { + "api-version": "7.0" + }, + "requestBody": "", + "status": 401, + "response": "{\"error\":{\"code\":\"Unauthorized\",\"message\":\"Request is missing a Bearer or PoP token.\"}}", + "responseHeaders": { + "strict-transport-security": "max-age=31536000;includeSubDomains", + "www-authenticate": "Bearer authorization=\"https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47\", resource=\"https://vault.azure.net\"", + "x-ms-keyvault-network-info": "addr=40.121.43.168;act_addr_fam=InterNetwork;", + "x-aspnet-version": "4.0.30319", + "x-powered-by": "ASP.NET", + "status": "401", + "x-ms-keyvault-region": "westus", + "content-length": "87", + "pragma": "no-cache", + "server": "Microsoft-IIS/10.0", + "x-ms-keyvault-service-version": "1.1.0.875", + "date": "Tue, 13 Aug 2019 19:37:35 GMT", + "content-type": "application/json; charset=utf-8", + "x-ms-request-id": "9078e8b3-30fb-4ac2-a226-e2973a732995", + "cache-control": "no-cache", + "x-content-type-options": "nosniff", + "expires": "-1" + } + }, + { + "method": "POST", + "url": "https://login.microsoftonline.com/azure_tenant_id/oauth2/v2.0/token", + "query": {}, + "requestBody": "response_type=token&grant_type=client_credentials&client_id=azure_client_id&client_secret=azure_client_secret&scope=https%3A%2F%2Fvault.azure.net%2F.default", + "status": 200, + "response": "{\"token_type\":\"Bearer\",\"expires_in\":3600,\"ext_expires_in\":3600,\"access_token\":\"access_token\"}", + "responseHeaders": { + "pragma": "no-cache", + "strict-transport-security": "max-age=31536000; includeSubDomains", + "x-content-type-options": "nosniff", + "date": "Tue, 13 Aug 2019 19:37:36 GMT", + "p3p": "CP=\"DSP CUR OTPi IND OTRi ONL FIN\"", + "x-ms-request-id": "810ce07d-7c17-4780-b5f1-5d559408a600", + "cache-control": "no-cache, no-store", + "content-type": "application/json; charset=utf-8", + "content-length": "1231", + "x-ms-ests-server": "2.1.9228.13 - EST ProdSlices", + "referrer-policy": "strict-origin-when-cross-origin", + "expires": "-1" + } + }, + { + "method": "DELETE", + "url": "https://keyvault_name.vault.azure.net/deletedcertificates/recoverCertificateName-cancreateacertificate-", + "query": { + "api-version": "7.0" + }, + "requestBody": null, + "status": 204, + "response": "", + "responseHeaders": { + "pragma": "no-cache", + "strict-transport-security": "max-age=31536000;includeSubDomains", + "x-content-type-options": "nosniff", + "x-ms-keyvault-network-info": "addr=40.121.43.168;act_addr_fam=InterNetwork;", + "server": "Microsoft-IIS/10.0", + "x-aspnet-version": "4.0.30319", + "x-ms-keyvault-service-version": "1.1.0.875", + "x-powered-by": "ASP.NET", + "status": "204", + "x-ms-request-id": "77c80ebe-fc0d-4daf-b759-a50cccc5a109", + "x-ms-keyvault-region": "westus", + "date": "Tue, 13 Aug 2019 19:37:36 GMT", + "cache-control": "no-cache", + "expires": "-1" + } + } + ], + "uniqueTestInfo": {} +} \ No newline at end of file diff --git a/sdk/keyvault/keyvault-certificates/recordings/node/certificates_client__create_read_update_and_delete_operations/recording_can_create_a_certificate.js b/sdk/keyvault/keyvault-certificates/recordings/node/certificates_client__create_read_update_and_delete_operations/recording_can_create_a_certificate.js new file mode 100644 index 000000000000..4af041fe34ca --- /dev/null +++ b/sdk/keyvault/keyvault-certificates/recordings/node/certificates_client__create_read_update_and_delete_operations/recording_can_create_a_certificate.js @@ -0,0 +1,443 @@ +let nock = require('nock'); + +module.exports.testInfo = {} + +nock('https://keyvault_name.vault.azure.net:443', {"encodedQueryParams":true}) + .post('/certificates/recoverCertificateName-cancreateacertificate-/create') + .query(true) + .reply(401, {"error":{"code":"Unauthorized","message":"Request is missing a Bearer or PoP token."}}, [ 'Cache-Control', + 'no-cache', + 'Pragma', + 'no-cache', + 'Content-Length', + '87', + 'Content-Type', + 'application/json; charset=utf-8', + 'Expires', + '-1', + 'Server', + 'Microsoft-IIS/10.0', + 'WWW-Authenticate', + 'Bearer authorization="https://login.windows.net/azure_tenant_id", resource="https://vault.azure.net"', + 'x-ms-keyvault-region', + 'westus', + 'x-ms-request-id', + '9581f6ed-a0bc-4143-905b-0e56e33c394b', + 'x-ms-keyvault-service-version', + '1.1.0.875', + 'x-ms-keyvault-network-info', + 'addr=40.121.43.168;act_addr_fam=InterNetwork;', + 'X-AspNet-Version', + '4.0.30319', + 'X-Powered-By', + 'ASP.NET', + 'Strict-Transport-Security', + 'max-age=31536000;includeSubDomains', + 'X-Content-Type-Options', + 'nosniff', + 'Date', + 'Tue, 13 Aug 2019 19:34:53 GMT', + 'Connection', + 'close' ]); + + +nock('https://login.microsoftonline.com:443', {"encodedQueryParams":true}) + .post('/azure_tenant_id/oauth2/v2.0/token', "response_type=token&grant_type=client_credentials&client_id=azure_client_id&client_secret=azure_client_secret&scope=https%3A%2F%2Fvault.azure.net%2F.default") + .reply(200, {"token_type":"Bearer","expires_in":3599,"ext_expires_in":3599,"access_token":"access_token"}, [ 'Cache-Control', + 'no-cache, no-store', + 'Pragma', + 'no-cache', + 'Content-Type', + 'application/json; charset=utf-8', + 'Expires', + '-1', + 'Strict-Transport-Security', + 'max-age=31536000; includeSubDomains', + 'X-Content-Type-Options', + 'nosniff', + 'x-ms-request-id', + '70a9e7f5-8404-47f4-a125-3e3bff963a00', + 'x-ms-ests-server', + '2.1.9228.13 - EST ProdSlices', + 'P3P', + 'CP="DSP CUR OTPi IND OTRi ONL FIN"', + 'Set-Cookie', + 'fpc=AmlKJjaLPh1Btl-g2FEhTVM_aSJHAQAAAN0H5dQOAAAA; expires=Thu, 12-Sep-2019 19:34:53 GMT; path=/; secure; HttpOnly', + 'Set-Cookie', + 'x-ms-gateway-slice=prod; path=/; secure; HttpOnly', + 'Set-Cookie', + 'stsservicecookie=ests; path=/; secure; HttpOnly', + 'Date', + 'Tue, 13 Aug 2019 19:34:53 GMT', + 'Connection', + 'close', + 'Content-Length', + '1231' ]); + + +nock('https://keyvault_name.vault.azure.net:443', {"encodedQueryParams":true}) + .post('/certificates/recoverCertificateName-cancreateacertificate-/create', {"policy":{"x509_props":{"subject":"cn=MyCert"},"issuer":{"name":"Self"}}}) + .query(true) + .reply(202, {"id":"https://keyvault_name.vault.azure.net/certificates/recoverCertificateName-cancreateacertificate-/pending","issuer":{"name":"Self"},"csr":"MIICoTCCAYkCAQAwETEPMA0GA1UEAxMGTXlDZXJ0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvTa5u0tB35rtudU3BpuxV95WOyoNzRtQVdTTlanV6cvVi8XOzNzLDoZk7KTqakljs5FrjAyxaCpp3lREU9UtgbSIVJc6CrDSi+ZrATj19evfR+IJR1q45mrc9cBZ5NjSRh9VAoBZf6WILEDwooirS5rjz+kSvBx7JpW86zrDYJezSVF/lTNlfiElXi+cvd9e+bas3MlqSYGuhIyrbNZ5dehpJM3M2SDPVgS1ZT/+0T+o1onrgVknRbmbHR/6cLa9mKfAoypGf0li3lor8cypqvuqz86wxUrBdcJv0YbnmLOdwhfoOYseYNVvv2TeoktKyp4+IQQ+QwQ46ca1S+EJzQIDAQABoEswSQYJKoZIhvcNAQkOMTwwOjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEBADq8i549mqXmAv3f5uJkVxA9EUiwOlhnkSMwDavD/yBF+JESG5Dc4vE/yzGPGTT3wWlFrEVNXvsy/iTjITXnCISXdhTuN9yyLdylcY5907SeVRA3bhXnqTxn3pgEy7FbTrpCzTmjU/JEtssIbZf58y3B2aBhdCDQrqdiXsfuGjDrt8/vSrHySV088w5wxe6b/5+ahmGUVMIiMuKRxr3JfDFbiBJd7d5f70WOH0g/P0lPDe5t4mOMOzRGiohNy3PhlZpsasMucBrNxw6SPxVpMf6MMBSuLyG3ylmfJrDGejnMd0PXU+Fda4UJgqniMJs1tVl5gocRZKLE4/DiO9LtPgU=","cancellation_requested":false,"status":"inProgress","status_details":"Pending certificate created. Certificate request is in progress. This may take some time based on the issuer provider. Please check again later.","request_id":"59ab5c2ceb7d4d24894d5ae8d2cfe88a"}, [ 'Cache-Control', + 'no-cache', + 'Pragma', + 'no-cache', + 'Content-Type', + 'application/json; charset=utf-8', + 'Expires', + '-1', + 'Location', + 'https://keyvault_name.vault.azure.net/certificates/recoverCertificateName-cancreateacertificate-/pending?api-version=7.0&request_id=59ab5c2ceb7d4d24894d5ae8d2cfe88a', + 'Retry-After', + '10', + 'Server', + 'Microsoft-IIS/10.0', + 'x-ms-keyvault-region', + 'westus', + 'x-ms-request-id', + '60e52241-7563-448c-baa4-9085be13977f', + 'x-ms-keyvault-service-version', + '1.1.0.875', + 'x-ms-keyvault-network-info', + 'addr=40.121.43.168;act_addr_fam=InterNetwork;', + 'X-AspNet-Version', + '4.0.30319', + 'X-Powered-By', + 'ASP.NET', + 'Strict-Transport-Security', + 'max-age=31536000;includeSubDomains', + 'X-Content-Type-Options', + 'nosniff', + 'Date', + 'Tue, 13 Aug 2019 19:34:53 GMT', + 'Connection', + 'close', + 'Content-Length', + '1330' ]); + + +nock('https://keyvault_name.vault.azure.net:443', {"encodedQueryParams":true}) + .delete('/certificates/recoverCertificateName-cancreateacertificate-') + .query(true) + .reply(401, {"error":{"code":"Unauthorized","message":"Request is missing a Bearer or PoP token."}}, [ 'Cache-Control', + 'no-cache', + 'Pragma', + 'no-cache', + 'Content-Length', + '87', + 'Content-Type', + 'application/json; charset=utf-8', + 'Expires', + '-1', + 'Server', + 'Microsoft-IIS/10.0', + 'WWW-Authenticate', + 'Bearer authorization="https://login.windows.net/azure_tenant_id", resource="https://vault.azure.net"', + 'x-ms-keyvault-region', + 'westus', + 'x-ms-request-id', + '17238c84-7b0c-4106-88a0-9a4577d7763a', + 'x-ms-keyvault-service-version', + '1.1.0.875', + 'x-ms-keyvault-network-info', + 'addr=40.121.43.168;act_addr_fam=InterNetwork;', + 'X-AspNet-Version', + '4.0.30319', + 'X-Powered-By', + 'ASP.NET', + 'Strict-Transport-Security', + 'max-age=31536000;includeSubDomains', + 'X-Content-Type-Options', + 'nosniff', + 'Date', + 'Tue, 13 Aug 2019 19:34:54 GMT', + 'Connection', + 'close' ]); + + +nock('https://login.microsoftonline.com:443', {"encodedQueryParams":true}) + .post('/azure_tenant_id/oauth2/v2.0/token', "response_type=token&grant_type=client_credentials&client_id=azure_client_id&client_secret=azure_client_secret&scope=https%3A%2F%2Fvault.azure.net%2F.default") + .reply(200, {"token_type":"Bearer","expires_in":3600,"ext_expires_in":3600,"access_token":"access_token"}, [ 'Cache-Control', + 'no-cache, no-store', + 'Pragma', + 'no-cache', + 'Content-Type', + 'application/json; charset=utf-8', + 'Expires', + '-1', + 'Strict-Transport-Security', + 'max-age=31536000; includeSubDomains', + 'X-Content-Type-Options', + 'nosniff', + 'x-ms-request-id', + 'a8f2f2b4-676e-4790-9e05-711489829500', + 'x-ms-ests-server', + '2.1.9228.13 - EST ProdSlices', + 'P3P', + 'CP="DSP CUR OTPi IND OTRi ONL FIN"', + 'Set-Cookie', + 'fpc=AmlKJjaLPh1Btl-g2FEhTVM_aSJHAgAAAN0H5dQOAAAA; expires=Thu, 12-Sep-2019 19:34:54 GMT; path=/; secure; HttpOnly', + 'Set-Cookie', + 'x-ms-gateway-slice=prod; path=/; secure; HttpOnly', + 'Set-Cookie', + 'stsservicecookie=ests; path=/; secure; HttpOnly', + 'Date', + 'Tue, 13 Aug 2019 19:34:54 GMT', + 'Connection', + 'close', + 'Content-Length', + '1231' ]); + + +nock('https://keyvault_name.vault.azure.net:443', {"encodedQueryParams":true}) + .delete('/certificates/recoverCertificateName-cancreateacertificate-') + .query(true) + .reply(200, {"recoveryId":"https://keyvault_name.vault.azure.net/deletedcertificates/recoverCertificateName-cancreateacertificate-","deletedDate":1565724894,"scheduledPurgeDate":1573500894,"id":"https://keyvault_name.vault.azure.net/certificates/recoverCertificateName-cancreateacertificate-/7411e5a891c74bfc9307fb9d08ac9c48","attributes":{"enabled":false,"nbf":1565724293,"exp":1597347293,"created":1565724893,"updated":1565724893,"recoveryLevel":"Recoverable+Purgeable"},"policy":{"id":"https://keyvault_name.vault.azure.net/certificates/recoverCertificateName-cancreateacertificate-/policy","key_props":{"exportable":true,"kty":"RSA","key_size":2048,"reuse_key":false},"secret_props":{"contentType":"application/x-pkcs12"},"x509_props":{"subject":"cn=MyCert","ekus":["1.3.6.1.5.5.7.3.1","1.3.6.1.5.5.7.3.2"],"key_usage":["digitalSignature","keyEncipherment"],"validity_months":12,"basic_constraints":{"ca":false}},"lifetime_actions":[{"trigger":{"lifetime_percentage":80},"action":{"action_type":"AutoRenew"}}],"issuer":{"name":"Self"},"attributes":{"enabled":true,"created":1565724894,"updated":1565724894}},"pending":{"id":"https://keyvault_name.vault.azure.net/certificates/recoverCertificateName-cancreateacertificate-/pending"}}, [ 'Cache-Control', + 'no-cache', + 'Pragma', + 'no-cache', + 'Content-Type', + 'application/json; charset=utf-8', + 'Expires', + '-1', + 'Server', + 'Microsoft-IIS/10.0', + 'x-ms-keyvault-region', + 'westus', + 'x-ms-request-id', + 'f3411477-b0b2-436a-805f-2a846d139c6d', + 'x-ms-keyvault-service-version', + '1.1.0.875', + 'x-ms-keyvault-network-info', + 'addr=40.121.43.168;act_addr_fam=InterNetwork;', + 'X-AspNet-Version', + '4.0.30319', + 'X-Powered-By', + 'ASP.NET', + 'Strict-Transport-Security', + 'max-age=31536000;includeSubDomains', + 'X-Content-Type-Options', + 'nosniff', + 'Date', + 'Tue, 13 Aug 2019 19:34:55 GMT', + 'Connection', + 'close', + 'Content-Length', + '1280' ]); + + +nock('https://keyvault_name.vault.azure.net:443', {"encodedQueryParams":true}) + .delete('/deletedcertificates/recoverCertificateName-cancreateacertificate-') + .query(true) + .reply(401, {"error":{"code":"Unauthorized","message":"Request is missing a Bearer or PoP token."}}, [ 'Cache-Control', + 'no-cache', + 'Pragma', + 'no-cache', + 'Content-Length', + '87', + 'Content-Type', + 'application/json; charset=utf-8', + 'Expires', + '-1', + 'Server', + 'Microsoft-IIS/10.0', + 'WWW-Authenticate', + 'Bearer authorization="https://login.windows.net/azure_tenant_id", resource="https://vault.azure.net"', + 'x-ms-keyvault-region', + 'westus', + 'x-ms-request-id', + 'cb0084a9-bc67-468b-b660-0808f660a8f7', + 'x-ms-keyvault-service-version', + '1.1.0.875', + 'x-ms-keyvault-network-info', + 'addr=40.121.43.168;act_addr_fam=InterNetwork;', + 'X-AspNet-Version', + '4.0.30319', + 'X-Powered-By', + 'ASP.NET', + 'Strict-Transport-Security', + 'max-age=31536000;includeSubDomains', + 'X-Content-Type-Options', + 'nosniff', + 'Date', + 'Tue, 13 Aug 2019 19:34:54 GMT', + 'Connection', + 'close' ]); + + +nock('https://login.microsoftonline.com:443', {"encodedQueryParams":true}) + .post('/azure_tenant_id/oauth2/v2.0/token', "response_type=token&grant_type=client_credentials&client_id=azure_client_id&client_secret=azure_client_secret&scope=https%3A%2F%2Fvault.azure.net%2F.default") + .reply(200, {"token_type":"Bearer","expires_in":3599,"ext_expires_in":3599,"access_token":"access_token"}, [ 'Cache-Control', + 'no-cache, no-store', + 'Pragma', + 'no-cache', + 'Content-Type', + 'application/json; charset=utf-8', + 'Expires', + '-1', + 'Strict-Transport-Security', + 'max-age=31536000; includeSubDomains', + 'X-Content-Type-Options', + 'nosniff', + 'x-ms-request-id', + 'd22907f7-adde-4817-8927-fa5de3639600', + 'x-ms-ests-server', + '2.1.9228.13 - EST ProdSlices', + 'P3P', + 'CP="DSP CUR OTPi IND OTRi ONL FIN"', + 'Set-Cookie', + 'fpc=AmlKJjaLPh1Btl-g2FEhTVM_aSJHAwAAAN0H5dQOAAAA; expires=Thu, 12-Sep-2019 19:34:55 GMT; path=/; secure; HttpOnly', + 'Set-Cookie', + 'x-ms-gateway-slice=prod; path=/; secure; HttpOnly', + 'Set-Cookie', + 'stsservicecookie=ests; path=/; secure; HttpOnly', + 'Date', + 'Tue, 13 Aug 2019 19:34:54 GMT', + 'Connection', + 'close', + 'Content-Length', + '1231' ]); + + +nock('https://keyvault_name.vault.azure.net:443', {"encodedQueryParams":true}) + .delete('/deletedcertificates/recoverCertificateName-cancreateacertificate-') + .query(true) + .reply(409, {"error":{"code":"Conflict","message":"Certificate is currently being deleted.","innererror":{"code":"ObjectIsBeingDeleted"}}}, [ 'Cache-Control', + 'no-cache', + 'Pragma', + 'no-cache', + 'Content-Length', + '126', + 'Content-Type', + 'application/json; charset=utf-8', + 'Expires', + '-1', + 'Server', + 'Microsoft-IIS/10.0', + 'x-ms-keyvault-region', + 'westus', + 'x-ms-request-id', + 'f91d1368-4cf8-4685-a589-4243d22ced21', + 'x-ms-keyvault-service-version', + '1.1.0.875', + 'x-ms-keyvault-network-info', + 'addr=40.121.43.168;act_addr_fam=InterNetwork;', + 'X-AspNet-Version', + '4.0.30319', + 'X-Powered-By', + 'ASP.NET', + 'Strict-Transport-Security', + 'max-age=31536000;includeSubDomains', + 'X-Content-Type-Options', + 'nosniff', + 'Date', + 'Tue, 13 Aug 2019 19:34:55 GMT', + 'Connection', + 'close' ]); + + +nock('https://keyvault_name.vault.azure.net:443', {"encodedQueryParams":true}) + .delete('/deletedcertificates/recoverCertificateName-cancreateacertificate-') + .query(true) + .reply(401, {"error":{"code":"Unauthorized","message":"Request is missing a Bearer or PoP token."}}, [ 'Cache-Control', + 'no-cache', + 'Pragma', + 'no-cache', + 'Content-Length', + '87', + 'Content-Type', + 'application/json; charset=utf-8', + 'Expires', + '-1', + 'Server', + 'Microsoft-IIS/10.0', + 'WWW-Authenticate', + 'Bearer authorization="https://login.windows.net/azure_tenant_id", resource="https://vault.azure.net"', + 'x-ms-keyvault-region', + 'westus', + 'x-ms-request-id', + 'b54b1d61-2a28-44df-b898-3ddf00aee5ac', + 'x-ms-keyvault-service-version', + '1.1.0.875', + 'x-ms-keyvault-network-info', + 'addr=40.121.43.168;act_addr_fam=InterNetwork;', + 'X-AspNet-Version', + '4.0.30319', + 'X-Powered-By', + 'ASP.NET', + 'Strict-Transport-Security', + 'max-age=31536000;includeSubDomains', + 'X-Content-Type-Options', + 'nosniff', + 'Date', + 'Tue, 13 Aug 2019 19:35:05 GMT', + 'Connection', + 'close' ]); + + +nock('https://login.microsoftonline.com:443', {"encodedQueryParams":true}) + .post('/azure_tenant_id/oauth2/v2.0/token', "response_type=token&grant_type=client_credentials&client_id=azure_client_id&client_secret=azure_client_secret&scope=https%3A%2F%2Fvault.azure.net%2F.default") + .reply(200, {"token_type":"Bearer","expires_in":3600,"ext_expires_in":3600,"access_token":"access_token"}, [ 'Cache-Control', + 'no-cache, no-store', + 'Pragma', + 'no-cache', + 'Content-Type', + 'application/json; charset=utf-8', + 'Expires', + '-1', + 'Strict-Transport-Security', + 'max-age=31536000; includeSubDomains', + 'X-Content-Type-Options', + 'nosniff', + 'x-ms-request-id', + 'a065e0e3-a8a5-4c2f-85fd-8cf7ac139e00', + 'x-ms-ests-server', + '2.1.9228.13 - EST ProdSlices', + 'P3P', + 'CP="DSP CUR OTPi IND OTRi ONL FIN"', + 'Set-Cookie', + 'fpc=AmlKJjaLPh1Btl-g2FEhTVM_aSJHBAAAAN0H5dQOAAAA; expires=Thu, 12-Sep-2019 19:35:06 GMT; path=/; secure; HttpOnly', + 'Set-Cookie', + 'x-ms-gateway-slice=prod; path=/; secure; HttpOnly', + 'Set-Cookie', + 'stsservicecookie=ests; path=/; secure; HttpOnly', + 'Date', + 'Tue, 13 Aug 2019 19:35:06 GMT', + 'Connection', + 'close', + 'Content-Length', + '1231' ]); + + +nock('https://keyvault_name.vault.azure.net:443', {"encodedQueryParams":true}) + .delete('/deletedcertificates/recoverCertificateName-cancreateacertificate-') + .query(true) + .reply(204, "", [ 'Cache-Control', + 'no-cache', + 'Pragma', + 'no-cache', + 'Expires', + '-1', + 'Server', + 'Microsoft-IIS/10.0', + 'x-ms-keyvault-region', + 'westus', + 'x-ms-request-id', + '1b1ba974-026a-4594-a96b-060ca314069d', + 'x-ms-keyvault-service-version', + '1.1.0.875', + 'x-ms-keyvault-network-info', + 'addr=40.121.43.168;act_addr_fam=InterNetwork;', + 'X-AspNet-Version', + '4.0.30319', + 'X-Powered-By', + 'ASP.NET', + 'Strict-Transport-Security', + 'max-age=31536000;includeSubDomains', + 'X-Content-Type-Options', + 'nosniff', + 'Date', + 'Tue, 13 Aug 2019 19:35:05 GMT', + 'Connection', + 'close' ]); + diff --git a/sdk/keyvault/keyvault-certificates/rollup.base.config.js b/sdk/keyvault/keyvault-certificates/rollup.base.config.js new file mode 100644 index 000000000000..f47cbd7e7b26 --- /dev/null +++ b/sdk/keyvault/keyvault-certificates/rollup.base.config.js @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import nodeResolve from "rollup-plugin-node-resolve"; +import multiEntry from "rollup-plugin-multi-entry"; +import cjs from "rollup-plugin-commonjs"; +import replace from "rollup-plugin-replace"; +import { terser } from "rollup-plugin-terser"; +import sourcemaps from "rollup-plugin-sourcemaps"; +import shim from "rollup-plugin-shim"; + +/** + * @type {import('rollup').RollupFileOptions} + */ + +const pkg = require("./package.json"); +const version = pkg.version; +const banner = [ + "/*!", + " * Copyright (c) Microsoft and contributors. All rights reserved.", + " * Licensed under the MIT License. See License.txt in the project root for", + " * license information.", + " * ", + ` * Azure KeyVault Certificates SDK for JavaScript - ${version}`, + " */" +].join("\n"); + +const depNames = Object.keys(pkg.dependencies); +const production = process.env.NODE_ENV === "production"; + +export function nodeConfig(test = false) { + const externalNodeBuiltins = ["fs", "os", "url", "assert"]; + const baseConfig = { + input: "dist-esm/src/index.js", + external: depNames.concat(externalNodeBuiltins), + output: { + file: "dist/index.js", + format: "cjs", + name: "azurekeyvaultcertificates", + sourcemap: true, + banner: banner + }, + plugins: [ + sourcemaps(), + replace({ + delimiters: ["", ""], + values: { + // replace dynamic checks with if (true) since this is for node only. + // Allows rollup's dead code elimination to be more aggressive. + "if (isNode)": ";isNode; if (true)" + } + }), + nodeResolve({ preferBuiltins: true }), + cjs() + ] + }; + + if (test) { + // entry point is every test file + baseConfig.input = ["dist-esm/tests/*.test.js"]; + baseConfig.plugins.unshift(multiEntry({ exports: false })); + + // different output file + baseConfig.output.file = "dist-test/index.node.js"; + + baseConfig.external.push("assert", "fs", "path"); + + baseConfig.context = "null"; + + // Disable tree-shaking of test code. In rollup-plugin-node-resolve@5.0.0, rollup started respecting + // the "sideEffects" field in package.json. Since our package.json sets "sideEffects=false", this also + // applies to test code, which causes all tests to be removed by tree-shaking. + baseConfig.treeshake = false; + } else if (production) { + baseConfig.plugins.push(terser()); + } + + return baseConfig; +} + +export function browserConfig(test = false) { + const baseConfig = { + input: "dist-esm/src/index.js", + output: { + file: "browser/azure-keyvault-certificates.js", + banner: banner, + format: "umd", + name: "azurekeyvaultcertificates", + globals: { + "@azure/core-http": "Azure.Core.HTTP", + "@azure/core-arm": "Azure.Core.ARM" + }, + sourcemap: true + }, + preserveSymlinks: false, + plugins: [ + sourcemaps(), + replace({ + delimiters: ["", ""], + values: { + // replace dynamic checks with if (false) since this is for + // browser only. Rollup's dead code elimination will remove + // any code guarded by if (isNode) { ... } + "if (isNode)": ";isNode; if (false)" + } + }), + // os is not used by the browser bundle, so just shim it + shim({ + dotenv: `export function config() { }`, + os: ` + export const type = 1; + export const release = 1; + ` + }), + nodeResolve({ + mainFields: ["module", "browser"], + preferBuiltins: false + }), + cjs({ + namedExports: { + assert: ["ok", "equal", "strictEqual"] + } + }) + ] + }; + + baseConfig.external = ["fs-extra", "path"]; + if (test) { + baseConfig.input = ["dist-esm/tests/*.test.js"]; + baseConfig.plugins.unshift(multiEntry({ exports: false })); + baseConfig.output.file = "dist-test/index.browser.js"; + // mark fs-extra as external + baseConfig.context = "null"; + + // Disable tree-shaking of test code. In rollup-plugin-node-resolve@5.0.0, rollup started respecting + // the "sideEffects" field in package.json. Since our package.json sets "sideEffects=false", this also + // applies to test code, which causes all tests to be removed by tree-shaking. + baseConfig.treeshake = false; + } else if (production) { + baseConfig.output.file = "browser/azure-keyvault-certificates.min.js"; + baseConfig.plugins.push( + terser({ + output: { + preamble: banner + } + }) + // Comment visualizer because it only works on Node.js 8+; Uncomment it to get bundle analysis report + // visualizer({ + // filename: "./statistics.html", + // sourcemap: true + // }) + ); + } + + return baseConfig; +} diff --git a/sdk/keyvault/keyvault-certificates/rollup.config.js b/sdk/keyvault/keyvault-certificates/rollup.config.js index f426f55447a2..53b75f9e5943 100644 --- a/sdk/keyvault/keyvault-certificates/rollup.config.js +++ b/sdk/keyvault/keyvault-certificates/rollup.config.js @@ -1,84 +1,14 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import nodeResolve from "rollup-plugin-node-resolve"; -import cjs from "rollup-plugin-commonjs"; - -/** - * @type {import('rollup').RollupFileOptions} - */ - -const pkg = require("./package.json"); -const version = pkg.version; -const banner = [ - "/*!", - " * Copyright (c) Microsoft and contributors. All rights reserved.", - " * Licensed under the MIT License. See License.txt in the project root for", - " * license information.", - " * ", - ` * Azure KeyVault Secrets SDK for JavaScript - ${version}`, - " */" -].join("\n"); - -const depNames = Object.keys(pkg.dependencies); -const input = "esm/index.js"; - -function nodeConfig(test = false) { - const externalNodeBuiltins = ["url"]; - const baseConfig = { - input: input, - external: depNames.concat(externalNodeBuiltins), - output: { - file: "dist/index.js", - format: "cjs", - name: "Azure.Keyvault.Secrets", - sourcemap: true, - banner: banner - }, - plugins: [ - nodeResolve({ preferBuiltins: true }), - cjs() - ] - }; - - return baseConfig; -} - -function browserConfig(test = false) { - const baseConfig = { - input: input, - output: { - file: "browser/index.js", - format: "umd", - name: "Azure.Keyvault.Secrets", - sourcemap: true, - globals: { - "@azure/core-http": "Azure.Core.HTTP", - "@azure/core-arm": "Azure.Core.ARM" - }, - banner: banner - }, - plugins: [ - nodeResolve({ - preferBuiltins: false, - browser: true, - module: true - }), - cjs() - ] - }; - - return baseConfig; -} +import * as base from "./rollup.base.config"; const inputs = []; if (!process.env.ONLY_BROWSER) { - inputs.push(nodeConfig()); + inputs.push(base.nodeConfig()); } +// Disable this until we are ready to run rollup for the browser. if (!process.env.ONLY_NODE) { - inputs.push(browserConfig()); + inputs.push(base.browserConfig()); } export default inputs; diff --git a/sdk/keyvault/keyvault-certificates/rollup.test.config.js b/sdk/keyvault/keyvault-certificates/rollup.test.config.js new file mode 100644 index 000000000000..ad98718cce46 --- /dev/null +++ b/sdk/keyvault/keyvault-certificates/rollup.test.config.js @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as base from "./rollup.base.config"; + +export default [base.nodeConfig(true), base.browserConfig(true)]; diff --git a/sdk/keyvault/keyvault-certificates/tests/CRUD.test.ts b/sdk/keyvault/keyvault-certificates/tests/CRUD.test.ts new file mode 100644 index 000000000000..af302374ad05 --- /dev/null +++ b/sdk/keyvault/keyvault-certificates/tests/CRUD.test.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import * as assert from "assert"; +import { CertificatesClient } from "../src"; +import { env } from "./utils/recorder"; +import { authenticate } from "./utils/testAuthentication"; +import TestClient from "./utils/testClient"; + +describe("Certificates client - create, read, update and delete operations", () => { + const prefix = `recover${env.CERTIFICATE_NAME || "CertificateName"}`; + let suffix: string; + let client: CertificatesClient; + let testClient: TestClient; + let recorder: any; + + beforeEach(async function() { + const authentication = await authenticate(this); + suffix = authentication.suffix; + client = authentication.client; + testClient = authentication.testClient; + recorder = authentication.recorder; + }); + + afterEach(async function() { + recorder.stop(); + }); + + // The tests follow + + it("can create a certificate", async function() { + const certificateName = testClient.formatName(`${prefix}-${this!.test!.title}-${suffix}`); + const result = await client.createCertificate(certificateName, { certificatePolicy: { issuerParameters: { name: "Self" }, x509CertificateProperties: { subject: "cn=MyCert" } }}); + assert.equal(result.name, certificateName, "Unexpected key name in result from createCertificate()."); + await testClient.flushCertificate(certificateName); + }); +}); diff --git a/sdk/keyvault/keyvault-certificates/tests/utils/index.browser.ts b/sdk/keyvault/keyvault-certificates/tests/utils/index.browser.ts new file mode 100644 index 000000000000..a9948afa1613 --- /dev/null +++ b/sdk/keyvault/keyvault-certificates/tests/utils/index.browser.ts @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export async function blobToString(blob: Blob): Promise { + const fileReader = new FileReader(); + return new Promise((resolve, reject) => { + fileReader.onloadend = (ev: any) => { + resolve(ev.target!.result); + }; + fileReader.onerror = reject; + fileReader.readAsText(blob); + }); +} diff --git a/sdk/keyvault/keyvault-certificates/tests/utils/recorder.ts b/sdk/keyvault/keyvault-certificates/tests/utils/recorder.ts new file mode 100644 index 000000000000..a967d3315350 --- /dev/null +++ b/sdk/keyvault/keyvault-certificates/tests/utils/recorder.ts @@ -0,0 +1,434 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import fs from "fs-extra"; +import nise from "nise"; +import { retry as realRetry } from "./retry"; +import { isNode as coreIsNode, delay as coreDelay } from "@azure/core-http"; +import queryString from "query-string"; +import * as dotenv from "dotenv"; +dotenv.config({ path: "../.env" }); + +export function isBrowser(): boolean { + return typeof window !== "undefined"; +} + +export const isNode = coreIsNode; + +export function escapeRegExp(str: string): string { + return encodeURIComponent(str).replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); +} + +export async function blobToString(blob: Blob): Promise { + const fileReader = new FileReader(); + return new Promise((resolve, reject) => { + fileReader.onloadend = (ev: any) => { + resolve(ev.target!.result); + }; + fileReader.onerror = reject; + fileReader.readAsText(blob); + }); +} + +let nock: any; +if (!isBrowser()) { + nock = require("nock"); +} + +export const env = isBrowser() ? (window as any).__env__ : process.env; +export const isRecording = env.TEST_MODE === "record"; +export const isPlayingBack = env.TEST_MODE === "playback"; + +// IMPORTANT: These are my attempts to make this more generic without changing it significantly +let replaceableVariables: { [key: string]: any } = {}; +export function setReplaceableVariables(a: { [key: string]: any }): void { + replaceableVariables = a; + if (isPlayingBack) { + // Providing dummy values to avoid the error + Object.keys(a).map((k) => { + env[k] = a[k]; + }); + } +} +let replacements: any[] = []; +export function setReplacements(maps: any): void { + replacements = maps; +} + +export function delay(milliseconds: number): Promise | null { + return isPlayingBack ? null : coreDelay(milliseconds); +} + +export async function retry( + target: () => Promise, + delay?: number, + timeout?: number, + increaseFactor?: number +): Promise { + return realRetry(target, isPlayingBack ? 0 : delay || 10000, timeout || Infinity, increaseFactor); +} + +abstract class Recorder { + protected readonly filepath: string; + public uniqueTestInfo: any = {}; + + constructor(env: string, testHierarchy: string, testTitle: string, ext: string) { + this.filepath = + env + + "/" + + this.formatPath(testHierarchy) + + "/recording_" + + this.formatPath(testTitle) + + "." + + ext; + } + + protected formatPath(path: string): string { + return path + .toLowerCase() + .replace(/ /g, "_") + .replace(/<=/g, "lte") + .replace(/>=/g, "gte") + .replace(//g, "gt") + .replace(/=/g, "eq") + .replace(/\W/g, ""); + } + + /** + * Additional layer of security to avoid unintended/accidental occurrences of secrets in the recordings + * */ + protected filterSecrets(recording: string): string { + let updatedRecording = recording; + for (const k of Object.keys(replaceableVariables)) { + const escaped = escapeRegExp(env[k]); + updatedRecording = updatedRecording.replace( + new RegExp(escaped, "g"), + replaceableVariables[k] + ); + } + for (const map of replacements) { + updatedRecording = map(updatedRecording); + } + return updatedRecording; + } + + public abstract record(): void; + public abstract playback(): void; + public abstract stop(): void; +} + +class NockRecorder extends Recorder { + constructor(testHierarchy: string, testTitle: string) { + super("node", testHierarchy, testTitle, "js"); + } + + public record(): void { + nock.recorder.rec({ + dont_print: true + }); + } + + public playback(): void { + // This path makes sense when tests are called through dist-test/index.node.js + // If tests are called directly, this would need to be `../../recordings/`. + const path = "../recordings/" + this.filepath; + this.uniqueTestInfo = require(path).testInfo; + } + + public stop(): void { + const importNock = "let nock = require('nock');\n"; + const fixtures = nock.recorder.play(); + + // Create the directories recursively incase they don't exist + try { + // Stripping away the filename from the filepath and retaining the directory structure + fs.ensureDirSync( + "./recordings/" + this.filepath.substring(0, this.filepath.lastIndexOf("/") + 1) + ); + } catch (err) { + if (err.code !== "EEXIST") throw err; + } + + const file = fs.createWriteStream("./recordings/" + this.filepath, { + flags: "w" + }); + + // Some tests expect errors to happen and, if a writing error is thrown in one of these tests, it may be captured in a catch block by accident, + // resulting in unexpected behavior. For this reason we're printing it to the console as well + file.on("error", (err: any) => { + console.log(err); + throw err; + }); + + file.write( + importNock + "\n" + "module.exports.testInfo = " + JSON.stringify(this.uniqueTestInfo) + "\n" + ); + + for (const fixture of fixtures) { + // We're not matching query string parameters because they may contain sensitive information, and Nock does not allow us to customize it easily + const updatedFixture = fixture.replace(/\.query\(.*\)/, ".query(true)"); + file.write(this.filterSecrets(updatedFixture) + "\n"); + } + + file.end(); + + nock.recorder.clear(); + nock.restore(); + } +} + +class NiseRecorder extends Recorder { + private readonly sasQueryParameters = ["se", "sig", "sp", "spr", "srt", "ss", "st", "sv"]; + private recordings: any[] = []; + + constructor(testHierarchy: string, testTitle: string) { + super("browsers", testHierarchy, testTitle, "json"); + } + + // Inserts a request/response pair into the recordings array + private async recordRequest(request: any, data: any): Promise { + const responseHeaders: any = {}; + const responseHeadersPairs = request.getAllResponseHeaders().split("\r\n"); + for (const pair of responseHeadersPairs) { + const [key, value] = pair.split(": "); + responseHeaders[key] = value; + } + + // We're not storing SAS Query Parameters because they may contain sensitive information + // We're ignoring the "_" parameter as well because it's not being added by our code + // More info on "_": https://stackoverflow.com/questions/3687729/who-add-single-underscore-query-parameter + const parsedUrl = queryString.parseUrl(request.url); + const query: any = {}; + for (const param in parsedUrl.query) { + if (!this.sasQueryParameters.includes(param) && param !== "_") { + query[param] = parsedUrl.query[param]; + } + } + + this.recordings.push({ + method: request.method, + url: parsedUrl.url, + query: query, + requestBody: data instanceof Blob ? await blobToString(data) : data, + status: request.status, + response: + request.response instanceof Blob ? await blobToString(request.response) : request.response, + responseHeaders: responseHeaders + }); + } + + // Checks whether a recording matches a request or not (we're not matching request headers) + private matchRequest(recording: any, request: any): boolean { + // Every parameter in the recording must be present and have the same value in the request + for (const param in recording.query) { + if (recording.query[param] !== request.query[param]) { + return false; + } + } + + // There shouldn't be parameters in the request that are not present in the recording (except for SAS Query Parameters and "_") + for (const param in request.query) { + if ( + recording.query[param] === undefined && + !this.sasQueryParameters.includes(param) && + param !== "_" + ) { + return false; + } + } + + return ( + recording.method === request.method && + recording.url === request.url && + recording.requestBody === request.requestBody + ); + } + + // When recording, we want to hit the server and intercept requests/responses + // Nise does not allow us to intercept requests if they're sent to the server, so we need to override its behavior + public record(): void { + const self = this; + const xhr = nise.fakeXhr.useFakeXMLHttpRequest(); + + // The following filter allows every request to be sent to the server without being mocked + xhr.useFilters = true; + xhr.addFilter(() => true); + + // 'onCreate' function is called when a new fake XMLHttpRequest object (req) is created + // Our intent is to override the request's 'onreadystatechange' function so we can create a recording once the response is ready + // We can only override 'onreadystatechange' AFTER the 'send' function is called because we need to make sure our implementation won't be overriden by the client + // But we can only override 'send' AFTER the 'open' function is called because the filter we set above makes Nise override it in 'open' body + xhr.onCreate = function(req: any) { + // We'll override the 'open' function, so we need to store a handle to its original implementation + const reqOpen = req.open; + req.open = function() { + // Here we are calling the original 'open' function to make sure everything is set up correctly (HTTP method, url, filters) + reqOpen.apply(req, arguments); + + // We'll override the 'send' function, so we need to store a handle to its original implementation + // We can already override it because we know 'open' has already been called + const reqSend = req.send; + req.send = function(data: any) { + // We'll override the 'onreadystatechange' function, so we need to store a handle to its original implementation + // Now we can finally override 'onreadystatechange' because 'send' has already been called + const reqStateChange = req.onreadystatechange; + req.onreadystatechange = function() { + // Record the request once the response is obtained + if (req.readyState === 4) { + self.recordRequest(req, data); + } + // Sometimes the client doesn't implement an 'onreadystatechange' function, so we need to make sure it exists before calling the original implementation + if (reqStateChange) { + reqStateChange.apply(null, arguments); + } + }; + + // Now that we have overriden 'onreadystatechange', we can send the request to the server + reqSend.apply(req, arguments); + }; + }; + }; + } + + // When playing back, we want to intercept requests, find a corresponding match in our recordings and respond to it with the recorded data + // We must override the request's 'send' function because all the request information (body, url, method, queries) will be ready when it's called + public playback(): void { + const self = this; + const xhr = nise.fakeXhr.useFakeXMLHttpRequest(); + + // 'karma-json-preprocessor' helps us to retrieve recordings + this.recordings = (window as any).__json__["recordings/" + this.filepath].recordings; + this.uniqueTestInfo = (window as any).__json__["recordings/" + this.filepath].uniqueTestInfo; + + // 'onCreate' function is called when a new fake XMLHttpRequest object (req) is created + xhr.onCreate = function(req: any) { + // We'll override the 'send' function, so we need to store a handle to its original implementation + const reqSend = req.send; + req.send = async function(data: any) { + // Here we're calling the original send method. Nise will make the request wait for a mock response that we'll send later + reqSend.call(req, data); + + // formattedRequest contains all the necessary information to look for a match in our recordings + const parsedUrl = queryString.parseUrl(req.url); + const formattedRequest = { + method: req.method, + url: parsedUrl.url, + query: parsedUrl.query, + requestBody: data instanceof Blob ? await blobToString(data) : data + }; + + // We look through our recordings to find a match to the current request + // If we find a match, we remove it from the recordings list so we don't match it again by accident + let recordingFound = false; + for (let i = 0; !recordingFound && i < self.recordings.length; i++) { + if (self.matchRequest(self.recordings[i], formattedRequest)) { + const status = self.recordings[i].status; + const responseHeaders = self.recordings[i].responseHeaders; + const response = self.recordings[i].response; + + // We are dealing with async requests so we're responding to them asynchronously + setTimeout(() => req.respond(status, responseHeaders, response)); + self.recordings.splice(i, 1); + recordingFound = true; + } + } + + // If we can't find a match, we throw an error + // Some tests expect errors to happen and, if a matching error is thrown in one of these tests, it may be captured in a catch block by accident, + // resulting in unexpected behavior. For this reason we're printing it to the console as well + if (!recordingFound) { + const err = new Error( + "No match for request " + JSON.stringify(formattedRequest, null, " ") + ); + console.log(err); + throw err; + } + }; + }; + } + + public stop(): void { + for (let i = 0; i < this.recordings.length; i++) { + for (const k of Object.keys(this.recordings[i])) { + if (typeof this.recordings[i][k] === "string") { + this.recordings[i][k] = this.filterSecrets(this.recordings[i][k]); + } + } + } + // We're sending the recordings to the 'karma-json-to-file-reporter' via console.log + console.log( + JSON.stringify({ + writeFile: true, + path: "./recordings/" + this.filepath, + content: { recordings: this.recordings, uniqueTestInfo: this.uniqueTestInfo } + }) + ); + } +} + +export function uniqueString(): string { + return isPlayingBack + ? "" + : Math.random() + .toString() + .slice(2); +} + +// To better understand how this class works, it's necessary to comprehend how HTTP async requests are made: +// A new request object is created +// let req = new XMLHttpRequest(); +// The request is opened with some replaceableVariableseq.open(method, url, async, user, password); +// Since we're dealing with an async request, we must set a way to know when the response is ready +// req.onreadystatechange = function() { +// if (req.readyState === 4) do_something; +// } +// Finally, the request is sent to the server +// req.send(data); + +export function record(testContext: any): any { + let recorder: Recorder; + let testHierarchy: string; + let testTitle: string; + + if (testContext.currentTest) { + testHierarchy = testContext.currentTest.parent.fullTitle(); + testTitle = testContext.currentTest.title; + } else { + testHierarchy = testContext.test.parent.fullTitle(); + testTitle = testContext.test.title; + } + + if (isBrowser()) { + recorder = new NiseRecorder(testHierarchy, testTitle); + } else { + recorder = new NockRecorder(testHierarchy, testTitle); + } + + // If neither recording nor playback is enabled, requests hit the live-service and no recordings are generated + if (isRecording) { + recorder.record(); + } else if (isPlayingBack) { + recorder.playback(); + } + + return { + stop: function() { + if (isRecording) { + recorder.stop(); + } + }, + newDate: function(recorderId: string): Date { + let date: Date; + if (isRecording) { + date = new Date(); + recorder.uniqueTestInfo[recorderId] = date.toISOString(); + } else if (isPlayingBack) { + date = new Date(recorder.uniqueTestInfo[recorderId]); + } else { + date = new Date(); + } + return date; + } + }; +} diff --git a/sdk/keyvault/keyvault-certificates/tests/utils/retry.test.ts b/sdk/keyvault/keyvault-certificates/tests/utils/retry.test.ts new file mode 100644 index 000000000000..2c8ef8273ecb --- /dev/null +++ b/sdk/keyvault/keyvault-certificates/tests/utils/retry.test.ts @@ -0,0 +1,24 @@ +import { assert } from "chai"; +import { retry } from "./retry"; + +describe("retry utility function", function() { + it("throws an exception if we reach the maximum retries", async () => { + const startingDate = new Date(); + await retry( + async () => { + throw new Error("I always fail"); + }, + 100, + 200 + ); + const endingDate = new Date(); + + const difference = endingDate.getTime() - startingDate.getTime(); + assert.ok(difference >= 200); // In CI this takes a lot longer than locally + }); + + it("returns the value if resolved on time", async () => { + const result = await retry(async () => true); + assert.strictEqual(result, true); + }); +}); diff --git a/sdk/keyvault/keyvault-certificates/tests/utils/retry.ts b/sdk/keyvault/keyvault-certificates/tests/utils/retry.ts new file mode 100644 index 000000000000..d9fef0ecf36e --- /dev/null +++ b/sdk/keyvault/keyvault-certificates/tests/utils/retry.ts @@ -0,0 +1,29 @@ +import { delay as coreDelay } from "@azure/core-http"; + +/** + * A simple abstraction to retry, and exponentially de-escalate retrying, a + * given async function until it is fulfileld. + * @param {() => Promise} target The async function you want to retry + * @param {number} delay The delay between each retry, defaults to 1000 + * @param {number} timeout Maximum time we'll let this lapse before we quit retrying, defaults to Infinity + * @param {number} increaseFactor Increase factor of each retry, defaults to 1 + * @returns {Promise} Resolved promise + */ +export async function retry( + target: () => Promise, + delay: number = 1000, + timeout: number = Infinity, + increaseFactor: number = 1 +): Promise { + const start = new Date().getTime(); + let updatedDelay = delay; + while (new Date().getTime() - start < timeout) { + try { + return await target(); + } catch { + await coreDelay(updatedDelay); + updatedDelay *= increaseFactor; + } + } + return null; +} diff --git a/sdk/keyvault/keyvault-certificates/tests/utils/testAuthentication.ts b/sdk/keyvault/keyvault-certificates/tests/utils/testAuthentication.ts new file mode 100644 index 000000000000..6776c6ec567c --- /dev/null +++ b/sdk/keyvault/keyvault-certificates/tests/utils/testAuthentication.ts @@ -0,0 +1,36 @@ +import { ClientSecretCredential } from "@azure/identity"; +import { getKeyvaultName } from "./utils.common"; +import { CertificatesClient } from "../../src"; +import { env, record, setReplaceableVariables, setReplacements, uniqueString } from "./recorder"; +import TestClient from "./testClient"; + +export async function authenticate(that: any): Promise { + setReplaceableVariables({ + AZURE_CLIENT_ID: "azure_client_id", + AZURE_CLIENT_SECRET: "azure_client_secret", + AZURE_TENANT_ID: "azure_tenant_id", + KEYVAULT_NAME: "keyvault_name" + }); + + const suffix = uniqueString(); + setReplacements([ + (recording: any): any => + recording.replace(/"access_token":"[^"]*"/g, `"access_token":"access_token"`), + (recording: any): any => + suffix === "" ? recording : recording.replace(new RegExp(suffix, "g"), "") + ]); + + const recorder = record(that); + const credential = await new ClientSecretCredential( + env.AZURE_TENANT_ID, + env.AZURE_CLIENT_ID, + env.AZURE_CLIENT_SECRET + ); + + const keyVaultName = getKeyvaultName(); + const keyVaultUrl = `https://${keyVaultName}.vault.azure.net`; + const client = new CertificatesClient(keyVaultUrl, credential); + const testClient = new TestClient(client); + + return { recorder, client, credential, testClient, suffix }; +} diff --git a/sdk/keyvault/keyvault-certificates/tests/utils/testClient.ts b/sdk/keyvault/keyvault-certificates/tests/utils/testClient.ts new file mode 100644 index 000000000000..fde39d0db73b --- /dev/null +++ b/sdk/keyvault/keyvault-certificates/tests/utils/testClient.ts @@ -0,0 +1,28 @@ +import { retry } from "./recorder"; +import { CertificatesClient } from "../../src"; + +export default class TestClient { + public readonly client: CertificatesClient; + constructor(client: CertificatesClient) { + this.client = client; + } + public formatName(name: string): string { + return name.replace(/[^0-9a-zA-Z-]/g, ""); + } + public async purgeCertificate(keyName: string): Promise { + const that = this; + await retry(async () => { + try { + await that.client.purgeDeletedCertificate(keyName); + } catch (e) { + if (["Certificate is currently being deleted."].includes(e.message)) throw e; + else return; + } + }); + } + public async flushCertificate(keyName: string): Promise { + const that = this; + await that.client.deleteCertificate(keyName); + await this.purgeCertificate(keyName); + } +} diff --git a/sdk/keyvault/keyvault-certificates/tests/utils/utils.common.ts b/sdk/keyvault/keyvault-certificates/tests/utils/utils.common.ts new file mode 100644 index 000000000000..c950af637728 --- /dev/null +++ b/sdk/keyvault/keyvault-certificates/tests/utils/utils.common.ts @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { env } from "./recorder"; + +// Async iterator's polyfill for Node 8 +if (!Symbol || !(Symbol as any).asyncIterator) { + (Symbol as any).asyncIterator = Symbol.for("Symbol.asyncIterator"); +} + +export function getKeyvaultName(): string { + const keyVaultEnvVarName = "KEYVAULT_NAME"; + const keyVaultName: string | undefined = env[keyVaultEnvVarName]; + + if (!keyVaultName) { + throw new Error(`${keyVaultEnvVarName} environment variable not specified.`); + } + + return keyVaultName; +} diff --git a/sdk/keyvault/keyvault-certificates/tsconfig.json b/sdk/keyvault/keyvault-certificates/tsconfig.json index a922716d6c1e..0edd3b69743e 100644 --- a/sdk/keyvault/keyvault-certificates/tsconfig.json +++ b/sdk/keyvault/keyvault-certificates/tsconfig.json @@ -1,44 +1,25 @@ { "compilerOptions": { - /* Basic Options */ - "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, - "module": "es6" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, - "lib": [] /* lib dependencies are triple-slash directives in lib/index.ts */, - "declaration": true /* Generates corresponding '.d.ts' file. */, - "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */, - "sourceMap": true /* Generates corresponding '.map' file. */, - - "outDir": "./esm", - "stripInternal": true /* Do not emit declarations for code with @internal annotation*/, - "declarationDir": "./types/src" /* Output directory for generated declaration files.*/, - "importHelpers": true /* Import emit helpers from 'tslib'. */, - - /* Strict Type-Checking Options */ - "strict": true /* Enable all strict type-checking options. */, - "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, - - /* Additional Checks */ - "noUnusedLocals": true /* Report errors on unused locals. */, - - /* Module Resolution Options */ - "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, - "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, - - /* Experimental Options */ - "forceConsistentCasingInFileNames": true, - - /* Other options */ - "newLine": "LF" /* Use the specified end of line sequence to be used when emitting files: "crlf" (windows) or "lf" (unix).”*/, - "allowJs": false /* Don't allow JavaScript files to be compiled.*/, - "resolveJsonModule": true + "alwaysStrict": true, + "noImplicitAny": true, + "preserveConstEnums": true, + "sourceMap": true, + "newLine": "LF", + "target": "es5", + "moduleResolution": "node", + "noUnusedLocals": true, + "noUnusedParameters": true, + "strict": true, + "module": "esNext", + "outDir": "./dist-esm", + "declaration": true, + "declarationMap": true, + "importHelpers": true, + "declarationDir": "./types", + "lib": ["dom", "es5", "es6", "es7", "esnext"], + "esModuleInterop": true }, "compileOnSave": true, - "exclude": [ - "node_modules", - "types/**", - "./samples/**/*.ts", - "./test/perf/azure-sb-package/*.ts" - ], - "include": ["./src/**/*.ts", "./test/**/*.ts"] + "exclude": ["node_modules", "./samples/**/*.ts"], + "include": ["./src/**/*.ts", "./tests/**/*.ts"] }