diff --git a/.circleci/config.yml b/.circleci/config.yml index feade2a..23fbdd2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,25 +1,29 @@ version: 2.1 +orbs: + win: circleci/windows@1.0.0 + workflows: - workflow: + build-and-test-all: jobs: - - build-and-test: + - build-test-linux: name: latest Node version + docker-image: circleci/node:latest run-lint: true - - build-and-test: + - build-test-linux: name: oldest supported Node version docker-image: circleci/node:6 run-lint: false + - build-test-windows: + name: Windows jobs: - build-and-test: + build-test-linux: parameters: run-lint: type: boolean - default: false docker-image: type: string - default: circleci/node:latest docker: - image: <> - image: redis @@ -43,3 +47,28 @@ jobs: path: reports/junit - store_artifacts: path: reports/junit + + build-test-windows: + executor: + name: win/vs2019 + shell: powershell.exe + steps: + - checkout + - run: + name: set up Redis + command: | + $ProgressPreference = "SilentlyContinue" # prevents console errors from CircleCI host + iwr -outf redis.zip https://github.com/MicrosoftArchive/redis/releases/download/win-3.0.504/Redis-x64-3.0.504.zip + mkdir redis + Expand-Archive -Path redis.zip -DestinationPath redis + - run: + name: start Redis + command: | + cd redis + ./redis-server --service-install + ./redis-server --service-start + Start-Sleep -s 5 + ./redis-cli ping + - run: node --version + - run: npm install + - run: npm test diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index 9b15ace..0000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,28 +0,0 @@ -jobs: - - job: build - pool: - vmImage: 'vs2017-win2016' - steps: - - task: PowerShell@2 - displayName: 'Setup Redis' - inputs: - targetType: inline - workingDirectory: $(System.DefaultWorkingDirectory) - script: | - iwr -outf redis.zip https://github.com/MicrosoftArchive/redis/releases/download/win-3.0.504/Redis-x64-3.0.504.zip - mkdir redis - Expand-Archive -Path redis.zip -DestinationPath redis - cd redis - ./redis-server --service-install - ./redis-server --service-start - Start-Sleep -s 5 - ./redis-cli ping - - task: PowerShell@2 - displayName: 'Setup SDK and Test' - inputs: - targetType: inline - workingDirectory: $(System.DefaultWorkingDirectory) - script: | - node --version - npm install - npm test diff --git a/configuration.js b/configuration.js index 459fb2a..e11b8f1 100644 --- a/configuration.js +++ b/configuration.js @@ -25,6 +25,24 @@ module.exports = (function() { }; }; + const typesForPropertiesWithNoDefault = { + // Add a value here if we add a configuration property whose type cannot be determined by looking + // in baseDefaults (for instance, the default is null but if the value isn't null it should be a + // string). The allowable values are 'boolean', 'string', 'number', 'object', 'function', or + // 'factory' (the last one means it can be either a function or an object). + eventProcessor: 'object', + featureStore: 'object', + logger: 'object', // winston.Logger + proxyAgent: 'object', + proxyAuth: 'string', + proxyHost: 'string', + proxyPort: 'number', + proxyScheme: 'string', + tlsParams: 'object', // LDTLSOptions + updateProcessor: 'factory', // gets special handling in validation + userAgent: 'string' + }; + const deprecatedOptions = { base_uri: 'baseUri', stream_uri: 'streamUri', @@ -58,7 +76,7 @@ module.exports = (function() { // This works differently from Object.assign() in that it will *not* override a default value // if the provided value is explicitly set to null. const ret = Object.assign({}, config); - Object.keys(defaults).forEach(function(name) { + Object.keys(defaults).forEach(name => { if (ret[name] === undefined || ret[name] === null) { ret[name] = defaults[name]; } @@ -70,6 +88,48 @@ module.exports = (function() { return uri.replace(/\/+$/, ''); } + function validateTypesAndNames(config, defaultConfig) { + const typeDescForValue = value => { + if (value === null || value === undefined) { + return undefined; + } + if (Array.isArray(value)) { + return 'array'; + } + const t = typeof(value); + if (t === 'boolean' || t === 'string' || t === 'number') { + return t; + } + return 'object'; + }; + Object.keys(config).forEach(name => { + const value = config[name]; + if (value !== null && value !== undefined) { + const defaultValue = defaultConfig[name]; + const typeDesc = typesForPropertiesWithNoDefault[name]; + if (defaultValue === undefined && typeDesc === undefined) { + config.logger.warn(messages.unknownOption(name)); + } else { + const expectedType = typeDesc || typeDescForValue(defaultValue); + const actualType = typeDescForValue(value); + if (actualType !== expectedType) { + if (expectedType == 'factory' && (typeof value === 'function' || typeof value === 'object')) { + // for some properties, we allow either a factory function or an instance + return; + } + if (expectedType === 'boolean') { + config[name] = !!value; + config.logger.warn(messages.wrongOptionTypeBoolean(name, actualType)); + } else { + config.logger.warn(messages.wrongOptionType(name, expectedType, actualType)); + config[name] = defaultConfig[name]; + } + } + } + } + }); + } + function validate(options) { let config = Object.assign({}, options || {}); @@ -89,7 +149,10 @@ module.exports = (function() { checkDeprecatedOptions(config); - config = applyDefaults(config, defaults()); + const defaultConfig = defaults(); + config = applyDefaults(config, defaultConfig); + + validateTypesAndNames(config, defaultConfig); config.baseUri = canonicalizeUri(config.baseUri); config.streamUri = canonicalizeUri(config.streamUri); @@ -100,6 +163,7 @@ module.exports = (function() { } return { - validate: validate + validate: validate, + defaults: defaults }; })(); diff --git a/index.d.ts b/index.d.ts index 2b46325..b27c45d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -11,7 +11,7 @@ declare module 'launchdarkly-node-server-sdk' { import { EventEmitter } from 'events'; - import { ClientOpts } from 'redis'; + import { ClientOpts, RedisClient } from 'redis'; namespace errors { export const LDPollingError: ErrorConstructor; @@ -52,6 +52,9 @@ declare module 'launchdarkly-node-server-sdk' { * A string that should be prepended to all Redis keys used by the feature store. * @param logger * A custom logger for warnings and errors, if you are not using the default logger. + * @param client + * Pass this parameter if you already have a Redis client instance that you wish to reuse. In this case, + * `redisOpts` will be ignored. * * @returns * An object to put in the `featureStore` property for [[LDOptions]]. @@ -60,7 +63,8 @@ declare module 'launchdarkly-node-server-sdk' { redisOpts?: ClientOpts, cacheTTL?: number, prefix?: string, - logger?: LDLogger | object + logger?: LDLogger | object, + client?: RedisClient ): LDFeatureStore; /** diff --git a/messages.js b/messages.js index 603cba9..df99d3f 100644 --- a/messages.js +++ b/messages.js @@ -10,3 +10,11 @@ exports.httpErrorMessage = (status, context, retryMessage) => + ' - ' + (errors.isHttpErrorRecoverable(status) ? retryMessage : 'giving up permanently'); exports.missingUserKeyNoEvent = () => 'User was unspecified or had no key; event will not be sent'; + +exports.unknownOption = name => 'Ignoring unknown config option "' + name + '"'; + +exports.wrongOptionType = (name, expectedType, actualType) => + 'Config option "' + name + '" should be of type ' + expectedType + ', got ' + actualType + ', using default value'; + +exports.wrongOptionTypeBoolean = (name, actualType) => + 'Config option "' + name + '" should be a boolean, got ' + actualType + ', converting to boolean'; diff --git a/package-lock.json b/package-lock.json index a9bf55a..6ede465 100644 --- a/package-lock.json +++ b/package-lock.json @@ -101,6 +101,16 @@ "@babel/types": "^7.4.4" } }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.7.4.tgz", + "integrity": "sha512-Mt+jBKaxL0zfOIWrfQpnfYCN7/rS6GKx6CCCfuoqVVd+17R8zNDlzVYmIi9qyb2wOk002NsmSTDymkIygDUH7A==", + "dev": true, + "requires": { + "@babel/helper-regex": "^7.4.4", + "regexpu-core": "^4.6.0" + } + }, "@babel/helper-define-map": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.4.4.tgz", @@ -302,6 +312,16 @@ "@babel/plugin-syntax-async-generators": "^7.2.0" } }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.7.4.tgz", + "integrity": "sha512-StH+nGAdO6qDB1l8sZ5UBV8AC3F2VW2I8Vfld73TMKyptMU9DY5YsJAS8U81+vEtxcH3Y/La0wG0btDrhpnhjQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-dynamic-import": "^7.7.4" + } + }, "@babel/plugin-proposal-json-strings": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz", @@ -389,6 +409,15 @@ "@babel/helper-plugin-utils": "^7.0.0" } }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.7.4.tgz", + "integrity": "sha512-jHQW0vbRGvwQNgyVxwDh4yuXu4bH1f5/EICJLAhl1SblLs2CDhrsmCk+v5XLdE9wxtAFRyxx+P//Iw+a5L/tTg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, "@babel/plugin-syntax-json-strings": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz", @@ -416,6 +445,15 @@ "@babel/helper-plugin-utils": "^7.0.0" } }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.7.4.tgz", + "integrity": "sha512-wdsOw0MvkL1UIgiQ/IFr3ETcfv1xb8RMM0H9wbiDyLaJFyiDg5oZvDLCXosIXmFeIlweML5iOBXAkqddkYNizg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, "@babel/plugin-transform-arrow-functions": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz", @@ -893,6 +931,15 @@ } } }, + "@babel/runtime": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.4.tgz", + "integrity": "sha512-r24eVUUr0QqNZa+qrImUk8fn5SPhHq+IfYvIoIMg0do3GdK9sMdiLKP3GYVVaxpPKORgm8KRKaNTEhAjgIpLMw==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.2" + } + }, "@babel/template": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", @@ -1413,12 +1460,6 @@ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -1478,6 +1519,15 @@ "slash": "^2.0.0" } }, + "babel-plugin-dynamic-import-node": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz", + "integrity": "sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==", + "dev": true, + "requires": { + "object.assign": "^4.1.0" + } + }, "babel-plugin-istanbul": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-5.1.4.tgz", @@ -1639,6 +1689,28 @@ } } }, + "browserslist": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.7.3.tgz", + "integrity": "sha512-jWvmhqYpx+9EZm/FxcZSbUZyDEvDTLDi3nSAKbzEkyWvtI0mNSmUosey+5awDW1RUlrgXbQb5A6qY1xQH9U6MQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001010", + "electron-to-chromium": "^1.3.306", + "node-releases": "^1.1.40" + }, + "dependencies": { + "node-releases": { + "version": "1.1.41", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.41.tgz", + "integrity": "sha512-+IctMa7wIs8Cfsa8iYzeaLTFwv5Y4r5jZud+4AnfymzeEXKBCavFX0KBgzVaPVqf0ywa6PrO8/b+bPqdwjGBSg==", + "dev": true, + "requires": { + "semver": "^6.3.0" + } + } + } + }, "bser": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz", @@ -1683,6 +1755,12 @@ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true }, + "caniuse-lite": { + "version": "1.0.30001012", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001012.tgz", + "integrity": "sha512-7RR4Uh04t9K1uYRWzOJmzplgEOAXbfK72oVNokCdMzA67trrhPzy93ahKk1AWHiA0c58tD2P+NHqxrA8FZ+Trg==", + "dev": true + }, "capture-exit": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", @@ -1697,20 +1775,6 @@ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, - "chai": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", - "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", - "dev": true, - "requires": { - "assertion-error": "^1.0.1", - "check-error": "^1.0.1", - "deep-eql": "^3.0.0", - "get-func-name": "^2.0.0", - "pathval": "^1.0.0", - "type-detect": "^4.0.0" - } - }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -1728,12 +1792,6 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", - "dev": true - }, "ci-info": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", @@ -2057,21 +2115,6 @@ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "dev": true }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "dev": true, - "requires": { - "type-detect": "^4.0.0" - } - }, - "deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", - "dev": true - }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -2177,6 +2220,12 @@ "safer-buffer": "^2.1.0" } }, + "electron-to-chromium": { + "version": "1.3.314", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.314.tgz", + "integrity": "sha512-IKDR/xCxKFhPts7h+VaSXS02Z1mznP3fli1BbXWXeN89i2gCzKraU8qLpEid8YzKcmZdZD3Mly3cn5/lY9xsBQ==", + "dev": true + }, "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", @@ -3299,12 +3348,6 @@ "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", "dev": true }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", - "dev": true - }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -3370,9 +3413,9 @@ "dev": true }, "handlebars": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.5.tgz", - "integrity": "sha512-0Ce31oWVB7YidkaTq33ZxEbN+UDxMMgThvCe8ptgQViymL5DPis9uLdTA13MiRPhgvqyxIegugrP97iK3JeBHg==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz", + "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", "dev": true, "requires": { "neo-async": "^2.6.0", @@ -4555,6 +4598,23 @@ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, + "json5": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.1.tgz", + "integrity": "sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -4578,6 +4638,788 @@ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true }, + "launchdarkly-js-test-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/launchdarkly-js-test-helpers/-/launchdarkly-js-test-helpers-1.0.0.tgz", + "integrity": "sha512-GtCq97yyVG1CDMiyST4jh6LmjPcpHfrpth6i5ZpjkPMxrWeji5P02Zj+TEbOExE5Mr/bCd4SWBqgZSiRZUWoTA==", + "dev": true, + "requires": { + "@babel/core": "^7.6.4", + "@babel/preset-env": "^7.6.3", + "@babel/runtime": "^7.6.3", + "@types/node": "^12.12.11", + "selfsigned": "^1.10.4" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", + "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/core": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.7.4.tgz", + "integrity": "sha512-+bYbx56j4nYBmpsWtnPUsKW3NdnYxbqyfrP2w9wILBuHzdfIKz9prieZK0DFPyIzkjYVUe4QkusGL07r5pXznQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.5.5", + "@babel/generator": "^7.7.4", + "@babel/helpers": "^7.7.4", + "@babel/parser": "^7.7.4", + "@babel/template": "^7.7.4", + "@babel/traverse": "^7.7.4", + "@babel/types": "^7.7.4", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "json5": "^2.1.0", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + } + }, + "@babel/generator": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.7.4.tgz", + "integrity": "sha512-m5qo2WgdOJeyYngKImbkyQrnUN1mPceaG5BV+G0E3gWsa4l/jCSryWJdM2x8OuGAOyh+3d5pVYfZWCiNFtynxg==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.7.4.tgz", + "integrity": "sha512-2BQmQgECKzYKFPpiycoF9tlb5HA4lrVyAmLLVK177EcQAqjVLciUb2/R+n1boQ9y5ENV3uz2ZqiNw7QMBBw1Og==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.7.4.tgz", + "integrity": "sha512-Biq/d/WtvfftWZ9Uf39hbPBYDUo986m5Bb4zhkeYDGUllF43D+nUe5M6Vuo6/8JDK/0YX/uBdeoQpyaNhNugZQ==", + "dev": true, + "requires": { + "@babel/helper-explode-assignable-expression": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-call-delegate": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.7.4.tgz", + "integrity": "sha512-8JH9/B7J7tCYJ2PpWVpw9JhPuEVHztagNVuQAFBVFYluRMlpG7F1CgKEgGeL6KFqcsIa92ZYVj6DSc0XwmN1ZA==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.7.4", + "@babel/traverse": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-define-map": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.7.4.tgz", + "integrity": "sha512-v5LorqOa0nVQUvAUTUF3KPastvUt/HzByXNamKQ6RdJRTV7j8rLL+WB5C/MzzWAwOomxDhYFb1wLLxHqox86lg==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.7.4", + "@babel/types": "^7.7.4", + "lodash": "^4.17.13" + } + }, + "@babel/helper-explode-assignable-expression": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.7.4.tgz", + "integrity": "sha512-2/SicuFrNSXsZNBxe5UGdLr+HZg+raWBLE9vC98bdYOKX/U6PY0mdGlYUJdtTDPSU0Lw0PNbKKDpwYHJLn2jLg==", + "dev": true, + "requires": { + "@babel/traverse": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-function-name": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.7.4.tgz", + "integrity": "sha512-AnkGIdiBhEuiwdoMnKm7jfPfqItZhgRaZfMg1XX3bS25INOnLPjPG1Ppnajh8eqgt5kPJnfqrRHqFqmjKDZLzQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.7.4", + "@babel/template": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.4.tgz", + "integrity": "sha512-QTGKEdCkjgzgfJ3bAyRwF4yyT3pg+vDgan8DSivq1eS0gwi+KGKE5x8kRcbeFTb/673mkO5SN1IZfmCfA5o+EA==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.7.4.tgz", + "integrity": "sha512-wQC4xyvc1Jo/FnLirL6CEgPgPCa8M74tOdjWpRhQYapz5JC7u3NYU1zCVoVAGCE3EaIP9T1A3iW0WLJ+reZlpQ==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.7.4.tgz", + "integrity": "sha512-9KcA1X2E3OjXl/ykfMMInBK+uVdfIVakVe7W7Lg3wfXUNyS3Q1HWLFRwZIjhqiCGbslummPDnmb7vIekS0C1vw==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-module-imports": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.7.4.tgz", + "integrity": "sha512-dGcrX6K9l8258WFjyDLJwuVKxR4XZfU0/vTUgOQYWEnRD8mgr+p4d6fCUMq/ys0h4CCt/S5JhbvtyErjWouAUQ==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-module-transforms": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.7.4.tgz", + "integrity": "sha512-ehGBu4mXrhs0FxAqN8tWkzF8GSIGAiEumu4ONZ/hD9M88uHcD+Yu2ttKfOCgwzoesJOJrtQh7trI5YPbRtMmnA==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.7.4", + "@babel/helper-simple-access": "^7.7.4", + "@babel/helper-split-export-declaration": "^7.7.4", + "@babel/template": "^7.7.4", + "@babel/types": "^7.7.4", + "lodash": "^4.17.13" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.7.4.tgz", + "integrity": "sha512-VB7gWZ2fDkSuqW6b1AKXkJWO5NyNI3bFL/kK79/30moK57blr6NbH8xcl2XcKCwOmJosftWunZqfO84IGq3ZZg==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.7.4.tgz", + "integrity": "sha512-Sk4xmtVdM9sA/jCI80f+KS+Md+ZHIpjuqmYPk1M7F/upHou5e4ReYmExAiu6PVe65BhJPZA2CY9x9k4BqE5klw==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.7.4", + "@babel/helper-wrap-function": "^7.7.4", + "@babel/template": "^7.7.4", + "@babel/traverse": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-replace-supers": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.7.4.tgz", + "integrity": "sha512-pP0tfgg9hsZWo5ZboYGuBn/bbYT/hdLPVSS4NMmiRJdwWhP0IznPwN9AE1JwyGsjSPLC364I0Qh5p+EPkGPNpg==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.7.4", + "@babel/helper-optimise-call-expression": "^7.7.4", + "@babel/traverse": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-simple-access": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.7.4.tgz", + "integrity": "sha512-zK7THeEXfan7UlWsG2A6CI/L9jVnI5+xxKZOdej39Y0YtDYKx9raHk5F2EtK9K8DHRTihYwg20ADt9S36GR78A==", + "dev": true, + "requires": { + "@babel/template": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.4.tgz", + "integrity": "sha512-guAg1SXFcVr04Guk9eq0S4/rWS++sbmyqosJzVs8+1fH5NI+ZcmkaSkc7dmtAFbHFva6yRJnjW3yAcGxjueDug==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-wrap-function": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.7.4.tgz", + "integrity": "sha512-VsfzZt6wmsocOaVU0OokwrIytHND55yvyT4BPB9AIIgwr8+x7617hetdJTsuGwygN5RC6mxA9EJztTjuwm2ofg==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.7.4", + "@babel/template": "^7.7.4", + "@babel/traverse": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/helpers": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.7.4.tgz", + "integrity": "sha512-ak5NGZGJ6LV85Q1Zc9gn2n+ayXOizryhjSUBTdu5ih1tlVCJeuQENzc4ItyCVhINVXvIT/ZQ4mheGIsfBkpskg==", + "dev": true, + "requires": { + "@babel/template": "^7.7.4", + "@babel/traverse": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/parser": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.4.tgz", + "integrity": "sha512-jIwvLO0zCL+O/LmEJQjWA75MQTWwx3c3u2JOTDK5D3/9egrWRRA0/0hk9XXywYnXZVVpzrBYeIQTmhwUaePI9g==", + "dev": true + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.7.4.tgz", + "integrity": "sha512-1ypyZvGRXriY/QP668+s8sFr2mqinhkRDMPSQLNghCQE+GAkFtp+wkHVvg2+Hdki8gwP+NFzJBJ/N1BfzCCDEw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-remap-async-to-generator": "^7.7.4", + "@babel/plugin-syntax-async-generators": "^7.7.4" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.7.4.tgz", + "integrity": "sha512-wQvt3akcBTfLU/wYoqm/ws7YOAQKu8EVJEvHip/mzkNtjaclQoCCIqKXFP5/eyfnfbQCDV3OLRIK3mIVyXuZlw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-json-strings": "^7.7.4" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.7.4.tgz", + "integrity": "sha512-rnpnZR3/iWKmiQyJ3LKJpSwLDcX/nSXhdLk4Aq/tXOApIvyu7qoabrige0ylsAJffaUC51WiBu209Q0U+86OWQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-object-rest-spread": "^7.7.4" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.7.4.tgz", + "integrity": "sha512-DyM7U2bnsQerCQ+sejcTNZh8KQEUuC3ufzdnVnSiUv/qoGJp2Z3hanKL18KDhsBT5Wj6a7CMT5mdyCNJsEaA9w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.7.4" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.7.4.tgz", + "integrity": "sha512-cHgqHgYvffluZk85dJ02vloErm3Y6xtH+2noOBOJ2kXOJH3aVCDnj5eR/lVNlTnYu4hndAPJD3rTFjW3qee0PA==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.7.4.tgz", + "integrity": "sha512-Li4+EjSpBgxcsmeEF8IFcfV/+yJGxHXDirDkEoyFjumuwbmfCVHUt0HuowD/iGM7OhIRyXJH9YXxqiH6N815+g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.7.4.tgz", + "integrity": "sha512-QpGupahTQW1mHRXddMG5srgpHWqRLwJnJZKXTigB9RPFCCGbDGCgBeM/iC82ICXp414WeYx/tD54w7M2qRqTMg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.7.4.tgz", + "integrity": "sha512-mObR+r+KZq0XhRVS2BrBKBpr5jqrqzlPvS9C9vuOf5ilSwzloAl7RPWLrgKdWS6IreaVrjHxTjtyqFiOisaCwg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.7.4.tgz", + "integrity": "sha512-4ZSuzWgFxqHRE31Glu+fEr/MirNZOMYmD/0BhBWyLyOOQz/gTAl7QmWm2hX1QxEIXsr2vkdlwxIzTyiYRC4xcQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.7.4.tgz", + "integrity": "sha512-zUXy3e8jBNPiffmqkHRNDdZM2r8DWhCB7HhcoyZjiK1TxYEluLHAvQuYnTT+ARqRpabWqy/NHkO6e3MsYB5YfA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.7.4.tgz", + "integrity": "sha512-zpUTZphp5nHokuy8yLlyafxCJ0rSlFoSHypTUWgpdwoDXWQcseaect7cJ8Ppk6nunOM6+5rPMkod4OYKPR5MUg==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-remap-async-to-generator": "^7.7.4" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.7.4.tgz", + "integrity": "sha512-kqtQzwtKcpPclHYjLK//3lH8OFsCDuDJBaFhVwf8kqdnF6MN4l618UDlcA7TfRs3FayrHj+svYnSX8MC9zmUyQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.7.4.tgz", + "integrity": "sha512-2VBe9u0G+fDt9B5OV5DQH4KBf5DoiNkwFKOz0TCvBWvdAN2rOykCTkrL+jTLxfCAm76l9Qo5OqL7HBOx2dWggg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "lodash": "^4.17.13" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.7.4.tgz", + "integrity": "sha512-sK1mjWat7K+buWRuImEzjNf68qrKcrddtpQo3swi9j7dUcG6y6R6+Di039QN2bD1dykeswlagupEmpOatFHHUg==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.7.4", + "@babel/helper-define-map": "^7.7.4", + "@babel/helper-function-name": "^7.7.4", + "@babel/helper-optimise-call-expression": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.7.4", + "@babel/helper-split-export-declaration": "^7.7.4", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.7.4.tgz", + "integrity": "sha512-bSNsOsZnlpLLyQew35rl4Fma3yKWqK3ImWMSC/Nc+6nGjC9s5NFWAer1YQ899/6s9HxO2zQC1WoFNfkOqRkqRQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.7.4.tgz", + "integrity": "sha512-4jFMXI1Cu2aXbcXXl8Lr6YubCn6Oc7k9lLsu8v61TZh+1jny2BWmdtvY9zSUlLdGUvcy9DMAWyZEOqjsbeg/wA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.7.4.tgz", + "integrity": "sha512-mk0cH1zyMa/XHeb6LOTXTbG7uIJ8Rrjlzu91pUx/KS3JpcgaTDwMS8kM+ar8SLOvlL2Lofi4CGBAjCo3a2x+lw==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.7.4.tgz", + "integrity": "sha512-g1y4/G6xGWMD85Tlft5XedGaZBCIVN+/P0bs6eabmcPP9egFleMAo65OOjlhcz1njpwagyY3t0nsQC9oTFegJA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.7.4.tgz", + "integrity": "sha512-MCqiLfCKm6KEA1dglf6Uqq1ElDIZwFuzz1WH5mTf8k2uQSxEJMbOIEh7IZv7uichr7PMfi5YVSrr1vz+ipp7AQ==", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.7.4.tgz", + "integrity": "sha512-zZ1fD1B8keYtEcKF+M1TROfeHTKnijcVQm0yO/Yu1f7qoDoxEIc/+GX6Go430Bg84eM/xwPFp0+h4EbZg7epAA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.7.4.tgz", + "integrity": "sha512-E/x09TvjHNhsULs2IusN+aJNRV5zKwxu1cpirZyRPw+FyyIKEHPXTsadj48bVpc1R5Qq1B5ZkzumuFLytnbT6g==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.7.4.tgz", + "integrity": "sha512-X2MSV7LfJFm4aZfxd0yLVFrEXAgPqYoDG53Br/tCKiKYfX0MjVjQeWPIhPHHsCqzwQANq+FLN786fF5rgLS+gw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.7.4.tgz", + "integrity": "sha512-9VMwMO7i69LHTesL0RdGy93JU6a+qOPuvB4F4d0kR0zyVjJRVJRaoaGjhtki6SzQUu8yen/vxPKN6CWnCUw6bA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.7.4.tgz", + "integrity": "sha512-/542/5LNA18YDtg1F+QHvvUSlxdvjZoD/aldQwkq+E3WCkbEjNSN9zdrOXaSlfg3IfGi22ijzecklF/A7kVZFQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0", + "babel-plugin-dynamic-import-node": "^2.3.0" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.7.4.tgz", + "integrity": "sha512-k8iVS7Jhc367IcNF53KCwIXtKAH7czev866ThsTgy8CwlXjnKZna2VHwChglzLleYrcHz1eQEIJlGRQxB53nqA==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-simple-access": "^7.7.4", + "babel-plugin-dynamic-import-node": "^2.3.0" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.7.4.tgz", + "integrity": "sha512-y2c96hmcsUi6LrMqvmNDPBBiGCiQu0aYqpHatVVu6kD4mFEXKjyNxd/drc18XXAf9dv7UXjrZwBVmTTGaGP8iw==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0", + "babel-plugin-dynamic-import-node": "^2.3.0" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.7.4.tgz", + "integrity": "sha512-u2B8TIi0qZI4j8q4C51ktfO7E3cQ0qnaXFI1/OXITordD40tt17g/sXqgNNCcMTcBFKrUPcGDx+TBJuZxLx7tw==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.7.4.tgz", + "integrity": "sha512-jBUkiqLKvUWpv9GLSuHUFYdmHg0ujC1JEYoZUfeOOfNydZXp1sXObgyPatpcwjWgsdBGsagWW0cdJpX/DO2jMw==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.7.4" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.7.4.tgz", + "integrity": "sha512-CnPRiNtOG1vRodnsyGX37bHQleHE14B9dnnlgSeEs3ek3fHN1A1SScglTCg1sfbe7sRQ2BUcpgpTpWSfMKz3gg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.7.4.tgz", + "integrity": "sha512-ho+dAEhC2aRnff2JCA0SAK7V2R62zJd/7dmtoe7MHcso4C2mS+vZjn1Pb1pCVZvJs1mgsvv5+7sT+m3Bysb6eg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.7.4" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.7.4.tgz", + "integrity": "sha512-VJwhVePWPa0DqE9vcfptaJSzNDKrWU/4FbYCjZERtmqEs05g3UMXnYMZoXja7JAJ7Y7sPZipwm/pGApZt7wHlw==", + "dev": true, + "requires": { + "@babel/helper-call-delegate": "^7.7.4", + "@babel/helper-get-function-arity": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.7.4.tgz", + "integrity": "sha512-MatJhlC4iHsIskWYyawl53KuHrt+kALSADLQQ/HkhTjX954fkxIEh4q5slL4oRAnsm/eDoZ4q0CIZpcqBuxhJQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.7.4.tgz", + "integrity": "sha512-e7MWl5UJvmPEwFJTwkBlPmqixCtr9yAASBqff4ggXTNicZiwbF8Eefzm6NVgfiBp7JdAGItecnctKTgH44q2Jw==", + "dev": true, + "requires": { + "regenerator-transform": "^0.14.0" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.7.4.tgz", + "integrity": "sha512-OrPiUB5s5XvkCO1lS7D8ZtHcswIC57j62acAnJZKqGGnHP+TIc/ljQSrgdX/QyOTdEK5COAhuc820Hi1q2UgLQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.7.4.tgz", + "integrity": "sha512-q+suddWRfIcnyG5YiDP58sT65AJDZSUhXQDZE3r04AuqD6d/XLaQPPXSBzP2zGerkgBivqtQm9XKGLuHqBID6Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.7.4.tgz", + "integrity": "sha512-8OSs0FLe5/80cndziPlg4R0K6HcWSM0zyNhHhLsmw/Nc5MaA49cAsnoJ/t/YZf8qkG7fD+UjTRaApVDB526d7Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.7.4.tgz", + "integrity": "sha512-Ls2NASyL6qtVe1H1hXts9yuEeONV2TJZmplLONkMPUG158CtmnrzW5Q5teibM5UVOFjG0D3IC5mzXR6pPpUY7A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-regex": "^7.0.0" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.7.4.tgz", + "integrity": "sha512-sA+KxLwF3QwGj5abMHkHgshp9+rRz+oY9uoRil4CyLtgEuE/88dpkeWgNk5qKVsJE9iSfly3nvHapdRiIS2wnQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.7.4.tgz", + "integrity": "sha512-KQPUQ/7mqe2m0B8VecdyaW5XcQYaePyl9R7IsKd+irzj6jvbhoGnRE+M0aNkyAzI07VfUQ9266L5xMARitV3wg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.7.4.tgz", + "integrity": "sha512-N77UUIV+WCvE+5yHw+oks3m18/umd7y392Zv7mYTpFqHtkpcc+QUz+gLJNTWVlWROIWeLqY0f3OjZxV5TcXnRw==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/preset-env": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.7.4.tgz", + "integrity": "sha512-Dg+ciGJjwvC1NIe/DGblMbcGq1HOtKbw8RLl4nIjlfcILKEOkWT/vRqPpumswABEBVudii6dnVwrBtzD7ibm4g==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-async-generator-functions": "^7.7.4", + "@babel/plugin-proposal-dynamic-import": "^7.7.4", + "@babel/plugin-proposal-json-strings": "^7.7.4", + "@babel/plugin-proposal-object-rest-spread": "^7.7.4", + "@babel/plugin-proposal-optional-catch-binding": "^7.7.4", + "@babel/plugin-proposal-unicode-property-regex": "^7.7.4", + "@babel/plugin-syntax-async-generators": "^7.7.4", + "@babel/plugin-syntax-dynamic-import": "^7.7.4", + "@babel/plugin-syntax-json-strings": "^7.7.4", + "@babel/plugin-syntax-object-rest-spread": "^7.7.4", + "@babel/plugin-syntax-optional-catch-binding": "^7.7.4", + "@babel/plugin-syntax-top-level-await": "^7.7.4", + "@babel/plugin-transform-arrow-functions": "^7.7.4", + "@babel/plugin-transform-async-to-generator": "^7.7.4", + "@babel/plugin-transform-block-scoped-functions": "^7.7.4", + "@babel/plugin-transform-block-scoping": "^7.7.4", + "@babel/plugin-transform-classes": "^7.7.4", + "@babel/plugin-transform-computed-properties": "^7.7.4", + "@babel/plugin-transform-destructuring": "^7.7.4", + "@babel/plugin-transform-dotall-regex": "^7.7.4", + "@babel/plugin-transform-duplicate-keys": "^7.7.4", + "@babel/plugin-transform-exponentiation-operator": "^7.7.4", + "@babel/plugin-transform-for-of": "^7.7.4", + "@babel/plugin-transform-function-name": "^7.7.4", + "@babel/plugin-transform-literals": "^7.7.4", + "@babel/plugin-transform-member-expression-literals": "^7.7.4", + "@babel/plugin-transform-modules-amd": "^7.7.4", + "@babel/plugin-transform-modules-commonjs": "^7.7.4", + "@babel/plugin-transform-modules-systemjs": "^7.7.4", + "@babel/plugin-transform-modules-umd": "^7.7.4", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.7.4", + "@babel/plugin-transform-new-target": "^7.7.4", + "@babel/plugin-transform-object-super": "^7.7.4", + "@babel/plugin-transform-parameters": "^7.7.4", + "@babel/plugin-transform-property-literals": "^7.7.4", + "@babel/plugin-transform-regenerator": "^7.7.4", + "@babel/plugin-transform-reserved-words": "^7.7.4", + "@babel/plugin-transform-shorthand-properties": "^7.7.4", + "@babel/plugin-transform-spread": "^7.7.4", + "@babel/plugin-transform-sticky-regex": "^7.7.4", + "@babel/plugin-transform-template-literals": "^7.7.4", + "@babel/plugin-transform-typeof-symbol": "^7.7.4", + "@babel/plugin-transform-unicode-regex": "^7.7.4", + "@babel/types": "^7.7.4", + "browserslist": "^4.6.0", + "core-js-compat": "^3.1.1", + "invariant": "^2.2.2", + "js-levenshtein": "^1.1.3", + "semver": "^5.5.0" + } + }, + "@babel/template": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.7.4.tgz", + "integrity": "sha512-qUzihgVPguAzXCK7WXw8pqs6cEwi54s3E+HrejlkuWO6ivMKx9hZl3Y2fSXp9i5HgyWmj7RKP+ulaYnKM4yYxw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/traverse": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.7.4.tgz", + "integrity": "sha512-P1L58hQyupn8+ezVA2z5KBm4/Zr4lCC8dwKCMYzsa5jFMDMQAzaBNy9W5VjB+KAmBjb40U7a/H6ao+Xo+9saIw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.5.5", + "@babel/generator": "^7.7.4", + "@babel/helper-function-name": "^7.7.4", + "@babel/helper-split-export-declaration": "^7.7.4", + "@babel/parser": "^7.7.4", + "@babel/types": "^7.7.4", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.7.4.tgz", + "integrity": "sha512-cz5Ji23KCi4T+YIE/BolWosrJuSmoZeN1EFnRtBwF+KKLi8GG/Z2c2hOJJeCXPk4mwk4QFvTmwIodJowXgttRA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "@types/node": { + "version": "12.12.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.14.tgz", + "integrity": "sha512-u/SJDyXwuihpwjXy7hOOghagLEV1KdAST6syfnOk6QZAMzZuWZqXy5aYYZbh8Jdpd4escVFP0MvftHNDb9pruA==", + "dev": true + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "core-js-compat": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.4.3.tgz", + "integrity": "sha512-HgW7pxjAdq6EMxT5uOhbTfjgZo8fvWn0D6QXVlWs3z1Eerux7tRrJ4gcgnZNn6S0OMPHlClJuIEPp0hnow44Sg==", + "dev": true, + "requires": { + "browserslist": "^4.7.3", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, "lcid": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", @@ -4903,46 +5745,6 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, - "nock": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/nock/-/nock-9.2.3.tgz", - "integrity": "sha512-4XYNSJDJ/PvNoH+cCRWcGOOFsq3jtZdNTRIlPIBA7CopGWJO56m5OaPEjjJ3WddxNYfe5HL9sQQAtMt8oyR9AA==", - "dev": true, - "requires": { - "chai": "^4.1.2", - "debug": "^3.1.0", - "deep-equal": "^1.0.0", - "json-stringify-safe": "^5.0.1", - "lodash": "^4.17.5", - "mkdirp": "^0.5.0", - "propagate": "^1.0.0", - "qs": "^6.5.1", - "semver": "^5.5.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, "node-cache": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-4.2.1.tgz", @@ -5121,6 +5923,18 @@ "isobject": "^3.0.0" } }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, "object.getownpropertydescriptors": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", @@ -5336,12 +6150,6 @@ "pify": "^3.0.0" } }, - "pathval": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", - "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", - "dev": true - }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -5438,12 +6246,6 @@ "sisteransi": "^1.0.0" } }, - "propagate": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz", - "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=", - "dev": true - }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -5469,12 +6271,6 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" }, - "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", - "dev": true - }, "react-is": { "version": "16.8.6", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", @@ -5561,6 +6357,21 @@ "regenerate": "^1.4.0" } }, + "regenerator-runtime": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", + "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==", + "dev": true + }, + "regenerator-transform": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.1.tgz", + "integrity": "sha512-flVuee02C3FKRISbxhXl9mGzdbWUVHubl1SMaknjxkFB1/iqpJhArQUvRxOOPEc/9tAiX0BaQ28FJH10E4isSQ==", + "dev": true, + "requires": { + "private": "^0.1.6" + } + }, "regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", @@ -5583,6 +6394,43 @@ "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", "dev": true }, + "regexpu-core": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.6.0.tgz", + "integrity": "sha512-YlVaefl8P5BnFYOITTNzDvan1ulLOiXJzCNZxduTIosN17b87h3bvG9yHMoHaRuo88H4mQ06Aodj5VtYGGGiTg==", + "dev": true, + "requires": { + "regenerate": "^1.4.0", + "regenerate-unicode-properties": "^8.1.0", + "regjsgen": "^0.5.0", + "regjsparser": "^0.6.0", + "unicode-match-property-ecmascript": "^1.0.4", + "unicode-match-property-value-ecmascript": "^1.1.0" + } + }, + "regjsgen": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.1.tgz", + "integrity": "sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg==", + "dev": true + }, + "regjsparser": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.0.tgz", + "integrity": "sha512-RQ7YyokLiQBomUJuUG8iGVvkgOLxwyZM8k6d3q5SAXpg4r5TZJZigKFvC6PpD+qQ98bCDC5YelPeA3EucDoNeQ==", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + } + } + }, "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -6498,12 +7346,6 @@ "prelude-ls": "~1.1.2" } }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, "typescript": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.0.1.tgz", @@ -6511,9 +7353,9 @@ "dev": true }, "uglify-js": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.3.tgz", - "integrity": "sha512-KfQUgOqTkLp2aZxrMbCuKCDGW9slFYu2A23A36Gs7sGzTLcRBDORdOi5E21KWHFIfkY8kzgi/Pr1cXCh0yIp5g==", + "version": "3.6.9", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.9.tgz", + "integrity": "sha512-pcnnhaoG6RtrvHJ1dFncAe8Od6Nuy30oaJ82ts6//sGSXOP5UjBMEthiProjXmMNHOfd93sqlkztifFMcb+4yw==", "dev": true, "optional": true, "requires": { diff --git a/package.json b/package.json index 5d86bcc..719f431 100644 --- a/package.json +++ b/package.json @@ -48,8 +48,7 @@ "eslint-formatter-pretty": "2.1.1", "jest": "24.7.1", "jest-junit": "6.3.0", - "nock": "9.2.3", - "selfsigned": "1.10.4", + "launchdarkly-js-test-helpers": "1.0.0", "tmp": "0.0.33", "typescript": "3.0.1" }, diff --git a/test-types.ts b/test-types.ts index c56f8af..3058318 100644 --- a/test-types.ts +++ b/test-types.ts @@ -1,8 +1,11 @@ // This file exists only so that we can run the TypeScript compiler in the CI build -// to validate our index.d.ts file. +// to validate our index.d.ts file. This code will not actually be run - the point is +// just to verify that the type declarations exist and are correct so a TypeScript +// developer can use all of the SDK features. import * as ld from 'launchdarkly-node-server-sdk'; +import * as redis from 'redis'; var logger: ld.LDLogger = { error: (...args) => { }, @@ -80,3 +83,10 @@ client.variation('key', user, 2).then((value: ld.LDFlagValue) => { }); client.variation('key', user, 'default').then((value: ld.LDFlagValue) => { }); client.variationDetail('key', user, 'default').then((detail: ld.LDEvaluationDetail) => { }); client.allFlags(user).then((flagSet: ld.LDFlagSet) => { }); + +// Redis integration +var redisStore0 = ld.RedisFeatureStore(); +var myRedisOpts: redis.ClientOpts = {}; +var redisStore1 = ld.RedisFeatureStore(myRedisOpts, 30, 'prefix', logger); +var myRedisClient: redis.RedisClient = new redis.RedisClient(myRedisOpts); +var redisStore2 = ld.RedisFeatureStore(undefined, 30, 'prefix', logger, myRedisClient); diff --git a/test/LDClient-end-to-end-test.js b/test/LDClient-end-to-end-test.js new file mode 100644 index 0000000..fe23eaf --- /dev/null +++ b/test/LDClient-end-to-end-test.js @@ -0,0 +1,115 @@ +const LDClient = require('../index.js'); +import { AsyncQueue, TestHttpHandlers, TestHttpServer, withCloseable } from 'launchdarkly-js-test-helpers'; +import { stubLogger } from './stubs'; + +async function withAllServers(asyncCallback) { + return await withCloseable(TestHttpServer.start, async pollingServer => + withCloseable(TestHttpServer.start, async streamingServer => + withCloseable(TestHttpServer.start, async eventsServer => { + const servers = { polling: pollingServer, streaming: streamingServer, events: eventsServer }; + const baseConfig = { + baseUri: pollingServer.url, + streamUri: streamingServer.url, + eventsUri: eventsServer.url, + logger: stubLogger() + }; + return await asyncCallback(servers, baseConfig); + }) + ) + ); +} + +describe('LDClient end-to-end', () => { + const sdkKey = 'sdkKey'; + const flagKey = 'flagKey'; + const expectedFlagValue = 'yes'; + const flag = { + key: flagKey, + version: 1, + on: false, + offVariation: 0, + variations: [ expectedFlagValue, 'no' ] + }; + const allData = { flags: { flagKey: flag }, segments: {} }; + + const user = { key: 'userKey' }; + + it('starts in polling mode', async () => { + await withAllServers(async (servers, config) => { + servers.polling.forMethodAndPath('get', '/sdk/latest-all', TestHttpHandlers.respondJson(allData)); + servers.events.forMethodAndPath('post', '/bulk', TestHttpHandlers.respond(200)); + + config.stream = false; + await withCloseable(LDClient.init(sdkKey, config), async client => { + await client.waitForInitialization(); + expect(client.initialized()).toBe(true); + + const value = await client.variation(flag.key, user); + expect(value).toEqual(expectedFlagValue); + + await client.flush(); + }); + + expect(servers.polling.requestCount()).toEqual(1); + expect(servers.streaming.requestCount()).toEqual(0); + expect(servers.events.requestCount()).toEqual(1); + }); + }); + + it('fails in polling mode with 401 error', async () => { + await withAllServers(async (servers, config) => { + servers.polling.forMethodAndPath('get', '/sdk/latest-all', TestHttpHandlers.respond(401)); + servers.events.forMethodAndPath('post', '/bulk', TestHttpHandlers.respond(200)); + + config.stream = false; + + await withCloseable(LDClient.init(sdkKey, config), async client => { + await expect(client.waitForInitialization()).rejects.toThrow(); + expect(client.initialized()).toBe(false); + }); + + expect(servers.polling.requestCount()).toEqual(1); + expect(servers.streaming.requestCount()).toEqual(0); + }); + }); + + it('starts in streaming mode', async () => { + await withAllServers(async (servers, config) => { + const streamEvent = { type: 'put', data: JSON.stringify({ data: allData }) }; + await withCloseable(new AsyncQueue(), async events => { + events.add(streamEvent); + servers.streaming.forMethodAndPath('get', '/all', TestHttpHandlers.sseStream(events)); + servers.events.forMethodAndPath('post', '/bulk', TestHttpHandlers.respond(200)); + + await withCloseable(LDClient.init(sdkKey, config), async client => { + await client.waitForInitialization(); + expect(client.initialized()).toBe(true); + + const value = await client.variation(flag.key, user); + expect(value).toEqual(expectedFlagValue); + + await client.flush(); + }); + + expect(servers.polling.requestCount()).toEqual(0); + expect(servers.streaming.requestCount()).toEqual(1); + expect(servers.events.requestCount()).toEqual(1); + }); + }); + }); + + it('fails in streaming mode with 401 error', async () => { + await withAllServers(async (servers, config) => { + servers.streaming.forMethodAndPath('get', '/all', TestHttpHandlers.respond(401)); + servers.events.forMethodAndPath('post', '/bulk', TestHttpHandlers.respond(200)); + + await withCloseable(LDClient.init(sdkKey, config), async client => { + await expect(client.waitForInitialization()).rejects.toThrow(); + expect(client.initialized()).toBe(false); + }); + + expect(servers.polling.requestCount()).toEqual(0); + expect(servers.streaming.requestCount()).toEqual(1); + }); + }); +}); diff --git a/test/LDClient-tls-test.js b/test/LDClient-tls-test.js index 5b24dc6..cb4ebd8 100644 --- a/test/LDClient-tls-test.js +++ b/test/LDClient-tls-test.js @@ -1,125 +1,104 @@ -import * as selfsigned from 'selfsigned'; - import * as LDClient from '../index'; -import { sleepAsync } from './async_utils'; -import * as httpServer from './http_server'; + +import { + AsyncQueue, + sleepAsync, + TestHttpHandlers, + TestHttpServer, + withCloseable +} from 'launchdarkly-js-test-helpers'; import * as stubs from './stubs'; describe('LDClient TLS configuration', () => { const sdkKey = 'secret'; let logger = stubs.stubLogger(); - let server; - let certData; - beforeEach(async () => { - certData = await makeSelfSignedPems(); - const serverOptions = { key: certData.private, cert: certData.cert, ca: certData.public }; - server = await httpServer.createServer(true, serverOptions); - }); + it('can connect via HTTPS to a server with a self-signed certificate, if CA is specified', async () => { + await withCloseable(TestHttpServer.startSecure, async server => { + server.forMethodAndPath('get', '/sdk/latest-all', TestHttpHandlers.respondJson({})); - afterEach(() => { - httpServer.closeServers(); + const config = { + baseUri: server.url, + sendEvents: false, + stream: false, + logger: stubs.stubLogger(), + tlsParams: { ca: server.certificate }, + }; + + await withCloseable(LDClient.init(sdkKey, config), async client => { + await client.waitForInitialization(); + }); + }); }); - async function makeSelfSignedPems() { - const certAttrs = [{ name: 'commonName', value: 'localhost' }]; - const certOptions = { - // This part is based on code within the selfsigned package - extensions: [ - { - name: 'subjectAltName', - altNames: [{ type: 6, value: 'https://localhost' }], - }, - ], - }; - return await selfsigned.generate(certAttrs, certOptions); - } + it('cannot connect via HTTPS to a server with a self-signed certificate, using default config', async () => { + await withCloseable(TestHttpServer.startSecure, async server => { + server.forMethodAndPath('get', '/sdk/latest-all', TestHttpHandlers.respondJson({})); - it('can connect via HTTPS to a server with a self-signed certificate, if CA is specified', async () => { - httpServer.autoRespond(server, res => httpServer.respondJson(res, {})); - const config = { - baseUri: server.url, - sendEvents: false, - stream: false, - logger: stubs.stubLogger(), - tlsParams: { ca: certData.cert }, - }; - const client = LDClient.init(sdkKey, config); - await client.waitForInitialization(); - client.close(); - }); + const config = { + baseUri: server.url, + sendEvents: false, + stream: false, + logger: stubs.stubLogger(), + }; - it('cannot connect via HTTPS to a server with a self-signed certificate, using default config', async () => { - httpServer.autoRespond(server, res => httpServer.respondJson(res, {})); - const config = { - baseUri: server.url, - sendEvents: false, - stream: false, - logger: stubs.stubLogger(), - }; - const client = LDClient.init(sdkKey, config); - await sleepAsync(300); // the client won't signal an unrecoverable error, but it should log a message - expect(config.logger.warn.mock.calls.length).toEqual(2); - expect(config.logger.warn.mock.calls[1][0]).toMatch(/self signed/); + await withCloseable(LDClient.init(sdkKey, config), async client => { + await sleepAsync(300); // the client won't signal an unrecoverable error, but it should log a message + expect(config.logger.warn.mock.calls.length).toEqual(2); + expect(config.logger.warn.mock.calls[1][0]).toMatch(/self signed/); + }); + }); }); it('can use custom TLS options for streaming as well as polling', async () => { - const eventData = { data: { flags: { flag: { version: 1 } }, segments: {} } }; - server.on('request', (req, res) => { - if (req.url.match(/\/stream/)) { - httpServer.respondSSEEvent(res, 'put', eventData); - } else { - httpServer.respondJson(res, {}); - } - }); + await withCloseable(TestHttpServer.startSecure, async server => { + const eventData = { data: { flags: { flag: { version: 1 } }, segments: {} } }; + await withCloseable(new AsyncQueue(), async events => { + events.add({ type: 'put', data: JSON.stringify(eventData) }); + server.forMethodAndPath('get', '/stream/all', TestHttpHandlers.sseStream(events)); - const config = { - baseUri: server.url, - streamUri: server.url + '/stream', - sendEvents: false, - logger: logger, - tlsParams: { ca: certData.cert }, - }; + const config = { + baseUri: server.url, + streamUri: server.url + '/stream', + sendEvents: false, + logger: logger, + tlsParams: { ca: server.certificate }, + }; - const client = LDClient.init(sdkKey, config); - await client.waitForInitialization(); // this won't return until the stream receives the "put" event - client.close(); + await withCloseable(LDClient.init(sdkKey, config), async client => { + await client.waitForInitialization(); // this won't return until the stream receives the "put" event + }); + }); + }); }); it('can use custom TLS options for posting events', async () => { - let receivedEventFn; - const receivedEvent = new Promise(resolve => { - receivedEventFn = resolve; - }); + await withCloseable(TestHttpServer.startSecure, async server => { + server.forMethodAndPath('post', '/events/bulk', TestHttpHandlers.respond(200)); + server.forMethodAndPath('get', '/sdk/latest-all', TestHttpHandlers.respondJson({})); - server.on('request', (req, res) => { - if (req.url.match(/\/events/)) { - httpServer.readAll(req).then(body => { - receivedEventFn(body); - httpServer.respond(res, 200); - }); - } else { - httpServer.respondJson(res, {}); - } - }); + const config = { + baseUri: server.url, + eventsUri: server.url + '/events', + stream: false, + logger: stubs.stubLogger(), + tlsParams: { ca: server.certificate }, + }; - const config = { - baseUri: server.url, - eventsUri: server.url + '/events', - stream: false, - logger: stubs.stubLogger(), - tlsParams: { ca: certData.cert }, - }; + await withCloseable(LDClient.init(sdkKey, config), async client => { + await client.waitForInitialization(); + client.identify({ key: 'user' }); + await client.flush(); - const client = LDClient.init(sdkKey, config); - await client.waitForInitialization(); - client.identify({ key: 'user' }); - await client.flush(); - - const receivedEventBody = await receivedEvent; - const eventData = JSON.parse(receivedEventBody); - expect(eventData.length).toEqual(1); - expect(eventData[0].kind).toEqual('identify'); - client.close(); + const flagsRequest = await server.nextRequest(); + expect(flagsRequest.path).toEqual('/sdk/latest-all'); + + const eventsRequest = await server.nextRequest(); + expect(eventsRequest.path).toEqual('/events/bulk'); + const eventData = JSON.parse(eventsRequest.body); + expect(eventData.length).toEqual(1); + expect(eventData[0].kind).toEqual('identify'); + }); + }); }); }); diff --git a/test/async_utils.js b/test/async_utils.js deleted file mode 100644 index f748172..0000000 --- a/test/async_utils.js +++ /dev/null @@ -1,53 +0,0 @@ - -// Converts a function that takes a single-parameter callback (like most SDK methods) into a Promise. -// Usage: asyncify(callback => doSomething(params, callback)) -function asyncify(f) { - return new Promise(resolve => f(resolve)); -} - -// Converts a function that takes a Node-style callback (err, result) into a Promise. -// Usage: asyncifyNode(callback => doSomething(params, callback)) -function asyncifyNode(f) { - return new Promise((resolve, reject) => f((err, result) => err ? reject(err) : resolve(result))); -} - -function sleepAsync(millis) { - return new Promise(resolve => { - setTimeout(resolve, millis); - }); -} - -function AsyncQueue() { - const items = []; - const awaiters = []; - - return { - add: item => { - if (awaiters.length) { - awaiters.shift()(item); - } else { - items.push(item); - } - }, - - take: () => { - if (items.length) { - return Promise.resolve(items.shift()); - } - return new Promise(resolve => { - awaiters.push(resolve); - }); - }, - - isEmpty: () => { - return items.length === 0; - } - }; -} - -module.exports = { - asyncify: asyncify, - asyncifyNode: asyncifyNode, - sleepAsync: sleepAsync, - AsyncQueue: AsyncQueue -}; diff --git a/test/caching_store_wrapper-test.js b/test/caching_store_wrapper-test.js index 0696011..240ee82 100644 --- a/test/caching_store_wrapper-test.js +++ b/test/caching_store_wrapper-test.js @@ -1,7 +1,7 @@ var CachingStoreWrapper = require('../caching_store_wrapper'); var features = require('../versioned_data_kind').features; var segments = require('../versioned_data_kind').segments; -const { asyncify, sleepAsync } = require('./async_utils'); +const { promisifySingle, sleepAsync } = require('launchdarkly-js-test-helpers'); function MockCore() { const c = { @@ -104,12 +104,12 @@ describe('CachingStoreWrapper', function() { core.forceSet(features, flagv1); - var item = await asyncify(cb => wrapper.get(features, flagv1.key, cb)); + var item = await promisifySingle(wrapper.get)(features, flagv1.key); expect(item).toEqual(flagv1); core.forceSet(features, flagv2); // Make a change that bypasses the cache - item = await asyncify(cb => wrapper.get(features, flagv1.key, cb)); + item = await promisifySingle(wrapper.get)(features, flagv1.key); // If cached, it should return the cached value rather than calling the underlying getter expect(item).toEqual(isCached ? flagv1 : flagv2); }); @@ -120,12 +120,12 @@ describe('CachingStoreWrapper', function() { core.forceSet(features, flagv1); - var item = await asyncify(cb => wrapper.get(features, flagv1.key, cb)); + var item = await promisifySingle(wrapper.get)(features, flagv1.key); expect(item).toBe(null); core.forceSet(features, flagv2); // Make a change that bypasses the cache - item = await asyncify(cb => wrapper.get(features, flagv2.key, cb)); + item = await promisifySingle(wrapper.get)(features, flagv2.key); // If cached, the deleted state should persist in the cache expect(item).toEqual(isCached ? null : flagv2); }); @@ -133,12 +133,12 @@ describe('CachingStoreWrapper', function() { runCachedAndUncachedTests('get() with missing item', async (wrapper, core, isCached) => { const flag = { key: 'flag', version: 1 }; - var item = await asyncify(cb => wrapper.get(features, flag.key, cb)); + var item = await promisifySingle(wrapper.get)(features, flag.key); expect(item).toBe(null); core.forceSet(features, flag); - item = await asyncify(cb => wrapper.get(features, flag.key, cb)); + item = await promisifySingle(wrapper.get)(features, flag.key); // If cached, the previous null result should persist in the cache expect(item).toEqual(isCached ? null : flag); }); @@ -149,12 +149,12 @@ describe('CachingStoreWrapper', function() { const allData = { features: { 'flag': flagv1 } }; - await asyncify(cb => wrapper.init(allData, cb)); + await promisifySingle(wrapper.init)(allData); expect(core.data).toEqual(allData); core.forceSet(features, flagv2); - var item = await asyncify(cb => wrapper.get(features, flagv1.key, cb)); + var item = await promisifySingle(wrapper.get)(features, flagv1.key); expect(item).toEqual(flagv1); }); @@ -165,12 +165,12 @@ describe('CachingStoreWrapper', function() { core.forceSet(features, flag1); core.forceSet(features, flag2); - var items = await asyncify(cb => wrapper.all(features, cb)); + var items = await promisifySingle(wrapper.all)(features); expect(items).toEqual({ 'flag1': flag1, 'flag2': flag2 }); core.forceRemove(features, flag2.key); - items = await asyncify(cb => wrapper.all(features, cb)); + items = await promisifySingle(wrapper.all)(features); if (isCached) { expect(items).toEqual({ 'flag1': flag1, 'flag2': flag2 }); } else { @@ -185,12 +185,12 @@ describe('CachingStoreWrapper', function() { core.forceSet(features, flag1); core.forceSet(features, flag2); - var items = await asyncify(cb => wrapper.all(features, cb)); + var items = await promisifySingle(wrapper.all)(features); expect(items).toEqual({ 'flag1': flag1 }); core.forceRemove(features, flag1.key); - items = await asyncify(cb => wrapper.all(features, cb)); + items = await promisifySingle(wrapper.all)(features); if (isCached) { expect(items).toEqual({ 'flag1': flag1 }); } else { @@ -201,7 +201,7 @@ describe('CachingStoreWrapper', function() { runCachedAndUncachedTests('all() error condition', async (wrapper, core, isCached) => { core.getAllError = true; - var items = await asyncify(cb => wrapper.all(features, cb)); + var items = await promisifySingle(wrapper.all)(features); expect(items).toBe(null); }); @@ -211,10 +211,10 @@ describe('CachingStoreWrapper', function() { const allData = { features: { flag1: flag1, flag2: flag2 } }; - await asyncify(cb => wrapper.init(allData, cb)); + await promisifySingle(wrapper.init)(allData); core.forceRemove(features, flag2.key); - var items = await asyncify(cb => wrapper.all(features, cb)); + var items = await promisifySingle(wrapper.all)(features); expect(items).toEqual({ flag1: flag1, flag2: flag2 }); }); @@ -226,16 +226,16 @@ describe('CachingStoreWrapper', function() { const allData = { features: { flag1: flag1v1, flag2: flag2v2 } }; - await asyncify(cb => wrapper.init(allData, cb)); + await promisifySingle(wrapper.init)(allData); expect(core.data).toEqual(allData); // make a change to flag1 using the wrapper - this should flush the cache - await asyncify(cb => wrapper.upsert(features, flag1v2, cb)); + await promisifySingle(wrapper.upsert)(features, flag1v2); // make a change to flag2 that bypasses the cache core.forceSet(features, flag2v2); // we should now see both changes since the cache was flushed - var items = await asyncify(cb => wrapper.all(features, cb)); + var items = await promisifySingle(wrapper.all)(features); expect(items).toEqual({ flag1: flag1v2, flag2: flag2v2 }); }); @@ -243,10 +243,10 @@ describe('CachingStoreWrapper', function() { const flagv1 = { key: 'flag', version: 1 }; const flagv2 = { key: 'flag', version: 2 }; - await asyncify(cb => wrapper.upsert(features, flagv1, cb)); + await promisifySingle(wrapper.upsert)(features, flagv1); expect(core.data[features.namespace][flagv1.key]).toEqual(flagv1); - await asyncify(cb => wrapper.upsert(features, flagv2, cb)); + await promisifySingle(wrapper.upsert)(features, flagv2); expect(core.data[features.namespace][flagv1.key]).toEqual(flagv2); // if we have a cache, verify that the new item is now cached by writing a different value @@ -256,7 +256,7 @@ describe('CachingStoreWrapper', function() { core.forceSet(features, flagv3); } - var item = await asyncify(cb => wrapper.get(features, flagv1.key, cb)); + var item = await promisifySingle(wrapper.get)(features, flagv1.key); expect(item).toEqual(flagv2); }); @@ -264,12 +264,12 @@ describe('CachingStoreWrapper', function() { const flagv1 = { key: 'flag', version: 1 }; const flagv2 = { key: 'flag', version: 2 }; - await asyncify(cb => wrapper.upsert(features, flagv1, cb)); + await promisifySingle(wrapper.upsert)(features, flagv1); expect(core.data[features.namespace][flagv1.key]).toEqual(flagv1); core.upsertError = new Error('sorry'); - await asyncify(cb => wrapper.upsert(features, flagv2, cb)); + await promisifySingle(wrapper.upsert)(features, flagv2); expect(core.data[features.namespace][flagv1.key]).toEqual(flagv1); // if we have a cache, verify that the old item is still cached by writing a different value @@ -277,7 +277,7 @@ describe('CachingStoreWrapper', function() { if (isCached) { const flagv3 = { key: 'flag', version: 3 }; core.forceSet(features, flagv3); - var item = await asyncify(cb => wrapper.get(features, flagv1.key, cb)); + var item = await promisifySingle(wrapper.get)(features, flagv1.key); expect(item).toEqual(flagv1); } }); @@ -288,7 +288,7 @@ describe('CachingStoreWrapper', function() { core.forceSet(features, flagv2); // this is now in the underlying data, but not in the cache - await asyncify(cb => wrapper.upsert(features, flagv1, cb)); + await promisifySingle(wrapper.upsert)(features, flagv1); expect(core.data[features.namespace][flagv1.key]).toEqual(flagv2); // value in store remains the same // the cache should now contain flagv2 - check this by making another change that bypasses @@ -296,7 +296,7 @@ describe('CachingStoreWrapper', function() { const flagv3 = { key: 'flag', version: 3 }; core.forceSet(features, flagv3); - var item = await asyncify(cb => wrapper.get(features, flagv1.key, cb)); + var item = await promisifySingle(wrapper.get)(features, flagv1.key); expect(item).toEqual(flagv2); }); @@ -307,17 +307,17 @@ describe('CachingStoreWrapper', function() { core.forceSet(features, flagv1); - var item = await asyncify(cb => wrapper.get(features, flagv1.key, cb)); + var item = await promisifySingle(wrapper.get)(features, flagv1.key); expect(item).toEqual(flagv1); - await asyncify(cb => wrapper.delete(features, flagv1.key, flagv2.version, cb)); + await promisifySingle(wrapper.delete)(features, flagv1.key, flagv2.version); expect(core.data[features.namespace][flagv1.key]).toEqual(flagv2); // make a change to the flag that bypasses the cache core.forceSet(features, flagv3); - var item = await asyncify(cb => wrapper.get(features, flagv1.key, cb)); + var item = await promisifySingle(wrapper.get)(features, flagv1.key); expect(item).toEqual(isCached ? null : flagv3); }); @@ -326,19 +326,19 @@ describe('CachingStoreWrapper', function() { const core = MockCore(); const wrapper = new CachingStoreWrapper(core, 0); - var value = await asyncify(cb => wrapper.initialized(cb)); + var value = await promisifySingle(wrapper.initialized)(); expect(value).toEqual(false); expect(core.initQueriedCount).toEqual(1); core.inited = true; - value = await asyncify(cb => wrapper.initialized(cb)); + value = await promisifySingle(wrapper.initialized)(); expect(value).toEqual(true); expect(core.initQueriedCount).toEqual(2); core.inited = false; // this should have no effect since we already returned true - value = await asyncify(cb => wrapper.initialized(cb)); + value = await promisifySingle(wrapper.initialized)(); expect(value).toEqual(true); expect(core.initQueriedCount).toEqual(2); }); @@ -347,14 +347,14 @@ describe('CachingStoreWrapper', function() { const core = MockCore(); const wrapper = new CachingStoreWrapper(core, 0); - var value = await asyncify(cb => wrapper.initialized(cb)); + var value = await promisifySingle(wrapper.initialized)(); expect(value).toEqual(false); expect(core.initQueriedCount).toEqual(1); const allData = { features: {} }; - await asyncify(cb => wrapper.init(allData, cb)); + await promisifySingle(wrapper.init)(allData); - value = await asyncify(cb => wrapper.initialized(cb)); + value = await promisifySingle(wrapper.initialized)(); expect(value).toEqual(true); expect(core.initQueriedCount).toEqual(1); }); @@ -363,19 +363,19 @@ describe('CachingStoreWrapper', function() { const core = MockCore(); const wrapper = new CachingStoreWrapper(core, 1); // cache TTL = 1 second - var value = await asyncify(cb => wrapper.initialized(cb)); + var value = await promisifySingle(wrapper.initialized)(); expect(value).toEqual(false); expect(core.initQueriedCount).toEqual(1); core.inited = true; - value = await asyncify(cb => wrapper.initialized(cb)); + value = await promisifySingle(wrapper.initialized)(); expect(value).toEqual(false); expect(core.initQueriedCount).toEqual(1); await sleepAsync(1100); - value = await asyncify(cb => wrapper.initialized(cb)); + value = await promisifySingle(wrapper.initialized)(); expect(value).toEqual(true); expect(core.initQueriedCount).toEqual(2); }); @@ -402,7 +402,7 @@ describe('CachingStoreWrapper', function() { dependencyOrderingTestData[segments.namespace] = { o: { key: "o" } }; - await asyncify(cb => wrapper.init(dependencyOrderingTestData, cb)); + await promisifySingle(wrapper.init)(dependencyOrderingTestData); var receivedData = core.data; expect(receivedData.length).toEqual(2); diff --git a/test/configuration-test.js b/test/configuration-test.js index e9cfec3..39c2d37 100644 --- a/test/configuration-test.js +++ b/test/configuration-test.js @@ -1,23 +1,18 @@ var configuration = require('../configuration'); describe('configuration', function() { - function checkDefault(name, value) { - it('applies defaults correctly for "' + name + '"', function() { - var configWithUnspecifiedValue = {}; - expect(configuration.validate(configWithUnspecifiedValue)[name]).toEqual(value); - var configWithNullValue = {}; - configWithNullValue[name] = null; - expect(configuration.validate(configWithNullValue)[name]).toEqual(value); - var configWithSpecifiedValue = {}; - configWithSpecifiedValue[name] = 'value'; - expect(configuration.validate(configWithSpecifiedValue)[name]).toEqual('value'); - }); + const defaults = configuration.defaults(); + + function emptyConfigWithMockLogger() { + return { logger: { warn: jest.fn() } }; } - checkDefault('sendEvents', true); - checkDefault('stream', true); - checkDefault('offline', false); - checkDefault('useLdd', false); + function expectDefault(name) { + const configIn = emptyConfigWithMockLogger(); + const config = configuration.validate(configIn); + expect(config[name]).toBe(defaults[name]); + expect(configIn.logger.warn).not.toHaveBeenCalled(); + } function checkDeprecated(oldName, newName, value) { it('allows "' + oldName + '" as a deprecated equivalent to "' + newName + '"', function() { @@ -49,30 +44,126 @@ describe('configuration', function() { checkDeprecated('proxy_auth', 'proxyAuth', 'basic'); checkDeprecated('feature_store', 'featureStore', {}); + function checkBooleanProperty(name) { + it('enforces boolean type and default for "' + name + '"', () => { + expectDefault(name); + + const configIn1 = emptyConfigWithMockLogger(); + configIn1[name] = true; + const config1 = configuration.validate(configIn1); + expect(config1[name]).toBe(true); + expect(configIn1.logger.warn).not.toHaveBeenCalled(); + + const configIn2 = emptyConfigWithMockLogger(); + configIn2[name] = false; + const config2 = configuration.validate(configIn2); + expect(config2[name]).toBe(false); + expect(configIn2.logger.warn).not.toHaveBeenCalled(); + + const configIn3 = emptyConfigWithMockLogger(); + configIn3[name] = 'abc'; + const config3 = configuration.validate(configIn3); + expect(config3[name]).toBe(true); + expect(configIn3.logger.warn).toHaveBeenCalledTimes(1); + + const configIn4 = emptyConfigWithMockLogger(); + configIn4[name] = 0; + const config4 = configuration.validate(configIn4); + expect(config4[name]).toBe(false); + expect(configIn4.logger.warn).toHaveBeenCalledTimes(1); + }); + } + + checkBooleanProperty('stream'); + checkBooleanProperty('sendEvents'); + checkBooleanProperty('offline'); + checkBooleanProperty('useLdd'); + checkBooleanProperty('allAttributesPrivate'); + + function checkNumericProperty(name, validValue) { + it('enforces numeric type and default for "' + name + '"', () => { + expectDefault(name); + + const configIn1 = emptyConfigWithMockLogger(); + configIn1[name] = validValue; + const config1 = configuration.validate(configIn1); + expect(config1[name]).toBe(validValue); + expect(configIn1.logger.warn).not.toHaveBeenCalled(); + + const configIn2 = emptyConfigWithMockLogger(); + configIn2[name] = 'no'; + const config2 = configuration.validate(configIn2); + expect(config2[name]).toBe(defaults[name]); + expect(configIn2.logger.warn).toHaveBeenCalledTimes(1); + }); + } + + checkNumericProperty('timeout', 10); + checkNumericProperty('capacity', 500); + checkNumericProperty('flushInterval', 45); + checkNumericProperty('pollInterval', 45); + checkNumericProperty('userKeysCapacity', 500); + checkNumericProperty('userKeysFlushInterval', 45); + function checkUriProperty(name) { - var config0 = {}; - config0[name] = 'http://test.com/'; - var config1 = configuration.validate(config0); - expect(config1[name]).toEqual('http://test.com'); + expectDefault(name); + + const configIn1 = emptyConfigWithMockLogger(); + configIn1[name] = 'http://test.com/'; + const config1 = configuration.validate(configIn1); + expect(config1[name]).toEqual('http://test.com'); // trailing slash is removed + expect(configIn1.logger.warn).not.toHaveBeenCalled(); + + const configIn2 = emptyConfigWithMockLogger(); + configIn2[name] = 3; + const config2 = configuration.validate(configIn2); + expect(config2[name]).toEqual(defaults[name]); + expect(configIn2.logger.warn).toHaveBeenCalledTimes(1); } checkUriProperty('baseUri'); checkUriProperty('streamUri'); checkUriProperty('eventsUri'); - it('enforces minimum poll interval', function() { + it('enforces array value for privateAttributeNames', () => { + const configIn0 = emptyConfigWithMockLogger(); + const config0 = configuration.validate(configIn0); + expect(config0.privateAttributeNames).toEqual([]); + expect(configIn0.logger.warn).not.toHaveBeenCalled(); + + const configIn1 = emptyConfigWithMockLogger(); + configIn1.privateAttributeNames = [ 'a' ]; + const config1 = configuration.validate(configIn1); + expect(config1.privateAttributeNames).toEqual([ 'a' ]); + expect(configIn1.logger.warn).not.toHaveBeenCalled(); + + const configIn2 = emptyConfigWithMockLogger(); + configIn2.privateAttributeNames = 'no'; + const config2 = configuration.validate(configIn2); + expect(config2.privateAttributeNames).toEqual([]); + expect(configIn2.logger.warn).toHaveBeenCalledTimes(1); + }); + + it('enforces minimum poll interval', () => { var config = configuration.validate({ pollInterval: 29 }); expect(config.pollInterval).toEqual(30); }); - it('allows larger poll interval', function() { + it('allows larger poll interval', () => { var config = configuration.validate({ pollInterval: 31 }); expect(config.pollInterval).toEqual(31); }); - it('should not share the default featureStore across different config instances', function() { + it('should not share the default featureStore across different config instances', () => { var config1 = configuration.validate({}); var config2 = configuration.validate({}); expect(config1.featureStore).not.toEqual(config2.featureStore); }); + + it('complains if you set an unknown property', () => { + const configIn = emptyConfigWithMockLogger(); + configIn.unsupportedThing = true; + configuration.validate(configIn); + expect(configIn.logger.warn).toHaveBeenCalledTimes(1); + }); }); diff --git a/test/event_processor-test.js b/test/event_processor-test.js index 4978ccc..2745301 100644 --- a/test/event_processor-test.js +++ b/test/event_processor-test.js @@ -1,9 +1,8 @@ -const nock = require('nock'); const EventProcessor = require('../event_processor'); +const { sleepAsync, TestHttpHandlers, TestHttpServer, withCloseable } = require('launchdarkly-js-test-helpers'); describe('EventProcessor', () => { - let ep; const eventsUri = 'http://example.com'; const sdkKey = 'SDK_KEY'; const defaultConfig = { @@ -24,31 +23,20 @@ describe('EventProcessor', () => { const stringifiedNumericUser = { key: '1', secondary: '2', ip: '3', country: '4', email: '5', firstName: '6', lastName: '7', avatar: '8', name: '9', anonymous: false, custom: { age: 99 } }; - afterEach(() => { - if (ep) { + function eventsServerTest(asyncCallback) { + return async () => withCloseable(TestHttpServer.start, async server => { + server.forMethodAndPath('post', '/bulk', TestHttpHandlers.respond(200)); + return await asyncCallback(server); + }); + } + + async function withEventProcessor(config, server, asyncCallback) { + const ep = EventProcessor(sdkKey, Object.assign({}, config, { eventsUri: server.url })); + try { + return await asyncCallback(ep); + } finally { ep.close(); } - nock.cleanAll(); - }); - - function flushAndGetRequest(options, cb) { - const callback = cb || options; - options = cb ? options : {}; - let requestBody; - let requestHeaders; - nock(eventsUri).post('/bulk') - .reply(function(uri, body) { - requestBody = body; - requestHeaders = this.req.headers; - return [ options.status || 200, '', options.headers || {} ]; - }); - ep.flush().then( - () => { - callback(requestBody, requestHeaders); - }, - error => { - callback(requestBody, requestHeaders, error); - }); } function headersWithDate(timestamp) { @@ -94,295 +82,302 @@ describe('EventProcessor', () => { expect(e.kind).toEqual('summary'); } - it('queues identify event', done => { - ep = EventProcessor(sdkKey, defaultConfig); - const e = { kind: 'identify', creationDate: 1000, user: user }; - ep.sendEvent(e); + async function getJsonRequest(server) { + return JSON.parse((await server.nextRequest()).body); + } + + it('queues identify event', eventsServerTest(async s => { + await withEventProcessor(defaultConfig, s, async ep => { + const e = { kind: 'identify', creationDate: 1000, user: user }; + ep.sendEvent(e); + await ep.flush(); - flushAndGetRequest(output => { + const output = await getJsonRequest(s); expect(output).toEqual([{ kind: 'identify', creationDate: 1000, key: user.key, user: user }]); - done(); }); - }); + })); - it('filters user in identify event', done => { + it('filters user in identify event', eventsServerTest(async s => { const config = Object.assign({}, defaultConfig, { allAttributesPrivate: true }); - ep = EventProcessor(sdkKey, config); - const e = { kind: 'identify', creationDate: 1000, user: user }; - ep.sendEvent(e); + await withEventProcessor(config, s, async ep => { + const e = { kind: 'identify', creationDate: 1000, user: user }; + ep.sendEvent(e); + await ep.flush(); - flushAndGetRequest(output => { + const output = await getJsonRequest(s); expect(output).toEqual([{ kind: 'identify', creationDate: 1000, key: user.key, user: filteredUser }]); - done(); }); - }); + })); - it('stringifies user attributes in identify event', done => { - ep = EventProcessor(sdkKey, defaultConfig); - const e = { kind: 'identify', creationDate: 1000, user: numericUser }; - ep.sendEvent(e); + it('stringifies user attributes in identify event', eventsServerTest(async s => { + await withEventProcessor(defaultConfig, s, async ep => { + const e = { kind: 'identify', creationDate: 1000, user: numericUser }; + ep.sendEvent(e); + await ep.flush(); - flushAndGetRequest(output => { + const output = await getJsonRequest(s); expect(output).toEqual([{ kind: 'identify', creationDate: 1000, key: stringifiedNumericUser.key, user: stringifiedNumericUser }]); - done(); }); - }); + })); - it('queues individual feature event with index event', done => { - ep = EventProcessor(sdkKey, defaultConfig); - const e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', - version: 11, variation: 1, value: 'value', trackEvents: true }; - ep.sendEvent(e); + it('queues individual feature event with index event', eventsServerTest(async s => { + await withEventProcessor(defaultConfig, s, async ep => { + const e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', + version: 11, variation: 1, value: 'value', trackEvents: true }; + ep.sendEvent(e); + await ep.flush(); - flushAndGetRequest(output => { + const output = await getJsonRequest(s); expect(output.length).toEqual(3); checkIndexEvent(output[0], e, user); checkFeatureEvent(output[1], e, false); checkSummaryEvent(output[2]); - done(); }); - }); + })); - it('filters user in index event', done => { + it('filters user in index event', eventsServerTest(async s => { const config = Object.assign({}, defaultConfig, { allAttributesPrivate: true }); - ep = EventProcessor(sdkKey, config); - const e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', - version: 11, variation: 1, value: 'value', trackEvents: true }; - ep.sendEvent(e); + await withEventProcessor(config, s, async ep => { + const e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', + version: 11, variation: 1, value: 'value', trackEvents: true }; + ep.sendEvent(e); + await ep.flush(); - flushAndGetRequest(output => { + const output = await getJsonRequest(s); expect(output.length).toEqual(3); checkIndexEvent(output[0], e, filteredUser); checkFeatureEvent(output[1], e, false); checkSummaryEvent(output[2]); - done(); }); - }); + })); - it('stringifies user attributes in index event', done => { - ep = EventProcessor(sdkKey, defaultConfig); - const e = { kind: 'feature', creationDate: 1000, user: numericUser, key: 'flagkey', - version: 11, variation: 1, value: 'value', trackEvents: true }; - ep.sendEvent(e); + it('stringifies user attributes in index event', eventsServerTest(async s => { + await withEventProcessor(defaultConfig, s, async ep => { + const e = { kind: 'feature', creationDate: 1000, user: numericUser, key: 'flagkey', + version: 11, variation: 1, value: 'value', trackEvents: true }; + ep.sendEvent(e); + await ep.flush(); - flushAndGetRequest(output => { + const output = await getJsonRequest(s); expect(output.length).toEqual(3); checkIndexEvent(output[0], e, stringifiedNumericUser); checkFeatureEvent(output[1], e, false); checkSummaryEvent(output[2]); - done(); }); - }); + })); - it('can include inline user in feature event', done => { + it('can include inline user in feature event', eventsServerTest(async s => { const config = Object.assign({}, defaultConfig, { inlineUsersInEvents: true }); - ep = EventProcessor(sdkKey, config); - const e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', - version: 11, variation: 1, value: 'value', trackEvents: true }; - ep.sendEvent(e); + await withEventProcessor(config, s, async ep => { + const e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', + version: 11, variation: 1, value: 'value', trackEvents: true }; + ep.sendEvent(e); + await ep.flush(); - flushAndGetRequest(output => { + const output = await getJsonRequest(s); expect(output.length).toEqual(2); checkFeatureEvent(output[0], e, false, user); checkSummaryEvent(output[1]); - done(); }); - }); + })); - it('filters user in feature event', done => { + it('filters user in feature event', eventsServerTest(async s => { const config = Object.assign({}, defaultConfig, { allAttributesPrivate: true, inlineUsersInEvents: true }); - ep = EventProcessor(sdkKey, config); - const e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', - version: 11, variation: 1, value: 'value', trackEvents: true }; - ep.sendEvent(e); + await withEventProcessor(config, s, async ep => { + const e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', + version: 11, variation: 1, value: 'value', trackEvents: true }; + ep.sendEvent(e); + await ep.flush(); - flushAndGetRequest(output => { + const output = await getJsonRequest(s); expect(output.length).toEqual(2); checkFeatureEvent(output[0], e, false, filteredUser); checkSummaryEvent(output[1]); - done(); }); - }); + })); - it('stringifies user attributes in feature event', done => { + it('stringifies user attributes in feature event', eventsServerTest(async s => { const config = Object.assign({}, defaultConfig, { inlineUsersInEvents: true }); - ep = EventProcessor(sdkKey, config); - const e = { kind: 'feature', creationDate: 1000, user: numericUser, key: 'flagkey', - version: 11, variation: 1, value: 'value', trackEvents: true }; - ep.sendEvent(e); + await withEventProcessor(config, s, async ep => { + const e = { kind: 'feature', creationDate: 1000, user: numericUser, key: 'flagkey', + version: 11, variation: 1, value: 'value', trackEvents: true }; + ep.sendEvent(e); + await ep.flush(); - flushAndGetRequest(output => { + const output = await getJsonRequest(s); expect(output.length).toEqual(2); checkFeatureEvent(output[0], e, false, stringifiedNumericUser); checkSummaryEvent(output[1]); - done(); }); - }); + })); - it('can include reason in feature event', done => { + it('can include reason in feature event', eventsServerTest(async s => { const config = Object.assign({}, defaultConfig, { inlineUsersInEvents: true }); - ep = EventProcessor(sdkKey, config); - const e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', - version: 11, variation: 1, value: 'value', trackEvents: true, - reason: { kind: 'FALLTHROUGH' } }; - ep.sendEvent(e); + await withEventProcessor(config, s, async ep => { + const e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', + version: 11, variation: 1, value: 'value', trackEvents: true, + reason: { kind: 'FALLTHROUGH' } }; + ep.sendEvent(e); + await ep.flush(); - flushAndGetRequest(output => { + const output = await getJsonRequest(s); expect(output.length).toEqual(2); checkFeatureEvent(output[0], e, false, user); checkSummaryEvent(output[1]); - done(); }); - }); + })); - it('still generates index event if inlineUsers is true but feature event is not tracked', done => { + it('still generates index event if inlineUsers is true but feature event is not tracked', eventsServerTest(async s => { const config = Object.assign({}, defaultConfig, { inlineUsersInEvents: true }); - ep = EventProcessor(sdkKey, config); - const e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', - version: 11, variation: 1, value: 'value', trackEvents: false }; - ep.sendEvent(e); + await withEventProcessor(config, s, async ep => { + const e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', + version: 11, variation: 1, value: 'value', trackEvents: false }; + ep.sendEvent(e); + await ep.flush(); - flushAndGetRequest(output => { + const output = await getJsonRequest(s); expect(output.length).toEqual(2); checkIndexEvent(output[0], e, user); checkSummaryEvent(output[1]); - done(); }); - }); + })); - it('sets event kind to debug if event is temporarily in debug mode', done => { - ep = EventProcessor(sdkKey, defaultConfig); - var futureTime = new Date().getTime() + 1000000; - const e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', - version: 11, variation: 1, value: 'value', trackEvents: false, debugEventsUntilDate: futureTime }; - ep.sendEvent(e); + it('sets event kind to debug if event is temporarily in debug mode', eventsServerTest(async s => { + await withEventProcessor(defaultConfig, s, async ep => { + var futureTime = new Date().getTime() + 1000000; + const e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', + version: 11, variation: 1, value: 'value', trackEvents: false, debugEventsUntilDate: futureTime }; + ep.sendEvent(e); + await ep.flush(); - flushAndGetRequest(output => { + const output = await getJsonRequest(s); expect(output.length).toEqual(3); checkIndexEvent(output[0], e, user); checkFeatureEvent(output[1], e, true, user); checkSummaryEvent(output[2]); - done(); }); - }); + })); - it('can both track and debug an event', done => { - ep = EventProcessor(sdkKey, defaultConfig); - var futureTime = new Date().getTime() + 1000000; - const e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', - version: 11, variation: 1, value: 'value', trackEvents: true, debugEventsUntilDate: futureTime }; - ep.sendEvent(e); + it('can both track and debug an event', eventsServerTest(async s => { + await withEventProcessor(defaultConfig, s, async ep => { + const futureTime = new Date().getTime() + 1000000; + const e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', + version: 11, variation: 1, value: 'value', trackEvents: true, debugEventsUntilDate: futureTime }; + ep.sendEvent(e); + await ep.flush(); - flushAndGetRequest(output => { + const output = await getJsonRequest(s); expect(output.length).toEqual(4); checkIndexEvent(output[0], e, user); checkFeatureEvent(output[1], e, false); checkFeatureEvent(output[2], e, true, user); checkSummaryEvent(output[3]); - done(); }); - }); + })); - it('expires debug mode based on client time if client time is later than server time', done => { - ep = EventProcessor(sdkKey, defaultConfig); + it('expires debug mode based on client time if client time is later than server time', eventsServerTest(async s => { + await withEventProcessor(defaultConfig, s, async ep => { + // Pick a server time that is somewhat behind the client time + const serverTime = new Date().getTime() - 20000; + s.forMethodAndPath('post', '/bulk', TestHttpHandlers.respond(200, headersWithDate(serverTime))); - // Pick a server time that is somewhat behind the client time - var serverTime = new Date().getTime() - 20000; + // Send and flush an event we don't care about, just to set the last server time + ep.sendEvent({ kind: 'identify', user: { key: 'otherUser' } }); + await ep.flush(); + await s.nextRequest(); - // Send and flush an event we don't care about, just to set the last server time - ep.sendEvent({ kind: 'identify', user: { key: 'otherUser' } }); - flushAndGetRequest({ status: 200, headers: headersWithDate(serverTime) }, function() { // Now send an event with debug mode on, with a "debug until" time that is further in // the future than the server time, but in the past compared to the client. - var debugUntil = serverTime + 1000; + const debugUntil = serverTime + 1000; const e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', version: 11, variation: 1, value: 'value', trackEvents: false, debugEventsUntilDate: debugUntil }; ep.sendEvent(e); + await ep.flush(); // Should get a summary event only, not a full feature event - flushAndGetRequest(output => { - expect(output.length).toEqual(2); - checkIndexEvent(output[0], e, user); - checkSummaryEvent(output[1]); - done(); - }); + const output = await getJsonRequest(s); + expect(output.length).toEqual(2); + checkIndexEvent(output[0], e, user); + checkSummaryEvent(output[1]); }); - }); + })); - it('expires debug mode based on server time if server time is later than client time', done => { - ep = EventProcessor(sdkKey, defaultConfig); + it('expires debug mode based on server time if server time is later than client time', eventsServerTest(async s => { + await withEventProcessor(defaultConfig, s, async ep => { + // Pick a server time that is somewhat ahead of the client time + const serverTime = new Date().getTime() + 20000; + s.forMethodAndPath('post', '/bulk', TestHttpHandlers.respond(200, headersWithDate(serverTime))); - // Pick a server time that is somewhat ahead of the client time - var serverTime = new Date().getTime() + 20000; + // Send and flush an event we don't care about, just to set the last server time + ep.sendEvent({ kind: 'identify', user: { key: 'otherUser' } }); + await ep.flush(); + await s.nextRequest(); - // Send and flush an event we don't care about, just to set the last server time - ep.sendEvent({ kind: 'identify', user: { key: 'otherUser' } }); - flushAndGetRequest({ status: 200, headers: headersWithDate(serverTime) }, function() { // Now send an event with debug mode on, with a "debug until" time that is further in // the future than the client time, but in the past compared to the server. - var debugUntil = serverTime - 1000; + const debugUntil = serverTime - 1000; const e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', version: 11, variation: 1, value: 'value', trackEvents: false, debugEventsUntilDate: debugUntil }; ep.sendEvent(e); + await ep.flush(); // Should get a summary event only, not a full feature event - flushAndGetRequest(output => { - expect(output.length).toEqual(2); - checkIndexEvent(output[0], e, user); - checkSummaryEvent(output[1]); - done(); - }); + const output = await getJsonRequest(s); + expect(output.length).toEqual(2); + checkIndexEvent(output[0], e, user); + checkSummaryEvent(output[1]); }); - }); - - it('generates only one index event from two feature events for same user', done => { - ep = EventProcessor(sdkKey, defaultConfig); - var e1 = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey1', - version: 11, variation: 1, value: 'value', trackEvents: true }; - var e2 = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey2', - version: 11, variation: 1, value: 'value', trackEvents: true }; - ep.sendEvent(e1); - ep.sendEvent(e2); - - flushAndGetRequest(output => { + })); + + it('generates only one index event from two feature events for same user', eventsServerTest(async s => { + await withEventProcessor(defaultConfig, s, async ep => { + const e1 = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey1', + version: 11, variation: 1, value: 'value', trackEvents: true }; + const e2 = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey2', + version: 11, variation: 1, value: 'value', trackEvents: true }; + ep.sendEvent(e1); + ep.sendEvent(e2); + await ep.flush(); + + const output = await getJsonRequest(s); expect(output.length).toEqual(4); checkIndexEvent(output[0], e1, user); checkFeatureEvent(output[1], e1, false); checkFeatureEvent(output[2], e2, false); checkSummaryEvent(output[3]); - done(); }); - }); - - it('summarizes nontracked events', done => { - ep = EventProcessor(sdkKey, defaultConfig); - var e1 = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey1', - version: 11, variation: 1, value: 'value1', default: 'default1', trackEvents: false }; - var e2 = { kind: 'feature', creationDate: 2000, user: user, key: 'flagkey2', - version: 22, variation: 1, value: 'value2', default: 'default2', trackEvents: false }; - ep.sendEvent(e1); - ep.sendEvent(e2); - - flushAndGetRequest(output => { + })); + + it('summarizes nontracked events', eventsServerTest(async s => { + await withEventProcessor(defaultConfig, s, async ep => { + const e1 = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey1', + version: 11, variation: 1, value: 'value1', default: 'default1', trackEvents: false }; + const e2 = { kind: 'feature', creationDate: 2000, user: user, key: 'flagkey2', + version: 22, variation: 1, value: 'value2', default: 'default2', trackEvents: false }; + ep.sendEvent(e1); + ep.sendEvent(e2); + await ep.flush(); + + const output = await getJsonRequest(s); expect(output.length).toEqual(2); checkIndexEvent(output[0], e1, user); - var se = output[1]; + const se = output[1]; checkSummaryEvent(se); expect(se.startDate).toEqual(1000); expect(se.endDate).toEqual(2000); @@ -396,187 +391,161 @@ describe('EventProcessor', () => { counters: [ { version: 22, variation: 1, value: 'value2', count: 1 } ] } }); - done(); }); - }); + })); - it('queues custom event with user', done => { - ep = EventProcessor(sdkKey, defaultConfig); - const e = { kind: 'custom', creationDate: 1000, user: user, key: 'eventkey', - data: { thing: 'stuff' } }; - ep.sendEvent(e); + it('queues custom event with user', eventsServerTest(async s => { + await withEventProcessor(defaultConfig, s, async ep => { + const e = { kind: 'custom', creationDate: 1000, user: user, key: 'eventkey', + data: { thing: 'stuff' } }; + ep.sendEvent(e); + await ep.flush(); - flushAndGetRequest(output => { + const output = await getJsonRequest(s); expect(output.length).toEqual(2); checkIndexEvent(output[0], e, user); checkCustomEvent(output[1], e); - done(); }); - }); + })); - it('can include metric value in custom event', done => { - ep = EventProcessor(sdkKey, defaultConfig); - const e = { kind: 'custom', creationDate: 1000, user: user, key: 'eventkey', - data: { thing: 'stuff' }, metricValue: 1.5 }; - ep.sendEvent(e); + it('can include metric value in custom event', eventsServerTest(async s => { + await withEventProcessor(defaultConfig, s, async ep => { + const e = { kind: 'custom', creationDate: 1000, user: user, key: 'eventkey', + data: { thing: 'stuff' }, metricValue: 1.5 }; + ep.sendEvent(e); + await ep.flush(); - flushAndGetRequest(output => { + const output = await getJsonRequest(s); expect(output.length).toEqual(2); checkIndexEvent(output[0], e, user); checkCustomEvent(output[1], e); - done(); }); - }); + })); - it('can include inline user in custom event', done => { + it('can include inline user in custom event', eventsServerTest(async s => { const config = Object.assign({}, defaultConfig, { inlineUsersInEvents: true }); - ep = EventProcessor(sdkKey, config); - const e = { kind: 'custom', creationDate: 1000, user: user, key: 'eventkey', - data: { thing: 'stuff' } }; - ep.sendEvent(e); + await withEventProcessor(config, s, async ep => { + const e = { kind: 'custom', creationDate: 1000, user: user, key: 'eventkey', + data: { thing: 'stuff' } }; + ep.sendEvent(e); + await ep.flush(); - flushAndGetRequest(output => { + const output = await getJsonRequest(s); expect(output.length).toEqual(1); checkCustomEvent(output[0], e, user); - done(); }); - }); + })); - it('stringifies user attributes in custom event', done => { + it('stringifies user attributes in custom event', eventsServerTest(async s => { const config = Object.assign({}, defaultConfig, { inlineUsersInEvents: true }); - ep = EventProcessor(sdkKey, config); - const e = { kind: 'custom', creationDate: 1000, user: numericUser, key: 'eventkey', - data: { thing: 'stuff' } }; - ep.sendEvent(e); + await withEventProcessor(config, s, async ep => { + const e = { kind: 'custom', creationDate: 1000, user: numericUser, key: 'eventkey', + data: { thing: 'stuff' } }; + ep.sendEvent(e); + await ep.flush(); - flushAndGetRequest(output => { + const output = await getJsonRequest(s); expect(output.length).toEqual(1); checkCustomEvent(output[0], e, stringifiedNumericUser); - done(); }); - }); + })); - it('filters user in custom event', done => { + it('filters user in custom event', eventsServerTest(async s => { const config = Object.assign({}, defaultConfig, { allAttributesPrivate: true, inlineUsersInEvents: true }); - ep = EventProcessor(sdkKey, config); - const e = { kind: 'custom', creationDate: 1000, user: user, key: 'eventkey', - data: { thing: 'stuff' } }; - ep.sendEvent(e); + await withEventProcessor(config, s, async ep => { + const e = { kind: 'custom', creationDate: 1000, user: user, key: 'eventkey', + data: { thing: 'stuff' } }; + ep.sendEvent(e); + await ep.flush(); - flushAndGetRequest(output => { + const output = await getJsonRequest(s); expect(output.length).toEqual(1); checkCustomEvent(output[0], e, filteredUser); - done(); - }); - }); - - it('sends nothing if there are no events', done => { - ep = EventProcessor(sdkKey, defaultConfig); - ep.flush(function() { - // Nock will generate an error if we sent a request we didn't explicitly listen for. - done(); }); - }); - - it('sends SDK key', done => { - ep = EventProcessor(sdkKey, defaultConfig); - const e = { kind: 'identify', creationDate: 1000, user: user }; - ep.sendEvent(e); + })); - flushAndGetRequest(function(requestBody, requestHeaders) { - expect(requestHeaders['authorization']).toEqual(sdkKey); - done(); + it('sends nothing if there are no events', eventsServerTest(async s => { + await withEventProcessor(defaultConfig, s, async ep => { + await ep.flush(); + expect(s.requestCount()).toEqual(0); }); - }); - - function verifyUnrecoverableHttpError(done, status) { - ep = EventProcessor(sdkKey, defaultConfig); - const e = { kind: 'identify', creationDate: 1000, user: user }; - ep.sendEvent(e); - - flushAndGetRequest({ status: status }, (body, headers, error) => { - expect(error.message).toContain('error ' + status); + })); + it('sends SDK key', eventsServerTest(async s => { + await withEventProcessor(defaultConfig, s, async ep => { + const e = { kind: 'identify', creationDate: 1000, user: user }; ep.sendEvent(e); + await ep.flush(); - ep.flush().then( - // no HTTP request should have been done here - Nock will error out if there was one - function() { }, - function(err) { - expect(err.message).toContain('SDK key is invalid'); - done(); - }); + const request = await s.nextRequest(); + expect(request.headers['authorization']).toEqual(sdkKey); + }); + })); + + function verifyUnrecoverableHttpError(status) { + return eventsServerTest(async s => { + s.forMethodAndPath('post', '/bulk', TestHttpHandlers.respond(status)); + await withEventProcessor(defaultConfig, s, async ep => { + const e = { kind: 'identify', creationDate: 1000, user: user }; + ep.sendEvent(e); + await expect(ep.flush()).rejects.toThrow('error ' + status); + + expect(s.requestCount()).toEqual(1); + await s.nextRequest(); + + ep.sendEvent(e); + await expect(ep.flush()).rejects.toThrow(/SDK key is invalid/); + expect(s.requestCount()).toEqual(1); + }); }); } - function verifyRecoverableHttpError(done, status) { - ep = EventProcessor(sdkKey, defaultConfig); - var e0 = { kind: 'identify', creationDate: 1000, user: user }; - ep.sendEvent(e0); - - nock(eventsUri).post('/bulk').reply(status); - nock(eventsUri).post('/bulk').reply(status); - // since we only queued two responses, Nock will throw an error if it gets a third. - ep.flush().then( - function() {}, - function(err) { - expect(err.message).toContain('error ' + status); - - var e1 = { kind: 'identify', creationDate: 1001, user: user }; - ep.sendEvent(e1); - - // this second event should go through - flushAndGetRequest(output => { - expect(output.length).toEqual(1); - expect(output[0].creationDate).toEqual(1001); - - done(); - }); + function verifyRecoverableHttpError(status) { + return eventsServerTest(async s => { + s.forMethodAndPath('post', '/bulk', TestHttpHandlers.respond(status)); + await withEventProcessor(defaultConfig, s, async ep => { + var e = { kind: 'identify', creationDate: 1000, user: user }; + ep.sendEvent(e); + await expect(ep.flush()).rejects.toThrow('error ' + status); + + expect(s.requestCount()).toEqual(2); + await s.nextRequest(); + await s.nextRequest(); + + s.forMethodAndPath('post', '/bulk', TestHttpHandlers.respond(200)); + ep.sendEvent(e); + await ep.flush(); + expect(s.requestCount()).toEqual(3); }); + }); } - it('retries after a 400 error', done => { - verifyRecoverableHttpError(done, 400); - }); + it('retries after a 400 error', verifyRecoverableHttpError(400)); - it('stops sending events after a 401 error', done => { - verifyUnrecoverableHttpError(done, 401); - }); + it('stops sending events after a 401 error', verifyUnrecoverableHttpError(401)); - it('stops sending events after a 403 error', done => { - verifyUnrecoverableHttpError(done, 403); - }); + it('stops sending events after a 403 error', verifyUnrecoverableHttpError(403)); - it('retries after a 408 error', done => { - verifyRecoverableHttpError(done, 408); - }); + it('retries after a 408 error', verifyRecoverableHttpError(408)); - it('retries after a 429 error', done => { - verifyRecoverableHttpError(done, 429); - }); + it('retries after a 429 error', verifyRecoverableHttpError(429)); - it('retries after a 503 error', done => { - verifyRecoverableHttpError(done, 503); - }); + it('retries after a 503 error', verifyRecoverableHttpError(503)); - it('swallows errors from failed background flush', done => { + it('swallows errors from failed background flush', eventsServerTest(async s => { // This test verifies that when a background flush fails, we don't emit an unhandled // promise rejection. Jest will fail the test if we do that. - + const config = Object.assign({}, defaultConfig, { flushInterval: 0.25 }); - ep = EventProcessor(sdkKey, config); - ep.sendEvent({ kind: 'identify', creationDate: 1000, user: user }); - - var req1 = nock(eventsUri).post('/bulk').reply(500); - var req2 = nock(eventsUri).post('/bulk').reply(500); - - // unfortunately we must wait for both the flush interval and the 1-second retry interval - var delay = 1500; - setTimeout(() => { - expect(req1.isDone()).toEqual(true); - expect(req2.isDone()).toEqual(true); - done(); - }, delay); - }); + await withEventProcessor(config, s, async ep => { + s.forMethodAndPath('post', '/bulk', TestHttpHandlers.respond(500)); + + ep.sendEvent({ kind: 'identify', creationDate: 1000, user: user }); + + // unfortunately we must wait for both the flush interval and the 1-second retry interval + await sleepAsync(1500); + expect(s.requestCount()).toEqual(2); + }); + })); }); diff --git a/test/feature_store_event_wrapper-test.js b/test/feature_store_event_wrapper-test.js index da9c9c1..aa435e6 100644 --- a/test/feature_store_event_wrapper-test.js +++ b/test/feature_store_event_wrapper-test.js @@ -2,7 +2,7 @@ const EventEmitter = require('events').EventEmitter; const FeatureStoreEventWrapper = require('../feature_store_event_wrapper'); const InMemoryFeatureStore = require('../feature_store'); const dataKind = require('../versioned_data_kind'); -const { AsyncQueue, asyncify } = require('./async_utils'); +const { AsyncQueue, promisifySingle } = require('launchdarkly-js-test-helpers'); describe('FeatureStoreEventWrapper', () => { function listenAndStoreEvents(emitter, queue, eventName) { @@ -21,14 +21,14 @@ describe('FeatureStoreEventWrapper', () => { segments: {} }; const emitter = new EventEmitter(); - const queue = AsyncQueue(); + const queue = new AsyncQueue(); listenAndStoreEvents(emitter, queue, 'update'); listenAndStoreEvents(emitter, queue, 'update:a'); listenAndStoreEvents(emitter, queue, 'update:b'); const wrapper = FeatureStoreEventWrapper(store, emitter); - await asyncify(f => wrapper.init(allData, f)); + await promisifySingle(wrapper.init)(allData); expect(await queue.take()).toEqual(['update', { key: 'a' }]); expect(await queue.take()).toEqual(['update:a', { key: 'a' }]); @@ -55,7 +55,7 @@ describe('FeatureStoreEventWrapper', () => { segments: {} }; const emitter = new EventEmitter(); - const queue = AsyncQueue(); + const queue = new AsyncQueue(); listenAndStoreEvents(emitter, queue, 'update'); listenAndStoreEvents(emitter, queue, 'update:a'); listenAndStoreEvents(emitter, queue, 'update:b'); @@ -63,7 +63,7 @@ describe('FeatureStoreEventWrapper', () => { const wrapper = FeatureStoreEventWrapper(store, emitter); - await asyncify(f => wrapper.init(allData0, f)); + await promisifySingle(wrapper.init)(allData0); expect(await queue.take()).toEqual(['update', { key: 'a' }]); expect(await queue.take()).toEqual(['update:a', { key: 'a' }]); @@ -73,7 +73,7 @@ describe('FeatureStoreEventWrapper', () => { expect(await queue.take()).toEqual(['update:c', { key: 'c' }]); expect(queue.isEmpty()).toEqual(true); - await asyncify(f => wrapper.init(allData1, f)); + await promisifySingle(wrapper.init)(allData1); expect(await queue.take()).toEqual(['update', { key: 'b' }]); // b was updated to version 2 expect(await queue.take()).toEqual(['update:b', { key: 'b' }]); expect(await queue.take()).toEqual(['update', { key: 'c' }]); // c was deleted @@ -90,20 +90,20 @@ describe('FeatureStoreEventWrapper', () => { segments: {} }; const emitter = new EventEmitter(); - const queue = AsyncQueue(); + const queue = new AsyncQueue(); listenAndStoreEvents(emitter, queue, 'update'); listenAndStoreEvents(emitter, queue, 'update:a'); const wrapper = FeatureStoreEventWrapper(store, emitter); - await asyncify(f => wrapper.init(allData, f)); + await promisifySingle(wrapper.init)(allData); expect(await queue.take()).toEqual(['update', { key: 'a' }]); expect(await queue.take()).toEqual(['update:a', { key: 'a' }]); expect(queue.isEmpty()).toEqual(true); - await asyncify(f => wrapper.upsert(dataKind.features, { key: 'a', version: 2 }, f)); - await asyncify(f => wrapper.upsert(dataKind.features, { key: 'a', version: 2 }, f)); // no event for this one + await promisifySingle(wrapper.upsert)(dataKind.features, { key: 'a', version: 2 }); + await promisifySingle(wrapper.upsert)(dataKind.features, { key: 'a', version: 2 }); // no event for this one expect(await queue.take()).toEqual(['update', { key: 'a' }]); expect(await queue.take()).toEqual(['update:a', { key: 'a' }]); expect(queue.isEmpty()).toEqual(true); @@ -118,19 +118,19 @@ describe('FeatureStoreEventWrapper', () => { segments: {} }; const emitter = new EventEmitter(); - const queue = AsyncQueue(); + const queue = new AsyncQueue(); listenAndStoreEvents(emitter, queue, 'update'); listenAndStoreEvents(emitter, queue, 'update:a'); const wrapper = FeatureStoreEventWrapper(store, emitter); - await asyncify(f => wrapper.init(allData, f)); + await promisifySingle(wrapper.init)(allData); expect(await queue.take()).toEqual(['update', { key: 'a' }]); expect(await queue.take()).toEqual(['update:a', { key: 'a' }]); expect(queue.isEmpty()).toEqual(true); - await asyncify(f => wrapper.delete(dataKind.features, 'a', 2, f)); + await promisifySingle(wrapper.delete)(dataKind.features, 'a', 2); expect(await queue.take()).toEqual(['update', { key: 'a' }]); expect(await queue.take()).toEqual(['update:a', { key: 'a' }]); expect(queue.isEmpty()).toEqual(true); @@ -155,12 +155,12 @@ describe('FeatureStoreEventWrapper', () => { } }; const emitter = new EventEmitter(); - const queue = AsyncQueue(); + const queue = new AsyncQueue(); listenAndStoreEvents(emitter, queue, 'update'); const wrapper = FeatureStoreEventWrapper(store, emitter); - await asyncify(f => wrapper.init(allData, f)); + await promisifySingle(wrapper.init)(allData); expect(await queue.take()).toEqual(['update', { key: 'a' }]); expect(await queue.take()).toEqual(['update', { key: 'b' }]); @@ -169,13 +169,13 @@ describe('FeatureStoreEventWrapper', () => { expect(await queue.take()).toEqual(['update', { key: 'e' }]); expect(queue.isEmpty()).toEqual(true); - await asyncify(f => wrapper.upsert(dataKind.features, - { key: 'd', version: 2, prerequisites: [ { key: 'e' } ] }, f)); + await promisifySingle(wrapper.upsert)(dataKind.features, + { key: 'd', version: 2, prerequisites: [ { key: 'e' } ] }); expect(await queue.take()).toEqual(['update', { key: 'b' }]); expect(await queue.take()).toEqual(['update', { key: 'c' }]); expect(await queue.take()).toEqual(['update', { key: 'd' }]); - await asyncify(f => wrapper.upsert(dataKind.segments, { key: 's0', version: 2 }, f)); + await promisifySingle(wrapper.upsert)(dataKind.segments, { key: 's0', version: 2 }); expect(await queue.take()).toEqual(['update', { key: 'b' }]); expect(await queue.take()).toEqual(['update', { key: 'c' }]); }); diff --git a/test/feature_store_test_base.js b/test/feature_store_test_base.js index 1afe1dd..5f85ee4 100644 --- a/test/feature_store_test_base.js +++ b/test/feature_store_test_base.js @@ -1,5 +1,5 @@ var dataKind = require('../versioned_data_kind'); -const { asyncify } = require('./async_utils'); +const { promisifySingle } = require('launchdarkly-js-test-helpers'); // The following tests should be run on every feature store implementation. If this type of // store supports caching, the tests should be run once with caching enabled and once with @@ -37,13 +37,13 @@ function baseFeatureStoreTests(makeStore, clearExistingData, isCached, makeStore 'foo': feature1, 'bar': feature2 }; - await asyncify(cb => store.init(initData, cb)); + await promisifySingle(store.init)(initData); return store; } it('is initialized after calling init()', async () => { var store = await initedStore(); - var result = await asyncify(cb => store.initialized(cb)); + var result = await promisifySingle(store.initialized)(); expect(result).toBe(true); }); @@ -58,10 +58,10 @@ function baseFeatureStoreTests(makeStore, clearExistingData, isCached, makeStore initData[dataKind.features.namespace] = flags; initData[dataKind.segments.namespace] = segments; - await asyncify(cb => store.init(initData, cb)); - var items = await asyncify(cb => store.all(dataKind.features, cb)); + await promisifySingle(store.init)(initData); + var items = await promisifySingle(store.all)(dataKind.features); expect(items).toEqual(flags); - items = await asyncify(cb => store.all(dataKind.segments, cb)); + items = await promisifySingle(store.all)(dataKind.segments); expect(items).toEqual(segments); var newFlags = { first: { key: 'first', version: 3 } }; @@ -70,10 +70,10 @@ function baseFeatureStoreTests(makeStore, clearExistingData, isCached, makeStore initData[dataKind.features.namespace] = newFlags; initData[dataKind.segments.namespace] = newSegments; - await asyncify(cb => store.init(initData, cb)); - items = await asyncify(cb => store.all(dataKind.features, cb)); + await promisifySingle(store.init)(initData); + items = await promisifySingle(store.all)(dataKind.features); expect(items).toEqual(newFlags); - items = await asyncify(cb => store.all(dataKind.segments, cb)); + items = await promisifySingle(store.all)(dataKind.segments); expect(items).toEqual(newSegments); }); @@ -83,11 +83,11 @@ function baseFeatureStoreTests(makeStore, clearExistingData, isCached, makeStore var store1 = makeStore(); var store2 = makeStore(); - var result = await asyncify(cb => store1.initialized(cb)); + var result = await promisifySingle(store1.initialized)(); expect(result).toBe(false); - await asyncify(cb => store2.init(initData, cb)); - result = await asyncify(cb => store1.initialized(cb)); + await promisifySingle(store2.init)(initData); + result = await promisifySingle(store1.initialized)(); expect(result).toBe(true); }); } @@ -102,13 +102,13 @@ function baseFeatureStoreTests(makeStore, clearExistingData, isCached, makeStore it('is independent from other instances with different prefixes', async () => { var flag = { key: 'flag', version: 1 }; var storeA = makeStoreWithPrefix('a'); - await asyncify(cb => storeA.init({ features: { flag: flag } }, cb)); + await promisifySingle(storeA.init)({ features: { flag: flag } }); var storeB = makeStoreWithPrefix('b'); - await asyncify(cb => storeB.init({ features: { } }, cb)); + await promisifySingle(storeB.init)({ features: { } }); var storeB1 = makeStoreWithPrefix('b'); // this ensures we're not just reading cached data - var item = await asyncify(cb => storeB1.get(dataKind.features, 'flag', cb)); + var item = await promisifySingle(storeB1.get)(dataKind.features, 'flag'); expect(item).toBe(null); - item = await asyncify(cb => storeA.get(dataKind.features, 'flag', cb)); + item = await promisifySingle(storeA.get)(dataKind.features, 'flag'); expect(item).toEqual(flag); }); } @@ -116,19 +116,19 @@ function baseFeatureStoreTests(makeStore, clearExistingData, isCached, makeStore it('gets existing feature', async () => { var store = await initedStore(); - var result = await asyncify(cb => store.get(dataKind.features, feature1.key, cb)); + var result = await promisifySingle(store.get)(dataKind.features, feature1.key); expect(result).toEqual(feature1); }); it('does not get nonexisting feature', async () => { var store = await initedStore(); - var result = await asyncify(cb => store.get(dataKind.features, 'biz', cb)); + var result = await promisifySingle(store.get)(dataKind.features, 'biz'); expect(result).toBe(null); }); it('gets all features', async () => { var store = await initedStore(); - var result = await asyncify(cb => store.all(dataKind.features, cb)); + var result = await promisifySingle(store.all)(dataKind.features); expect(result).toEqual({ 'foo': feature1, 'bar': feature2 @@ -138,24 +138,24 @@ function baseFeatureStoreTests(makeStore, clearExistingData, isCached, makeStore it('upserts with newer version', async () => { var newVer = { key: feature1.key, version: feature1.version + 1 }; var store = await initedStore(); - await asyncify(cb => store.upsert(dataKind.features, newVer, cb)); - var result = await asyncify(cb => store.get(dataKind.features, feature1.key, cb)); + await promisifySingle(store.upsert)(dataKind.features, newVer); + var result = await promisifySingle(store.get)(dataKind.features, feature1.key); expect(result).toEqual(newVer); }); it('does not upsert with older version', async () => { var oldVer = { key: feature1.key, version: feature1.version - 1 }; var store = await initedStore(); - await asyncify(cb => store.upsert(dataKind.features, oldVer, cb)); - var result = await asyncify(cb => store.get(dataKind.features, feature1.key, cb)); + await promisifySingle(store.upsert)(dataKind.features, oldVer); + var result = await promisifySingle(store.get)(dataKind.features, feature1.key); expect(result).toEqual(feature1); }); it('upserts new feature', async () => { var newFeature = { key: 'biz', version: 99 }; var store = await initedStore(); - await asyncify(cb => store.upsert(dataKind.features, newFeature, cb)); - var result = await asyncify(cb => store.get(dataKind.features, newFeature.key, cb)); + await promisifySingle(store.upsert)(dataKind.features, newFeature); + var result = await promisifySingle(store.get)(dataKind.features, newFeature.key); expect(result).toEqual(newFeature); }); @@ -183,30 +183,30 @@ function baseFeatureStoreTests(makeStore, clearExistingData, isCached, makeStore it('deletes with newer version', async () => { var store = await initedStore(); - await asyncify(cb => store.delete(dataKind.features, feature1.key, feature1.version + 1, cb)); - var result = await asyncify(cb => store.get(dataKind.features, feature1.key, cb)); + await promisifySingle(store.delete)(dataKind.features, feature1.key, feature1.version + 1); + var result = await promisifySingle(store.get)(dataKind.features, feature1.key); expect(result).toBe(null); }); it('does not delete with older version', async () => { var store = await initedStore(); - await asyncify(cb => store.delete(dataKind.features, feature1.key, feature1.version - 1, cb)); - var result = await asyncify(cb => store.get(dataKind.features, feature1.key, cb)); + await promisifySingle(store.delete)(dataKind.features, feature1.key, feature1.version - 1); + var result = await promisifySingle(store.get)(dataKind.features, feature1.key); expect(result).not.toBe(null); }); it('allows deleting unknown feature', async () => { var store = await initedStore(); - await asyncify(cb => store.delete(dataKind.features, 'biz', 99, cb)); - var result = await asyncify(cb => store.get(dataKind.features, 'biz', cb)); + await promisifySingle(store.delete)(dataKind.features, 'biz', 99); + var result = await promisifySingle(store.get)(dataKind.features, 'biz'); expect(result).toBe(null); }); it('does not upsert older version after delete', async () => { var store = await initedStore(); - await asyncify(cb => store.delete(dataKind.features, feature1.key, feature1.version + 1, cb)); - await asyncify(cb => store.upsert(dataKind.features, feature1, cb)); - var result = await asyncify(cb => store.get(dataKind.features, feature1.key, cb)); + await promisifySingle(store.delete)(dataKind.features, feature1.key, feature1.version + 1); + await promisifySingle(store.upsert)(dataKind.features, feature1); + var result = await promisifySingle(store.get)(dataKind.features, feature1.key); expect(result).toBe(null); }); } @@ -234,7 +234,7 @@ function concurrentModificationTests(makeStore, makeStoreWithHook) { async function initStore(store) { var allData = { features: {} }; allData['features'][flagKey] = makeFlagWithVersion(initialVersion); - await asyncify(cb => store.init(allData, cb)); + await promisifySingle(store.init)(allData); } function writeCompetingVersions(flagVersionsToWrite) { @@ -257,8 +257,8 @@ function concurrentModificationTests(makeStore, makeStoreWithHook) { var myStore = makeStoreWithHook(writeCompetingVersions(competingStoreVersions)); await initStore(myStore); - await asyncify(cb => myStore.upsert(dataKind.features, makeFlagWithVersion(myDesiredVersion), cb)); - var result = await asyncify(cb => myStore.get(dataKind.features, flagKey, cb)); + await promisifySingle(myStore.upsert)(dataKind.features, makeFlagWithVersion(myDesiredVersion)); + var result = await promisifySingle(myStore.get)(dataKind.features, flagKey); expect(result.version).toEqual(myDesiredVersion); }); @@ -269,8 +269,8 @@ function concurrentModificationTests(makeStore, makeStoreWithHook) { var myStore = makeStoreWithHook(writeCompetingVersions([ competingStoreVersion ])); await initStore(myStore); - await asyncify(cb => myStore.upsert(dataKind.features, makeFlagWithVersion(myDesiredVersion), cb)); - var result = await asyncify(cb => myStore.get(dataKind.features, flagKey, cb)); + await promisifySingle(myStore.upsert)(dataKind.features, makeFlagWithVersion(myDesiredVersion)); + var result = await promisifySingle(myStore.get)(dataKind.features, flagKey); expect(result.version).toEqual(competingStoreVersion); }); } diff --git a/test/file_data_source-test.js b/test/file_data_source-test.js index a343f60..e79f0f1 100644 --- a/test/file_data_source-test.js +++ b/test/file_data_source-test.js @@ -1,18 +1,19 @@ -var fs = require('fs'); -var tmp = require('tmp'); -var dataKind = require('../versioned_data_kind'); -const { asyncify, asyncifyNode, sleepAsync } = require('./async_utils'); - -var LaunchDarkly = require('../index'); -var FileDataSource = require('../file_data_source'); -var InMemoryFeatureStore = require('../feature_store'); - -var flag1Key = 'flag1'; -var flag2Key = 'flag2'; -var flag2Value = 'value2'; -var segment1Key = 'seg1'; - -var flag1 = { +const fs = require('fs'); +const tmp = require('tmp'); +const dataKind = require('../versioned_data_kind'); +const { promisify, promisifySingle, sleepAsync } = require('launchdarkly-js-test-helpers'); +const { stubLogger } = require('./stubs'); + +const LaunchDarkly = require('../index'); +const FileDataSource = require('../file_data_source'); +const InMemoryFeatureStore = require('../feature_store'); + +const flag1Key = 'flag1'; +const flag2Key = 'flag2'; +const flag2Value = 'value2'; +const segment1Key = 'seg1'; + +const flag1 = { "key": flag1Key, "on": true, "fallthrough": { @@ -21,26 +22,26 @@ var flag1 = { "variations": [ "fall", "off", "on" ] }; -var segment1 = { +const segment1 = { "key": segment1Key, "include": ["user1"] }; -var flagOnlyJson = ` +const flagOnlyJson = ` { "flags": { "${flag1Key}": ${ JSON.stringify(flag1) } } }`; -var segmentOnlyJson = ` +const segmentOnlyJson = ` { "segments": { "${segment1Key}": ${ JSON.stringify(segment1) } } }`; -var allPropertiesJson = ` +const allPropertiesJson = ` { "flags": { "${flag1Key}": ${ JSON.stringify(flag1) } @@ -53,7 +54,7 @@ var allPropertiesJson = ` } }`; -var allPropertiesYaml = ` +const allPropertiesYaml = ` flags: ${flag1Key}: key: ${flag1Key} @@ -81,10 +82,7 @@ describe('FileDataSource', function() { beforeEach(() => { store = InMemoryFeatureStore(); dataSources = []; - logger = { - info: jest.fn(), - warn: jest.fn() - }; + logger = stubLogger(); }); afterEach(() => { @@ -92,14 +90,14 @@ describe('FileDataSource', function() { }); function makeTempFile(content) { - return asyncifyNode(tmp.file) + return promisify(tmp.file)() .then(path => { return replaceFileContent(path, content).then(() => path); }); } function replaceFileContent(path, content) { - return asyncifyNode(cb => fs.writeFile(path, content, cb)); + return promisify(fs.writeFile)(path, content); } function setupDataSource(options) { @@ -120,23 +118,23 @@ describe('FileDataSource', function() { var fds = setupDataSource({ paths: [path] }); expect(fds.initialized()).toBe(false); - expect(await asyncify(cb => store.initialized(cb))).toBe(false); - expect(await asyncify(cb => store.all(dataKind.features, cb))).toEqual({}); - expect(await asyncify(cb => store.all(dataKind.segments, cb))).toEqual({}); + expect(await promisifySingle(store.initialized)()).toBe(false); + expect(await promisifySingle(store.all)(dataKind.features)).toEqual({}); + expect(await promisifySingle(store.all)(dataKind.segments)).toEqual({}); }); async function testLoadAllProperties(content) { var path = await makeTempFile(content); var fds = setupDataSource({ paths: [path] }); - await asyncify(fds.start); + await promisifySingle(fds.start)(); expect(fds.initialized()).toBe(true); - expect(await asyncify(cb => store.initialized(cb))).toBe(true); - var items = await asyncify(cb => store.all(dataKind.features, cb)); + expect(await promisifySingle(store.initialized)()).toBe(true); + var items = await promisifySingle(store.all)(dataKind.features); expect(sorted(Object.keys(items))).toEqual([ flag1Key, flag2Key ]); - var flag = await asyncify(cb => store.get(dataKind.features, flag1Key, cb)); + var flag = await promisifySingle(store.get)(dataKind.features, flag1Key); expect(flag).toEqual(flag1); - items = await asyncify(cb => store.all(dataKind.segments, cb)); + items = await promisifySingle(store.all)(dataKind.segments); expect(items).toEqual({ seg1: segment1 }); } @@ -146,33 +144,33 @@ describe('FileDataSource', function() { it('does not load if file is missing', async () => { var fds = setupDataSource({ paths: ['no-such-file'] }); - await asyncify(fds.start); + await promisifySingle(fds.start)(); expect(fds.initialized()).toBe(false); - expect(await asyncify(cb => store.initialized(cb))).toBe(false); + expect(await promisifySingle(store.initialized)()).toBe(false); }); it('does not load if file data is malformed', async () => { var path = await makeTempFile('{x'); var fds = setupDataSource({ paths: [path] }); - await asyncify(fds.start); + await promisifySingle(fds.start)(); expect(fds.initialized()).toBe(false); - expect(await asyncify(cb => store.initialized(cb))).toBe(false); + expect(await promisifySingle(store.initialized)()).toBe(false); }); it('can load multiple files', async () => { var path1 = await makeTempFile(flagOnlyJson); var path2 = await makeTempFile(segmentOnlyJson); var fds = setupDataSource({ paths: [path1, path2] }); - await asyncify(fds.start); + await promisifySingle(fds.start)(); expect(fds.initialized()).toBe(true); - expect(await asyncify(cb => store.initialized(cb))).toBe(true); + expect(await promisifySingle(store.initialized)()).toBe(true); - var items = await asyncify(cb => store.all(dataKind.features, cb)); + var items = await promisifySingle(store.all)(dataKind.features); expect(Object.keys(items)).toEqual([ flag1Key ]); - items = await asyncify(cb => store.all(dataKind.segments, cb)); + items = await promisifySingle(store.all)(dataKind.segments); expect(Object.keys(items)).toEqual([ segment1Key ]); }); @@ -180,41 +178,41 @@ describe('FileDataSource', function() { var path1 = await makeTempFile(flagOnlyJson); var path2 = await makeTempFile(flagOnlyJson); var fds = setupDataSource({ paths: [path1, path2] }); - await asyncify(fds.start); + await promisifySingle(fds.start)(); expect(fds.initialized()).toBe(false); - expect(await asyncify(cb => store.initialized(cb))).toBe(false); + expect(await promisifySingle(store.initialized)()).toBe(false); }); it('does not reload modified file if auto-update is off', async () => { var path = await makeTempFile(flagOnlyJson); var fds = setupDataSource({ paths: [path] }); - await asyncify(fds.start); + await promisifySingle(fds.start)(); - var items = await asyncify(cb => store.all(dataKind.segments, cb)); + var items = await promisifySingle(store.all)(dataKind.segments); expect(Object.keys(items).length).toEqual(0); await sleepAsync(200); await replaceFileContent(path, segmentOnlyJson); await sleepAsync(200); - items = await asyncify(cb => store.all(dataKind.segments, cb)); + items = await promisifySingle(store.all)(dataKind.segments); expect(Object.keys(items).length).toEqual(0); }); it('reloads modified file if auto-update is on', async () => { var path = await makeTempFile(flagOnlyJson); var fds = setupDataSource({ paths: [path], autoUpdate: true }); - await asyncify(fds.start); + await promisifySingle(fds.start)(); - var items = await asyncify(cb => store.all(dataKind.segments, cb)); + var items = await promisifySingle(store.all)(dataKind.segments); expect(Object.keys(items).length).toEqual(0); await sleepAsync(200); await replaceFileContent(path, segmentOnlyJson); await sleepAsync(4000); // the long wait here is to see if we get any spurious reloads (ch32123) - items = await asyncify(cb => store.all(dataKind.segments, cb)); + items = await promisifySingle(store.all)(dataKind.segments); expect(Object.keys(items).length).toEqual(1); // We call logger.warn() once for each reload. It should only have reloaded once, but for @@ -226,7 +224,7 @@ describe('FileDataSource', function() { it('evaluates simplified flag with client as expected', async () => { var path = await makeTempFile(allPropertiesJson); var factory = FileDataSource({ paths: [ path ]}); - var config = { updateProcessor: factory, sendEvents: false }; + var config = { updateProcessor: factory, sendEvents: false, logger: logger }; var client = LaunchDarkly.init('dummy-key', config); var user = { key: 'userkey' }; @@ -242,7 +240,7 @@ describe('FileDataSource', function() { it('evaluates full flag with client as expected', async () => { var path = await makeTempFile(allPropertiesJson); var factory = FileDataSource({ paths: [ path ]}); - var config = { updateProcessor: factory, sendEvents: false }; + var config = { updateProcessor: factory, sendEvents: false, logger: logger }; var client = LaunchDarkly.init('dummy-key', config); var user = { key: 'userkey' }; @@ -254,4 +252,4 @@ describe('FileDataSource', function() { client.close(); } }); -}); \ No newline at end of file +}); diff --git a/test/http_server.js b/test/http_server.js deleted file mode 100644 index c97fb4b..0000000 --- a/test/http_server.js +++ /dev/null @@ -1,82 +0,0 @@ -const http = require('http'); -const https = require('https'); - -// This is adapted from some helper code in https://github.com/EventSource/eventsource/blob/master/test/eventsource_test.js - -let nextPort = 8000; -let servers = []; - -export async function createServer(secure, options) { - const server = secure ? https.createServer(options) : http.createServer(options); - let port = nextPort++; - - server.requests = []; - const responses = []; - - server.on('request', (req, res) => { - server.requests.push(req); - responses.push(res); - }); - - const realClose = server.close; - server.close = callback => { - responses.forEach(res => res.end()); - realClose.call(server, callback); - }; - - servers.push(server); - - while (true) { - try { - await new Promise((resolve, reject) => { - server.listen(port); - server.on('error', reject); - server.on('listening', resolve); - }); - server.url = (secure ? 'https' : 'http') + '://localhost:' + port; - return server; - } catch (err) { - if (err.message.match(/EADDRINUSE/)) { - port = nextPort++; - } else { - throw err; - } - } - } -} - -export function closeServers() { - servers.forEach(server => server.close()); - servers = []; -} - -export function readAll(req) { - return new Promise(resolve => { - let body = ''; - req.on('data', data => { - body += data; - }); - req.on('end', () => resolve(body)); - }); -} - -export function respond(res, status, headers, body) { - res.writeHead(status, headers); - body && res.write(body); - res.end(); -} - -export function respondJson(res, data) { - respond(res, 200, { 'Content-Type': 'application/json' }, JSON.stringify(data)); -} - -export function respondSSEEvent(res, eventType, eventData) { - res.writeHead(200, { 'Content-Type': 'text/event-stream' }) - res.write('event: ' + eventType + '\ndata: ' + JSON.stringify(eventData) + '\n\n'); - res.write(':\n'); - // purposely do not close the stream -} - -export function autoRespond(server, respondFn) { - server.on('request', (req, res) => respondFn(res)); -} diff --git a/test/polling-test.js b/test/polling-test.js index 56f4c7d..03321d5 100644 --- a/test/polling-test.js +++ b/test/polling-test.js @@ -1,7 +1,7 @@ const InMemoryFeatureStore = require('../feature_store'); const PollingProcessor = require('../polling'); const dataKind = require('../versioned_data_kind'); -const { asyncify, asyncifyNode, sleepAsync } = require('./async_utils'); +const { promisify, promisifySingle, sleepAsync } = require('launchdarkly-js-test-helpers'); const stubs = require('./stubs'); describe('PollingProcessor', () => { @@ -48,7 +48,7 @@ describe('PollingProcessor', () => { }; processor = PollingProcessor(config, requestor); - await asyncifyNode(cb => processor.start(cb)); // didn't throw -> success + await promisify(processor.start)(); // didn't throw -> success }); it('initializes feature store', async () => { @@ -57,11 +57,11 @@ describe('PollingProcessor', () => { }; processor = PollingProcessor(config, requestor); - await asyncifyNode(cb => processor.start(cb)); + await promisify(processor.start)(); - const flags = await asyncify(cb => store.all(dataKind.features, cb)); + const flags = await promisifySingle(store.all)(dataKind.features); expect(flags).toEqual(allData.flags); - const segments = await asyncify(cb => store.all(dataKind.segments, cb)); + const segments = await promisifySingle(store.all)(dataKind.segments); expect(segments).toEqual(allData.segments); }); diff --git a/test/redis_feature_store-test.js b/test/redis_feature_store-test.js index 37b18f0..4415f77 100644 --- a/test/redis_feature_store-test.js +++ b/test/redis_feature_store-test.js @@ -1,5 +1,6 @@ const RedisFeatureStore = require('../redis_feature_store'); const testBase = require('./feature_store_test_base'); +const { stubLogger } = require('./stubs'); const redis = require('redis'); @@ -11,15 +12,15 @@ const shouldSkip = (process.env.LD_SKIP_DATABASE_TESTS === '1'); const extraRedisClient = redis.createClient(redisOpts); function makeCachedStore() { - return new RedisFeatureStore(redisOpts, 30); + return new RedisFeatureStore(redisOpts, 30, null, stubLogger()); } function makeUncachedStore() { - return new RedisFeatureStore(redisOpts, 0); + return new RedisFeatureStore(redisOpts, 0, null, stubLogger()); } function makeStoreWithPrefix(prefix) { - return new RedisFeatureStore(redisOpts, 0, prefix); + return new RedisFeatureStore(redisOpts, 0, prefix, stubLogger()); } function clearExistingData(callback) { diff --git a/test/requestor-test.js b/test/requestor-test.js index f913a2c..85c5a84 100644 --- a/test/requestor-test.js +++ b/test/requestor-test.js @@ -1,7 +1,6 @@ import Requestor from '../requestor'; import * as dataKind from '../versioned_data_kind'; -import { asyncifyNode } from './async_utils'; -import * as httpServer from './http_server'; +import { promisify, TestHttpHandlers, TestHttpServer, withCloseable } from 'launchdarkly-js-test-helpers'; describe('Requestor', () => { const sdkKey = 'x'; @@ -9,84 +8,63 @@ describe('Requestor', () => { const someData = { key: { version: 1 } }; const allData = { flags: someData, segments: someData }; - let server; - let config; - - beforeEach(async () => { - server = await httpServer.createServer(); - config = { baseUri: server.url }; - }); - - afterEach(() => { - httpServer.closeServers(); - }); - describe('requestObject', () => { - it('uses correct flag URL', async () => { - httpServer.autoRespond(server, res => httpServer.respondJson(res, {})); - const r = Requestor(sdkKey, config); - await asyncifyNode(cb => r.requestObject(dataKind.features, 'key', cb)); - expect(server.requests.length).toEqual(1); - expect(server.requests[0].url).toEqual('/sdk/latest-flags/key'); - }); + it('gets flag data', async () => + await withCloseable(TestHttpServer.start, async server => { + server.forMethodAndPath('get', '/sdk/latest-flags/key', TestHttpHandlers.respondJson(someData)); + const r = Requestor(sdkKey, { baseUri: server.url }); + const result = await promisify(r.requestObject)(dataKind.features, 'key'); + expect(JSON.parse(result)).toEqual(someData); + }) + ); - it('uses correct segment URL', async () => { - httpServer.autoRespond(server, res => httpServer.respondJson(res, {})); - const r = Requestor(sdkKey, config); - await asyncifyNode(cb => r.requestObject(dataKind.segments, 'key', cb)); - expect(server.requests.length).toEqual(1); - expect(server.requests[0].url).toEqual('/sdk/latest-segments/key'); - }); + it('gets segment data', async () => + await withCloseable(TestHttpServer.start, async server => { + server.forMethodAndPath('get', '/sdk/latest-segments/key', TestHttpHandlers.respondJson(someData)); + const r = Requestor(sdkKey, { baseUri: server.url }); + const result = await promisify(r.requestObject)(dataKind.segments, 'key'); + expect(JSON.parse(result)).toEqual(someData); + }) + ); - it('returns successful result', async () => { - httpServer.autoRespond(server, res => httpServer.respondJson(res, someData)); - const r = Requestor(sdkKey, config); - const result = await asyncifyNode(cb => r.requestObject(dataKind.features, 'key', cb)); - expect(JSON.parse(result)).toEqual(someData); - }); - - it('returns error result for HTTP error', async () => { - httpServer.autoRespond(server, res => httpServer.respond(res, 404)); - const r = Requestor(sdkKey, config); - const req = asyncifyNode(cb => r.requestObject(dataKind.features, 'key', cb)); - await expect(req).rejects.toThrow(/404/); - }); + it('returns error result for HTTP error', async () => + await withCloseable(TestHttpServer.start, async server => { + server.forMethodAndPath('get', '/sdk/latest-flags/key', TestHttpHandlers.respond(404)); + const r = Requestor(sdkKey, { baseUri: server.url }); + const req = promisify(r.requestObject)(dataKind.features, 'key'); + await expect(req).rejects.toThrow(/404/); + }) + ); it('returns error result for network error', async () => { - config.baseUri = badUri; - const r = Requestor(sdkKey, config); - const req = asyncifyNode(cb => r.requestObject(dataKind.features, 'key', cb)); + const r = Requestor(sdkKey, { baseUri: badUri }); + const req = promisify(r.requestObject)(dataKind.features, 'key'); await expect(req).rejects.toThrow(/bad-uri/); }); }); describe('requestAllData', () => { - it('uses correct URL', async () => { - httpServer.autoRespond(server, res => httpServer.respondJson(res, {})); - const r = Requestor(sdkKey, config); - await asyncifyNode(cb => r.requestAllData(cb)); - expect(server.requests.length).toEqual(1); - expect(server.requests[0].url).toEqual('/sdk/latest-all'); - }); + it('gets data', async () => + await withCloseable(TestHttpServer.start, async server => { + server.forMethodAndPath('get', '/sdk/latest-all', TestHttpHandlers.respondJson(allData)); + const r = Requestor(sdkKey, { baseUri: server.url }); + const result = await promisify(r.requestAllData)(); + expect(JSON.parse(result)).toEqual(allData); + }) + ); - it('returns successful result', async () => { - httpServer.autoRespond(server, res => httpServer.respondJson(res, allData)); - const r = Requestor(sdkKey, config); - const result = await asyncifyNode(cb => r.requestAllData(cb)); - expect(JSON.parse(result)).toEqual(allData); - }); - - it('returns error result for HTTP error', async () => { - httpServer.autoRespond(server, res => httpServer.respond(res, 404)); - const r = Requestor(sdkKey, config); - const req = asyncifyNode(cb => r.requestAllData(cb)); - await expect(req).rejects.toThrow(/404/); - }); + it('returns error result for HTTP error', async () => + await withCloseable(TestHttpServer.start, async server => { + server.forMethodAndPath('get', '/sdk/latest-all', TestHttpHandlers.respond(401)); + const r = Requestor(sdkKey, { baseUri: server.url }); + const req = promisify(r.requestAllData)(); + await expect(req).rejects.toThrow(/401/); + }) + ); it('returns error result for network error', async () => { - config.baseUri = badUri; - const r = Requestor(sdkKey, config); - const req = asyncifyNode(cb => r.requestAllData(cb)); + const r = Requestor(sdkKey, { baseUri: badUri }); + const req = promisify(r.requestAllData)(); await expect(req).rejects.toThrow(/bad-uri/); }); }); diff --git a/test/streaming-test.js b/test/streaming-test.js index ae02022..d5b928e 100644 --- a/test/streaming-test.js +++ b/test/streaming-test.js @@ -1,7 +1,7 @@ const InMemoryFeatureStore = require('../feature_store'); const StreamProcessor = require('../streaming'); const dataKind = require('../versioned_data_kind'); -const { asyncify, sleepAsync } = require('./async_utils'); +const { promisifySingle, sleepAsync } = require('launchdarkly-js-test-helpers'); const stubs = require('./stubs'); describe('StreamProcessor', () => { @@ -67,12 +67,12 @@ describe('StreamProcessor', () => { es.handlers.put({ data: JSON.stringify(putData) }); - var flag = await asyncify(cb => featureStore.initialized(cb)); + var flag = await promisifySingle(featureStore.initialized)(); expect(flag).toEqual(true); - var f = await asyncify(cb => featureStore.get(dataKind.features, 'flagkey', cb)); + var f = await promisifySingle(featureStore.get)(dataKind.features, 'flagkey'); expect(f.version).toEqual(1); - var s = await asyncify(cb => featureStore.get(dataKind.segments, 'segkey', cb)); + var s = await promisifySingle(featureStore.get)(dataKind.segments, 'segkey'); expect(s.version).toEqual(2); }); @@ -82,7 +82,7 @@ describe('StreamProcessor', () => { var es = fakeEventSource(); var sp = StreamProcessor(sdkKey, config, null, es.constructor); - var waitUntilStarted = asyncify(cb => sp.start(cb)); + var waitUntilStarted = promisifySingle(sp.start)(); es.handlers.put({ data: JSON.stringify(putData) }); var result = await waitUntilStarted; expect(result).toBe(undefined); @@ -94,7 +94,7 @@ describe('StreamProcessor', () => { var es = fakeEventSource(); var sp = StreamProcessor(sdkKey, config, null, es.constructor); - var waitUntilStarted = asyncify(cb => sp.start(cb)); + var waitUntilStarted = promisifySingle(sp.start)(); es.handlers.put({ data: '{not-good' }); var result = await waitUntilStarted; expectJsonError(result, config); @@ -116,7 +116,7 @@ describe('StreamProcessor', () => { sp.start(); es.handlers.patch({ data: JSON.stringify(patchData) }); - var f = await asyncify(cb => featureStore.get(dataKind.features, 'flagkey', cb)); + var f = await promisifySingle(featureStore.get)(dataKind.features, 'flagkey'); expect(f.version).toEqual(1); }); @@ -134,7 +134,7 @@ describe('StreamProcessor', () => { sp.start(); es.handlers.patch({ data: JSON.stringify(patchData) }); - var s = await asyncify(cb => featureStore.get(dataKind.segments, 'segkey', cb)); + var s = await promisifySingle(featureStore.get)(dataKind.segments, 'segkey'); expect(s.version).toEqual(1); }); @@ -144,7 +144,7 @@ describe('StreamProcessor', () => { var es = fakeEventSource(); var sp = StreamProcessor(sdkKey, config, null, es.constructor); - var waitForCallback = asyncify(cb => sp.start(cb)); + var waitForCallback = promisifySingle(sp.start)(); es.handlers.patch({ data: '{not-good' }); var result = await waitForCallback; expectJsonError(result, config); @@ -161,14 +161,14 @@ describe('StreamProcessor', () => { sp.start(); var flag = { key: 'flagkey', version: 1 } - await asyncify(cb => featureStore.upsert(dataKind.features, flag, cb)); - var f = await asyncify(cb => featureStore.get(dataKind.features, flag.key, cb)); + await promisifySingle(featureStore.upsert)(dataKind.features, flag); + var f = await promisifySingle(featureStore.get)(dataKind.features, flag.key); expect(f).toEqual(flag); var deleteData = { path: '/flags/' + flag.key, version: 2 }; es.handlers.delete({ data: JSON.stringify(deleteData) }); - var f = await asyncify(cb => featureStore.get(dataKind.features, flag.key, cb)); + var f = await promisifySingle(featureStore.get)(dataKind.features, flag.key); expect(f).toBe(null); }); @@ -181,14 +181,14 @@ describe('StreamProcessor', () => { sp.start(); var segment = { key: 'segkey', version: 1 } - await asyncify(cb => featureStore.upsert(dataKind.segments, segment, cb)); - var s = await asyncify(cb => featureStore.get(dataKind.segments, segment.key, cb)); + await promisifySingle(featureStore.upsert)(dataKind.segments, segment); + var s = await promisifySingle(featureStore.get)(dataKind.segments, segment.key); expect(s).toEqual(segment); var deleteData = { path: '/segments/' + segment.key, version: 2 }; es.handlers.delete({ data: JSON.stringify(deleteData) }); - s = await asyncify(cb => featureStore.get(dataKind.segments, segment.key, cb)); + s = await promisifySingle(featureStore.get)(dataKind.segments, segment.key); expect(s).toBe(null); }); @@ -198,7 +198,7 @@ describe('StreamProcessor', () => { var es = fakeEventSource(); var sp = StreamProcessor(sdkKey, config, null, es.constructor); - var waitForResult = asyncify(cb => sp.start(cb)); + var waitForResult = promisifySingle(sp.start)(); es.handlers.delete({ data: '{not-good' }); var result = await waitForResult; expectJsonError(result, config); @@ -231,11 +231,11 @@ describe('StreamProcessor', () => { es.handlers['indirect/put']({}); await sleepAsync(0); - var f = await asyncify(cb => featureStore.get(dataKind.features, 'flagkey', cb)); + var f = await promisifySingle(featureStore.get)(dataKind.features, 'flagkey'); expect(f.version).toEqual(1); - var s = await asyncify(cb => featureStore.get(dataKind.segments, 'segkey', cb)); + var s = await promisifySingle(featureStore.get)(dataKind.segments, 'segkey'); expect(s.version).toEqual(2); - var value = await asyncify(cb => featureStore.initialized(cb)); + var value = await promisifySingle(featureStore.initialized)(); expect(value).toBe(true); }); }); @@ -261,7 +261,7 @@ describe('StreamProcessor', () => { es.handlers['indirect/patch']({ data: '/flags/flagkey' }); await sleepAsync(0); - var f = await asyncify(cb => featureStore.get(dataKind.features, 'flagkey', cb)); + var f = await promisifySingle(featureStore.get)(dataKind.features, 'flagkey'); expect(f.version).toEqual(1); }); @@ -285,7 +285,7 @@ describe('StreamProcessor', () => { es.handlers['indirect/patch']({ data: '/segments/segkey' }); await sleepAsync(0); - var s = await asyncify(cb => featureStore.get(dataKind.segments, 'segkey', cb)); + var s = await promisifySingle(featureStore.get)(dataKind.segments, 'segkey'); expect(s.version).toEqual(1); }); });