From 1cf982b15f6c5d6f1276c8b98d921219e598db86 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 4 Dec 2019 17:34:27 -0800 Subject: [PATCH] use new test helpers + misc test cleanup --- package-lock.json | 381 +++++----- package.json | 3 +- src/__tests__/EventProcessor-test.js | 674 ++++++++--------- src/__tests__/EventSender-test.js | 127 ++-- src/__tests__/EventSummarizer-test.js | 2 + src/__tests__/LDClient-events-test.js | 462 ++++++------ src/__tests__/LDClient-localstorage-test.js | 187 +++-- src/__tests__/LDClient-streaming-test.js | 633 ++++++++-------- src/__tests__/LDClient-test.js | 754 +++++++++++--------- src/__tests__/Requestor-test.js | 310 ++++---- src/__tests__/Stream-test.js | 33 +- src/__tests__/mockHttp.js | 122 ++++ src/__tests__/stubPlatform.js | 60 +- src/__tests__/testUtils.js | 28 - 14 files changed, 2060 insertions(+), 1716 deletions(-) create mode 100644 src/__tests__/mockHttp.js diff --git a/package-lock.json b/package-lock.json index 9c8b73f..4b58a61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "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" } @@ -42,6 +43,7 @@ "version": "7.6.4", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.6.4.tgz", "integrity": "sha512-Rm0HGw101GY8FTzpWSyRbki/jzq+/PkNQJ+nSulrdY6gFGOsNseCqD6KHRYe2E+EdzuBdr2pxCp6s4Uk6eJ+XQ==", + "dev": true, "requires": { "@babel/code-frame": "^7.5.5", "@babel/generator": "^7.6.4", @@ -63,6 +65,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, "requires": { "ms": "^2.1.1" } @@ -70,7 +73,8 @@ "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true } } }, @@ -78,6 +82,7 @@ "version": "7.6.4", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.6.4.tgz", "integrity": "sha512-jsBuXkFoZxk0yWLyGI9llT9oiQ2FeTASmRFE32U+aaDTfoE92t78eroO7PTpU/OrYq38hlcDM6vbfLDaOLy+7w==", + "dev": true, "requires": { "@babel/types": "^7.6.3", "jsesc": "^2.5.1", @@ -88,7 +93,8 @@ "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true } } }, @@ -96,6 +102,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz", "integrity": "sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q==", + "dev": true, "requires": { "@babel/types": "^7.0.0" } @@ -104,6 +111,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.1.0.tgz", "integrity": "sha512-qNSR4jrmJ8M1VMM9tibvyRAHXQs2PmaksQF7c1CGJNipfe3D8p+wgNwgso/P2A2r2mdgBWAXljNWR0QRZAMW8w==", + "dev": true, "requires": { "@babel/helper-explode-assignable-expression": "^7.1.0", "@babel/types": "^7.0.0" @@ -113,6 +121,7 @@ "version": "7.4.4", "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.4.4.tgz", "integrity": "sha512-l79boDFJ8S1c5hvQvG+rc+wHw6IuH7YldmRKsYtpbawsxURu/paVy57FZMomGK22/JckepaikOkY0MoAmdyOlQ==", + "dev": true, "requires": { "@babel/helper-hoist-variables": "^7.4.4", "@babel/traverse": "^7.4.4", @@ -123,6 +132,7 @@ "version": "7.5.5", "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.5.5.tgz", "integrity": "sha512-fTfxx7i0B5NJqvUOBBGREnrqbTxRh7zinBANpZXAVDlsZxYdclDp467G1sQ8VZYMnAURY3RpBUAgOYT9GfzHBg==", + "dev": true, "requires": { "@babel/helper-function-name": "^7.1.0", "@babel/types": "^7.5.5", @@ -132,7 +142,8 @@ "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true } } }, @@ -140,6 +151,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.1.0.tgz", "integrity": "sha512-NRQpfHrJ1msCHtKjbzs9YcMmJZOg6mQMmGRB+hbamEdG5PNpaSm95275VD92DvJKuyl0s2sFiDmMZ+EnnvufqA==", + "dev": true, "requires": { "@babel/traverse": "^7.1.0", "@babel/types": "^7.0.0" @@ -149,6 +161,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", + "dev": true, "requires": { "@babel/helper-get-function-arity": "^7.0.0", "@babel/template": "^7.1.0", @@ -159,6 +172,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", + "dev": true, "requires": { "@babel/types": "^7.0.0" } @@ -167,6 +181,7 @@ "version": "7.4.4", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.4.4.tgz", "integrity": "sha512-VYk2/H/BnYbZDDg39hr3t2kKyifAm1W6zHRfhx8jGjIHpQEBv9dry7oQ2f3+J703TLu69nYdxsovl0XYfcnK4w==", + "dev": true, "requires": { "@babel/types": "^7.4.4" } @@ -175,6 +190,7 @@ "version": "7.5.5", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.5.5.tgz", "integrity": "sha512-5qZ3D1uMclSNqYcXqiHoA0meVdv+xUEex9em2fqMnrk/scphGlGgg66zjMrPJESPwrFJ6sbfFQYUSa0Mz7FabA==", + "dev": true, "requires": { "@babel/types": "^7.5.5" } @@ -183,6 +199,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz", "integrity": "sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==", + "dev": true, "requires": { "@babel/types": "^7.0.0" } @@ -191,6 +208,7 @@ "version": "7.5.5", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.5.5.tgz", "integrity": "sha512-jBeCvETKuJqeiaCdyaheF40aXnnU1+wkSiUs/IQg3tB85up1LyL8x77ClY8qJpuRJUcXQo+ZtdNESmZl4j56Pw==", + "dev": true, "requires": { "@babel/helper-module-imports": "^7.0.0", "@babel/helper-simple-access": "^7.1.0", @@ -203,7 +221,8 @@ "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true } } }, @@ -211,6 +230,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz", "integrity": "sha512-u8nd9NQePYNQV8iPWu/pLLYBqZBa4ZaY1YWRFMuxrid94wKI1QNt67NEZ7GAe5Kc/0LLScbim05xZFWkAdrj9g==", + "dev": true, "requires": { "@babel/types": "^7.0.0" } @@ -218,12 +238,14 @@ "@babel/helper-plugin-utils": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz", - "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==" + "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==", + "dev": true }, "@babel/helper-regex": { "version": "7.5.5", "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.5.5.tgz", "integrity": "sha512-CkCYQLkfkiugbRDO8eZn6lRuR8kzZoGXCg3149iTk5se7g6qykSpy3+hELSwquhu+TgHn8nkLiBwHvNX8Hofcw==", + "dev": true, "requires": { "lodash": "^4.17.13" }, @@ -231,7 +253,8 @@ "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true } } }, @@ -239,6 +262,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz", "integrity": "sha512-3fOK0L+Fdlg8S5al8u/hWE6vhufGSn0bN09xm2LXMy//REAF8kDCrYoOBKYmA8m5Nom+sV9LyLCwrFynA8/slg==", + "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.0.0", "@babel/helper-wrap-function": "^7.1.0", @@ -251,6 +275,7 @@ "version": "7.5.5", "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.5.5.tgz", "integrity": "sha512-XvRFWrNnlsow2u7jXDuH4jDDctkxbS7gXssrP4q2nUD606ukXHRvydj346wmNg+zAgpFx4MWf4+usfC93bElJg==", + "dev": true, "requires": { "@babel/helper-member-expression-to-functions": "^7.5.5", "@babel/helper-optimise-call-expression": "^7.0.0", @@ -262,6 +287,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz", "integrity": "sha512-Vk+78hNjRbsiu49zAPALxTb+JUQCz1aolpd8osOF16BGnLtseD21nbHgLPGUwrXEurZgiCOUmvs3ExTu4F5x6w==", + "dev": true, "requires": { "@babel/template": "^7.1.0", "@babel/types": "^7.0.0" @@ -271,6 +297,7 @@ "version": "7.4.4", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", + "dev": true, "requires": { "@babel/types": "^7.4.4" } @@ -279,6 +306,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz", "integrity": "sha512-o9fP1BZLLSrYlxYEYyl2aS+Flun5gtjTIG8iln+XuEzQTs0PLagAGSXUcqruJwD5fM48jzIEggCKpIfWTcR7pQ==", + "dev": true, "requires": { "@babel/helper-function-name": "^7.1.0", "@babel/template": "^7.1.0", @@ -290,6 +318,7 @@ "version": "7.6.2", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.6.2.tgz", "integrity": "sha512-3/bAUL8zZxYs1cdX2ilEE0WobqbCmKWr/889lf2SS0PpDcpEIY8pb1CCyz0pEcX3pEb+MCbks1jIokz2xLtGTA==", + "dev": true, "requires": { "@babel/template": "^7.6.0", "@babel/traverse": "^7.6.2", @@ -300,6 +329,7 @@ "version": "7.5.0", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", + "dev": true, "requires": { "chalk": "^2.0.0", "esutils": "^2.0.2", @@ -310,6 +340,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -318,6 +349,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -327,12 +359,14 @@ "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -342,12 +376,14 @@ "@babel/parser": { "version": "7.6.4", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.6.4.tgz", - "integrity": "sha512-D8RHPW5qd0Vbyo3qb+YjO5nvUVRTXFLQ/FsDxJU2Nqz4uB5EnUN0ZQSEYpvTIbRuttig1XbHWU5oMeQwQSAA+A==" + "integrity": "sha512-D8RHPW5qd0Vbyo3qb+YjO5nvUVRTXFLQ/FsDxJU2Nqz4uB5EnUN0ZQSEYpvTIbRuttig1XbHWU5oMeQwQSAA+A==", + "dev": true }, "@babel/plugin-proposal-async-generator-functions": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz", "integrity": "sha512-+Dfo/SCQqrwx48ptLVGLdE39YtWRuKc/Y9I5Fy0P1DDBB9lsAHpjcEJQt+4IifuSOSTLBKJObJqMvaO1pIE8LQ==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/helper-remap-async-to-generator": "^7.1.0", @@ -358,6 +394,7 @@ "version": "7.5.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.5.0.tgz", "integrity": "sha512-x/iMjggsKTFHYC6g11PL7Qy58IK8H5zqfm9e6hu4z1iH2IRyAp9u9dL80zA6R76yFovETFLKz2VJIC2iIPBuFw==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/plugin-syntax-dynamic-import": "^7.2.0" @@ -367,6 +404,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz", "integrity": "sha512-MAFV1CA/YVmYwZG0fBQyXhmj0BHCB5egZHCKWIFVv/XCxAeVGIHfos3SwDck4LvCllENIAg7xMKOG5kH0dzyUg==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/plugin-syntax-json-strings": "^7.2.0" @@ -376,6 +414,7 @@ "version": "7.6.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.6.2.tgz", "integrity": "sha512-LDBXlmADCsMZV1Y9OQwMc0MyGZ8Ta/zlD9N67BfQT8uYwkRswiu2hU6nJKrjrt/58aH/vqfQlR/9yId/7A2gWw==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/plugin-syntax-object-rest-spread": "^7.2.0" @@ -385,6 +424,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz", "integrity": "sha512-mgYj3jCcxug6KUcX4OBoOJz3CMrwRfQELPQ5560F70YQUBZB7uac9fqaWamKR1iWUzGiK2t0ygzjTScZnVz75g==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/plugin-syntax-optional-catch-binding": "^7.2.0" @@ -394,6 +434,7 @@ "version": "7.6.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.6.2.tgz", "integrity": "sha512-NxHETdmpeSCtiatMRYWVJo7266rrvAC3DTeG5exQBIH/fMIUK7ejDNznBbn3HQl/o9peymRRg7Yqkx6PdUXmMw==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/helper-regex": "^7.4.4", @@ -403,12 +444,14 @@ "jsesc": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "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", @@ -421,12 +464,14 @@ "regjsgen": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.1.tgz", - "integrity": "sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg==" + "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" } @@ -437,6 +482,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz", "integrity": "sha512-1ZrIRBv2t0GSlcwVoQ6VgSLpLgiN/FVQUzt9znxo7v2Ov4jJrs8RY8tv0wvDmFN3qIdMKWrmMMW6yZ0G19MfGg==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -445,6 +491,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz", "integrity": "sha512-mVxuJ0YroI/h/tbFTPGZR8cv6ai+STMKNBq0f8hFxsxWjl94qqhsb+wXbpNMDPU3cfR1TIsVFzU3nXyZMqyK4w==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -453,6 +500,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz", "integrity": "sha512-5UGYnMSLRE1dqqZwug+1LISpA403HzlSfsg6P9VXU6TBjcSHeNlw4DxDx7LgpF+iKZoOG/+uzqoRHTdcUpiZNg==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -461,6 +509,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz", "integrity": "sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -469,6 +518,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.2.0.tgz", "integrity": "sha512-bDe4xKNhb0LI7IvZHiA13kff0KEfaGX/Hv4lMA9+7TEc63hMNvfKo6ZFpXhKuEp+II/q35Gc4NoMeDZyaUbj9w==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -477,6 +527,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz", "integrity": "sha512-ER77Cax1+8/8jCB9fo4Ud161OZzWN5qawi4GusDuRLcDbDG+bIGYY20zb2dfAFdTRGzrfq2xZPvF0R64EHnimg==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -485,6 +536,7 @@ "version": "7.5.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.5.0.tgz", "integrity": "sha512-mqvkzwIGkq0bEF1zLRRiTdjfomZJDV33AH3oQzHVGkI2VzEmXLpKKOBvEVaFZBJdN0XTyH38s9j/Kiqr68dggg==", + "dev": true, "requires": { "@babel/helper-module-imports": "^7.0.0", "@babel/helper-plugin-utils": "^7.0.0", @@ -495,6 +547,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.2.0.tgz", "integrity": "sha512-ntQPR6q1/NKuphly49+QiQiTN0O63uOwjdD6dhIjSWBI5xlrbUFh720TIpzBhpnrLfv2tNH/BXvLIab1+BAI0w==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -503,6 +556,7 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.6.3.tgz", "integrity": "sha512-7hvrg75dubcO3ZI2rjYTzUrEuh1E9IyDEhhB6qfcooxhDA33xx2MasuLVgdxzcP6R/lipAC6n9ub9maNW6RKdw==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "lodash": "^4.17.13" @@ -511,7 +565,8 @@ "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true } } }, @@ -519,6 +574,7 @@ "version": "7.5.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.5.5.tgz", "integrity": "sha512-U2htCNK/6e9K7jGyJ++1p5XRU+LJjrwtoiVn9SzRlDT2KubcZ11OOwy3s24TjHxPgxNwonCYP7U2K51uVYCMDg==", + "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.0.0", "@babel/helper-define-map": "^7.5.5", @@ -534,6 +590,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.2.0.tgz", "integrity": "sha512-kP/drqTxY6Xt3NNpKiMomfgkNn4o7+vKxK2DDKcBG9sHj51vHqMBGy8wbDS/J4lMxnqs153/T3+DmCEAkC5cpA==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -542,6 +599,7 @@ "version": "7.6.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.6.0.tgz", "integrity": "sha512-2bGIS5P1v4+sWTCnKNDZDxbGvEqi0ijeqM/YqHtVGrvG2y0ySgnEEhXErvE9dA0bnIzY9bIzdFK0jFA46ASIIQ==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -550,6 +608,7 @@ "version": "7.6.2", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.6.2.tgz", "integrity": "sha512-KGKT9aqKV+9YMZSkowzYoYEiHqgaDhGmPNZlZxX6UeHC4z30nC1J9IrZuGqbYFB1jaIGdv91ujpze0exiVK8bA==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/helper-regex": "^7.4.4", @@ -559,12 +618,14 @@ "jsesc": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "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", @@ -577,12 +638,14 @@ "regjsgen": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.1.tgz", - "integrity": "sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg==" + "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" } @@ -593,6 +656,7 @@ "version": "7.5.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.5.0.tgz", "integrity": "sha512-igcziksHizyQPlX9gfSjHkE2wmoCH3evvD2qR5w29/Dk0SMKE/eOI7f1HhBdNhR/zxJDqrgpoDTq5YSLH/XMsQ==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -601,6 +665,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.2.0.tgz", "integrity": "sha512-umh4hR6N7mu4Elq9GG8TOu9M0bakvlsREEC+ialrQN6ABS4oDQ69qJv1VtR3uxlKMCQMCvzk7vr17RHKcjx68A==", + "dev": true, "requires": { "@babel/helper-builder-binary-assignment-operator-visitor": "^7.1.0", "@babel/helper-plugin-utils": "^7.0.0" @@ -610,6 +675,7 @@ "version": "7.4.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.4.4.tgz", "integrity": "sha512-9T/5Dlr14Z9TIEXLXkt8T1DU7F24cbhwhMNUziN3hB1AXoZcdzPcTiKGRn/6iOymDqtTKWnr/BtRKN9JwbKtdQ==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -618,6 +684,7 @@ "version": "7.4.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.4.4.tgz", "integrity": "sha512-iU9pv7U+2jC9ANQkKeNF6DrPy4GBa4NWQtl6dHB4Pb3izX2JOEvDTFarlNsBj/63ZEzNNIAMs3Qw4fNCcSOXJA==", + "dev": true, "requires": { "@babel/helper-function-name": "^7.1.0", "@babel/helper-plugin-utils": "^7.0.0" @@ -627,6 +694,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.2.0.tgz", "integrity": "sha512-2ThDhm4lI4oV7fVQ6pNNK+sx+c/GM5/SaML0w/r4ZB7sAneD/piDJtwdKlNckXeyGK7wlwg2E2w33C/Hh+VFCg==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -635,6 +703,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.2.0.tgz", "integrity": "sha512-HiU3zKkSU6scTidmnFJ0bMX8hz5ixC93b4MHMiYebmk2lUVNGOboPsqQvx5LzooihijUoLR/v7Nc1rbBtnc7FA==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -643,6 +712,7 @@ "version": "7.5.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.5.0.tgz", "integrity": "sha512-n20UsQMKnWrltocZZm24cRURxQnWIvsABPJlw/fvoy9c6AgHZzoelAIzajDHAQrDpuKFFPPcFGd7ChsYuIUMpg==", + "dev": true, "requires": { "@babel/helper-module-transforms": "^7.1.0", "@babel/helper-plugin-utils": "^7.0.0", @@ -653,6 +723,7 @@ "version": "7.6.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.6.0.tgz", "integrity": "sha512-Ma93Ix95PNSEngqomy5LSBMAQvYKVe3dy+JlVJSHEXZR5ASL9lQBedMiCyVtmTLraIDVRE3ZjTZvmXXD2Ozw3g==", + "dev": true, "requires": { "@babel/helper-module-transforms": "^7.4.4", "@babel/helper-plugin-utils": "^7.0.0", @@ -664,6 +735,7 @@ "version": "7.5.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.5.0.tgz", "integrity": "sha512-Q2m56tyoQWmuNGxEtUyeEkm6qJYFqs4c+XyXH5RAuYxObRNz9Zgj/1g2GMnjYp2EUyEy7YTrxliGCXzecl/vJg==", + "dev": true, "requires": { "@babel/helper-hoist-variables": "^7.4.4", "@babel/helper-plugin-utils": "^7.0.0", @@ -674,6 +746,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.2.0.tgz", "integrity": "sha512-BV3bw6MyUH1iIsGhXlOK6sXhmSarZjtJ/vMiD9dNmpY8QXFFQTj+6v92pcfy1iqa8DeAfJFwoxcrS/TUZda6sw==", + "dev": true, "requires": { "@babel/helper-module-transforms": "^7.1.0", "@babel/helper-plugin-utils": "^7.0.0" @@ -683,6 +756,7 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.6.3.tgz", "integrity": "sha512-jTkk7/uE6H2s5w6VlMHeWuH+Pcy2lmdwFoeWCVnvIrDUnB5gQqTVI8WfmEAhF2CDEarGrknZcmSFg1+bkfCoSw==", + "dev": true, "requires": { "regexpu-core": "^4.6.0" }, @@ -690,12 +764,14 @@ "jsesc": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "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", @@ -708,12 +784,14 @@ "regjsgen": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.1.tgz", - "integrity": "sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg==" + "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" } @@ -724,6 +802,7 @@ "version": "7.4.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.4.4.tgz", "integrity": "sha512-r1z3T2DNGQwwe2vPGZMBNjioT2scgWzK9BCnDEh+46z8EEwXBq24uRzd65I7pjtugzPSj921aM15RpESgzsSuA==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -732,6 +811,7 @@ "version": "7.5.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.5.5.tgz", "integrity": "sha512-un1zJQAhSosGFBduPgN/YFNvWVpRuHKU7IHBglLoLZsGmruJPOo6pbInneflUdmq7YvSVqhpPs5zdBvLnteltQ==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/helper-replace-supers": "^7.5.5" @@ -741,6 +821,7 @@ "version": "7.4.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.4.4.tgz", "integrity": "sha512-oMh5DUO1V63nZcu/ZVLQFqiihBGo4OpxJxR1otF50GMeCLiRx5nUdtokd+u9SuVJrvvuIh9OosRFPP4pIPnwmw==", + "dev": true, "requires": { "@babel/helper-call-delegate": "^7.4.4", "@babel/helper-get-function-arity": "^7.0.0", @@ -751,6 +832,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.2.0.tgz", "integrity": "sha512-9q7Dbk4RhgcLp8ebduOpCbtjh7C0itoLYHXd9ueASKAG/is5PQtMR5VJGka9NKqGhYEGn5ITahd4h9QeBMylWQ==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -759,6 +841,7 @@ "version": "7.4.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.4.5.tgz", "integrity": "sha512-gBKRh5qAaCWntnd09S8QC7r3auLCqq5DI6O0DlfoyDjslSBVqBibrMdsqO+Uhmx3+BlOmE/Kw1HFxmGbv0N9dA==", + "dev": true, "requires": { "regenerator-transform": "^0.14.0" } @@ -767,6 +850,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.2.0.tgz", "integrity": "sha512-fz43fqW8E1tAB3DKF19/vxbpib1fuyCwSPE418ge5ZxILnBhWyhtPgz8eh1RCGGJlwvksHkyxMxh0eenFi+kFw==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -795,6 +879,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz", "integrity": "sha512-QP4eUM83ha9zmYtpbnyjTLAGKQritA5XW/iG9cjtuOI8s1RuL/3V6a3DeSHfKutJQ+ayUfeZJPcnCYEQzaPQqg==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -803,6 +888,7 @@ "version": "7.6.2", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.6.2.tgz", "integrity": "sha512-DpSvPFryKdK1x+EDJYCy28nmAaIMdxmhot62jAXF/o99iA33Zj2Lmcp3vDmz+MUh0LNYVPvfj5iC3feb3/+PFg==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -811,6 +897,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.2.0.tgz", "integrity": "sha512-KKYCoGaRAf+ckH8gEL3JHUaFVyNHKe3ASNsZ+AlktgHevvxGigoIttrEJb8iKN03Q7Eazlv1s6cx2B2cQ3Jabw==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/helper-regex": "^7.0.0" @@ -820,6 +907,7 @@ "version": "7.4.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.4.4.tgz", "integrity": "sha512-mQrEC4TWkhLN0z8ygIvEL9ZEToPhG5K7KDW3pzGqOfIGZ28Jb0POUkeWcoz8HnHvhFy6dwAT1j8OzqN8s804+g==", + "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.0.0", "@babel/helper-plugin-utils": "^7.0.0" @@ -829,6 +917,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.2.0.tgz", "integrity": "sha512-2LNhETWYxiYysBtrBTqL8+La0jIoQQnIScUJc74OYvUGRmkskNY4EzLCnjHBzdmb38wqtTaixpo1NctEcvMDZw==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -837,6 +926,7 @@ "version": "7.6.2", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.6.2.tgz", "integrity": "sha512-orZI6cWlR3nk2YmYdb0gImrgCUwb5cBUwjf6Ks6dvNVvXERkwtJWOQaEOjPiu0Gu1Tq6Yq/hruCZZOOi9F34Dw==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/helper-regex": "^7.4.4", @@ -846,12 +936,14 @@ "jsesc": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "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", @@ -864,12 +956,14 @@ "regjsgen": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.1.tgz", - "integrity": "sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg==" + "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" } @@ -889,6 +983,7 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.6.3.tgz", "integrity": "sha512-CWQkn7EVnwzlOdR5NOm2+pfgSNEZmvGjOhlCHBDq0J8/EStr+G+FvPEiz9B56dR6MoiUFjXhfE4hjLoAKKJtIQ==", + "dev": true, "requires": { "@babel/helper-module-imports": "^7.0.0", "@babel/helper-plugin-utils": "^7.0.0", @@ -946,6 +1041,7 @@ "version": "4.7.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.7.2.tgz", "integrity": "sha512-uZavT/gZXJd2UTi9Ov7/Z340WOSQ3+m1iBVRUknf+okKxonL9P83S3ctiBDtuRmRu8PiCHjqyueqQ9HYlJhxiw==", + "dev": true, "requires": { "caniuse-lite": "^1.0.30001004", "electron-to-chromium": "^1.3.295", @@ -955,12 +1051,14 @@ "caniuse-lite": { "version": "1.0.30001005", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001005.tgz", - "integrity": "sha512-g78miZm1Z5njjYR216a5812oPiLgV1ssndgGxITHWUopmjUrCswMisA0a2kSB7a0vZRox6JOKhM51+efmYN8Mg==" + "integrity": "sha512-g78miZm1Z5njjYR216a5812oPiLgV1ssndgGxITHWUopmjUrCswMisA0a2kSB7a0vZRox6JOKhM51+efmYN8Mg==", + "dev": true }, "electron-to-chromium": { "version": "1.3.296", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.296.tgz", - "integrity": "sha512-s5hv+TSJSVRsxH190De66YHb50pBGTweT9XGWYu/LMR20KX6TsjFzObo36CjVAzM+PUeeKSBRtm/mISlCzeojQ==" + "integrity": "sha512-s5hv+TSJSVRsxH190De66YHb50pBGTweT9XGWYu/LMR20KX6TsjFzObo36CjVAzM+PUeeKSBRtm/mISlCzeojQ==", + "dev": true } } }, @@ -968,6 +1066,7 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.6.3.tgz", "integrity": "sha512-kq6anf9JGjW8Nt5rYfEuGRaEAaH1mkv3Bbu6rYvLOpPh/RusSJXuKPEAoZ7L7gybZkchE8+NV5g9vKF4AGAtsA==", + "dev": true, "requires": { "regenerator-runtime": "^0.13.2" } @@ -976,6 +1075,7 @@ "version": "7.6.0", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.6.0.tgz", "integrity": "sha512-5AEH2EXD8euCk446b7edmgFdub/qfH1SN6Nii3+fyXP807QRx9Q73A2N5hNwRRslC2H9sNzaFhsPubkS4L8oNQ==", + "dev": true, "requires": { "@babel/code-frame": "^7.0.0", "@babel/parser": "^7.6.0", @@ -986,6 +1086,7 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.6.3.tgz", "integrity": "sha512-unn7P4LGsijIxaAJo/wpoU11zN+2IaClkQAxcJWBNCMS6cmVh802IyLHNkAjQ0iYnRS3nnxk5O3fuXW28IMxTw==", + "dev": true, "requires": { "@babel/code-frame": "^7.5.5", "@babel/generator": "^7.6.3", @@ -1002,6 +1103,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, "requires": { "ms": "^2.1.1" } @@ -1009,7 +1111,8 @@ "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true } } }, @@ -1017,6 +1120,7 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.3.tgz", "integrity": "sha512-CqbcpTxMcpuQTMhjI37ZHVgjBkysg5icREQIEZ0eG1yCNwg3oy+5AaLiOKmjsCj6nqOsa6Hf0ObjRVwokb7srA==", + "dev": true, "requires": { "esutils": "^2.0.2", "lodash": "^4.17.13", @@ -1026,7 +1130,8 @@ "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true } } }, @@ -1440,42 +1545,6 @@ "estree-walker": "^0.6.1" } }, - "@sinonjs/commons": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz", - "integrity": "sha512-9jHK3YF/8HtJ9wCAbG+j8cD0i0+ATS9A7gXFqS36TblLPNy6rEEc+SB0imo91eCboGaBYGV/MT1/br/J+EE7Tw==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - }, - "@sinonjs/formatio": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz", - "integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1", - "@sinonjs/samsam": "^3.1.0" - } - }, - "@sinonjs/samsam": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.2.0.tgz", - "integrity": "sha512-j5F1rScewLtx6pbTK0UAjA3jJj4RYiSKOix53YWv+Jzy/AZ69qHxUpU8fwVLjyKbEEud9QrLpv6Ggs7WqTimYw==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.0.2", - "array-from": "^2.1.1", - "lodash": "^4.17.11" - } - }, - "@sinonjs/text-encoding": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", - "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", - "dev": true - }, "@types/babel__core": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.3.tgz", @@ -2052,12 +2121,6 @@ "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", "dev": true }, - "array-from": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", - "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", - "dev": true - }, "array-union": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", @@ -2230,6 +2293,7 @@ "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" } @@ -2894,6 +2958,7 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, "requires": { "color-name": "1.1.3" } @@ -2901,7 +2966,8 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true }, "colors": { "version": "1.4.0", @@ -2952,6 +3018,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", + "dev": true, "requires": { "safe-buffer": "~5.1.1" } @@ -2971,6 +3038,7 @@ "version": "3.3.5", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.3.5.tgz", "integrity": "sha512-44ZORuapx0MUht0MUk0p9lcQPh7n/LDXehimTmjCs0CYblpKZcqVd5w0OQDUDq5OQjEbazWObHDQJWvvHYPNTg==", + "dev": true, "requires": { "browserslist": "^4.7.2", "semver": "^6.3.0" @@ -2980,6 +3048,7 @@ "version": "4.7.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.7.2.tgz", "integrity": "sha512-uZavT/gZXJd2UTi9Ov7/Z340WOSQ3+m1iBVRUknf+okKxonL9P83S3ctiBDtuRmRu8PiCHjqyueqQ9HYlJhxiw==", + "dev": true, "requires": { "caniuse-lite": "^1.0.30001004", "electron-to-chromium": "^1.3.295", @@ -2989,17 +3058,20 @@ "caniuse-lite": { "version": "1.0.30001005", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001005.tgz", - "integrity": "sha512-g78miZm1Z5njjYR216a5812oPiLgV1ssndgGxITHWUopmjUrCswMisA0a2kSB7a0vZRox6JOKhM51+efmYN8Mg==" + "integrity": "sha512-g78miZm1Z5njjYR216a5812oPiLgV1ssndgGxITHWUopmjUrCswMisA0a2kSB7a0vZRox6JOKhM51+efmYN8Mg==", + "dev": true }, "electron-to-chromium": { "version": "1.3.296", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.296.tgz", - "integrity": "sha512-s5hv+TSJSVRsxH190De66YHb50pBGTweT9XGWYu/LMR20KX6TsjFzObo36CjVAzM+PUeeKSBRtm/mISlCzeojQ==" + "integrity": "sha512-s5hv+TSJSVRsxH190De66YHb50pBGTweT9XGWYu/LMR20KX6TsjFzObo36CjVAzM+PUeeKSBRtm/mISlCzeojQ==", + "dev": true }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true } } }, @@ -3132,6 +3204,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, "requires": { "object-keys": "^1.0.12" } @@ -3216,12 +3289,6 @@ "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", "dev": true }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true - }, "diff-sequences": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.9.0.tgz", @@ -3319,7 +3386,8 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true }, "escodegen": { "version": "1.11.0", @@ -3617,7 +3685,8 @@ "esutils": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true }, "exec-sh": { "version": "0.3.2", @@ -4543,7 +4612,8 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true }, "functional-red-black-tree": { "version": "1.0.1", @@ -4610,7 +4680,8 @@ "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true }, "globby": { "version": "5.0.0", @@ -4713,12 +4784,14 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true }, "has-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=" + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true }, "has-value": { "version": "1.0.0", @@ -4925,6 +4998,7 @@ "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, "requires": { "loose-envify": "^1.0.0" } @@ -6184,12 +6258,14 @@ "js-levenshtein": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", - "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==" + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true }, "js-tokens": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true }, "js-yaml": { "version": "3.13.1", @@ -6245,7 +6321,8 @@ "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true }, "json-parse-better-errors": { "version": "1.0.2", @@ -6281,6 +6358,7 @@ "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" }, @@ -6288,7 +6366,8 @@ "minimist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true } } }, @@ -6304,12 +6383,6 @@ "verror": "1.10.0" } }, - "just-extend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", - "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", - "dev": true - }, "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", @@ -6326,9 +6399,10 @@ "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==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/launchdarkly-js-test-helpers/-/launchdarkly-js-test-helpers-1.1.0.tgz", + "integrity": "sha512-6kNqQ359DbKjefJV1z6YVgU/yHSQBWvNz0U6DfaS04Km8OTO6uH1bwVJbtseD4wI7aG/BDbdF7R/xkZE9wRfoA==", + "dev": true, "requires": { "@babel/core": "^7.6.4", "@babel/preset-env": "^7.6.3", @@ -6340,7 +6414,8 @@ "@types/node": { "version": "12.12.14", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.14.tgz", - "integrity": "sha512-u/SJDyXwuihpwjXy7hOOghagLEV1KdAST6syfnOk6QZAMzZuWZqXy5aYYZbh8Jdpd4escVFP0MvftHNDb9pruA==" + "integrity": "sha512-u/SJDyXwuihpwjXy7hOOghagLEV1KdAST6syfnOk6QZAMzZuWZqXy5aYYZbh8Jdpd4escVFP0MvftHNDb9pruA==", + "dev": true } } }, @@ -6454,16 +6529,11 @@ } } }, - "lolex": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-3.1.0.tgz", - "integrity": "sha512-zFo5MgCJ0rZ7gQg69S4pqBsLURbFw11X68C18OcJjJQbqaXm2NoTrGl1IMM3TIz0/BnN1tIs2tzmmqvCsOMMjw==", - "dev": true - }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, "requires": { "js-tokens": "^3.0.0 || ^4.0.0" } @@ -6639,7 +6709,8 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "mute-stream": { "version": "0.0.7", @@ -6711,31 +6782,11 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, - "nise": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.10.tgz", - "integrity": "sha512-sa0RRbj53dovjc7wombHmVli9ZihXbXCQ2uH3TNm03DyvOSIQbxg+pbqDKrk2oxMK1rtLGVlKxcB9rrc6X5YjA==", - "dev": true, - "requires": { - "@sinonjs/formatio": "^3.1.0", - "@sinonjs/text-encoding": "^0.7.1", - "just-extend": "^4.0.2", - "lolex": "^2.3.2", - "path-to-regexp": "^1.7.0" - }, - "dependencies": { - "lolex": { - "version": "2.7.5", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz", - "integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==", - "dev": true - } - } - }, "node-forge": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz", - "integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==" + "integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==", + "dev": true }, "node-int64": { "version": "0.4.0", @@ -6766,6 +6817,7 @@ "version": "1.1.39", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.39.tgz", "integrity": "sha512-8MRC/ErwNCHOlAFycy9OPca46fQYUjbJRDcZTHVWIGXIjYLM73k70vv3WkYutVnM4cCo4hE0MqBVVZjP6vjISA==", + "dev": true, "requires": { "semver": "^6.3.0" }, @@ -6773,7 +6825,8 @@ "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true } } }, @@ -6856,7 +6909,8 @@ "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true }, "object-visit": { "version": "1.0.1", @@ -6879,6 +6933,7 @@ "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", @@ -7081,24 +7136,8 @@ "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" - }, - "path-to-regexp": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", - "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", - "dev": true, - "requires": { - "isarray": "0.0.1" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - } - } + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true }, "path-type": { "version": "3.0.0", @@ -7239,7 +7278,8 @@ "private": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==" + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "dev": true }, "process-nextick-args": { "version": "2.0.0", @@ -7671,12 +7711,14 @@ "regenerate": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", - "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==" + "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", + "dev": true }, "regenerate-unicode-properties": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz", "integrity": "sha512-LGZzkgtLY79GeXLm8Dp0BVLdQlWICzBnJz/ipWUgo59qBaZ+BHtq51P2q1uVZlppMuUAT37SDk39qUbjTWB7bA==", + "dev": true, "requires": { "regenerate": "^1.4.0" } @@ -7690,6 +7732,7 @@ "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" } @@ -7802,6 +7845,7 @@ "version": "1.12.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", + "dev": true, "requires": { "path-parse": "^1.0.6" } @@ -7984,7 +8028,8 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true }, "safe-regex": { "version": "1.1.0", @@ -8036,6 +8081,7 @@ "version": "1.10.7", "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.7.tgz", "integrity": "sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==", + "dev": true, "requires": { "node-forge": "0.9.0" } @@ -8043,7 +8089,8 @@ "semver": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true }, "semver-compare": { "version": "1.0.0", @@ -8113,32 +8160,6 @@ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true }, - "sinon": { - "version": "7.2.7", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.2.7.tgz", - "integrity": "sha512-rlrre9F80pIQr3M36gOdoCEWzFAMDgHYD8+tocqOw+Zw9OZ8F84a80Ds69eZfcjnzDqqG88ulFld0oin/6rG/g==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.3.1", - "@sinonjs/formatio": "^3.2.1", - "@sinonjs/samsam": "^3.2.0", - "diff": "^3.5.0", - "lolex": "^3.1.0", - "nise": "^1.4.10", - "supports-color": "^5.5.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, "sisteransi": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.3.tgz", @@ -8286,7 +8307,8 @@ "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true }, "source-map-resolve": { "version": "0.5.2", @@ -8668,7 +8690,8 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true }, "to-object-path": { "version": "0.3.0", @@ -8815,12 +8838,14 @@ "unicode-canonical-property-names-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", - "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==" + "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", + "dev": true }, "unicode-match-property-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", + "dev": true, "requires": { "unicode-canonical-property-names-ecmascript": "^1.0.4", "unicode-property-aliases-ecmascript": "^1.0.4" @@ -8829,12 +8854,14 @@ "unicode-match-property-value-ecmascript": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz", - "integrity": "sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g==" + "integrity": "sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g==", + "dev": true }, "unicode-property-aliases-ecmascript": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz", - "integrity": "sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==" + "integrity": "sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==", + "dev": true }, "union-value": { "version": "1.0.1", diff --git a/package.json b/package.json index 1355e2e..3386f42 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "eslint-plugin-prettier": "2.6.0", "jest": "^24.9.0", "jsdom": "^11.11.0", + "launchdarkly-js-test-helpers": "1.1.0", "prettier": "1.11.1", "readline-sync": "1.4.9", "rimraf": "2.6.2", @@ -67,14 +68,12 @@ "rollup-plugin-uglify": "^6.0.3", "semver": "5.5.0", "semver-compare": "1.0.0", - "sinon": "7.2.7", "typescript": "3.0.1" }, "dependencies": { "@babel/polyfill": "7.6.0", "base64-js": "1.3.0", "fast-deep-equal": "2.0.1", - "launchdarkly-js-test-helpers": "1.0.0", "uuid": "3.3.2" }, "repository": { diff --git a/src/__tests__/EventProcessor-test.js b/src/__tests__/EventProcessor-test.js index 5879e68..2365764 100644 --- a/src/__tests__/EventProcessor-test.js +++ b/src/__tests__/EventProcessor-test.js @@ -1,13 +1,13 @@ -import sinon from 'sinon'; - import EventProcessor from '../EventProcessor'; import * as messages from '../messages'; import * as stubPlatform from './stubPlatform'; +// These tests verify that the event processor produces the expected event payload data for +// various inputs. The actual delivery of data is done by EventSender, which has its own +// tests; here, we use a mock EventSender. + describe('EventProcessor', () => { - let sandbox; - const mockEventSender = {}; const user = { key: 'userKey', name: 'Red' }; const filteredUser = { key: 'userKey', privateAttrs: ['name'] }; const eventsUrl = '/fake-url'; @@ -22,23 +22,35 @@ describe('EventProcessor', () => { }; const platform = stubPlatform.defaults(); - mockEventSender.sendEvents = function(events, sync) { - mockEventSender.calls.push({ - events: events, - sync: !!sync, - }); - return Promise.resolve({ serverTime: mockEventSender.serverTime, status: mockEventSender.status || 200 }); - }; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - mockEventSender.calls = []; - mockEventSender.serverTime = null; - }); + function createMockEventSender() { + const calls = []; + let serverTime = null; + let status = 200; + const sender = { + calls, + sendEvents: (events, sync) => { + calls.push({ events: events, sync: !!sync }); + return Promise.resolve({ serverTime, status }); + }, + setServerTime: time => { + serverTime = time; + }, + setStatus: respStatus => { + status = respStatus; + }, + }; + return sender; + } - afterEach(() => { - sandbox.restore(); - }); + async function withProcessorAndSender(config, asyncCallback) { + const sender = createMockEventSender(); + const ep = EventProcessor(platform, config, envId, null, sender); + try { + return await asyncCallback(ep, sender); + } finally { + ep.stop(); + } + } function checkFeatureEvent(e, source, debug, inlineUser) { expect(e.kind).toEqual(debug ? 'debug' : 'feature'); @@ -73,381 +85,397 @@ describe('EventProcessor', () => { } it('should enqueue identify event', async () => { - const ep = EventProcessor(platform, defaultConfig, envId, null, mockEventSender); - const event = { kind: 'identify', creationDate: 1000, key: user.key, user: user }; - ep.enqueue(event); - await ep.flush(); + await withProcessorAndSender(defaultConfig, async (ep, mockEventSender) => { + const event = { kind: 'identify', creationDate: 1000, key: user.key, user: user }; + ep.enqueue(event); + await ep.flush(); - expect(mockEventSender.calls.length).toEqual(1); - expect(mockEventSender.calls[0].events).toEqual([event]); + expect(mockEventSender.calls.length).toEqual(1); + expect(mockEventSender.calls[0].events).toEqual([event]); + }); }); it('filters user in identify event', async () => { const config = { ...defaultConfig, allAttributesPrivate: true }; - const ep = EventProcessor(platform, config, envId, null, mockEventSender); - const event = { kind: 'identify', creationDate: 1000, key: user.key, user: user }; - ep.enqueue(event); - await ep.flush(); - - expect(mockEventSender.calls.length).toEqual(1); - expect(mockEventSender.calls[0].events).toEqual([ - { - kind: 'identify', - creationDate: event.creationDate, - key: user.key, - user: filteredUser, - }, - ]); + await withProcessorAndSender(config, async (ep, mockEventSender) => { + const event = { kind: 'identify', creationDate: 1000, key: user.key, user: user }; + ep.enqueue(event); + await ep.flush(); + + expect(mockEventSender.calls.length).toEqual(1); + expect(mockEventSender.calls[0].events).toEqual([ + { + kind: 'identify', + creationDate: event.creationDate, + key: user.key, + user: filteredUser, + }, + ]); + }); }); it('queues individual feature event', async () => { - const ep = EventProcessor(platform, defaultConfig, envId, null, mockEventSender); - const event = { - kind: 'feature', - creationDate: 1000, - key: 'flagkey', - user: user, - trackEvents: true, - }; - ep.enqueue(event); - await ep.flush(); - - expect(mockEventSender.calls.length).toEqual(1); - const output = mockEventSender.calls[0].events; - expect(output.length).toEqual(2); - checkFeatureEvent(output[0], event, false); - checkSummaryEvent(output[1]); + await withProcessorAndSender(defaultConfig, async (ep, mockEventSender) => { + const event = { + kind: 'feature', + creationDate: 1000, + key: 'flagkey', + user: user, + trackEvents: true, + }; + ep.enqueue(event); + await ep.flush(); + + expect(mockEventSender.calls.length).toEqual(1); + const output = mockEventSender.calls[0].events; + expect(output.length).toEqual(2); + checkFeatureEvent(output[0], event, false); + checkSummaryEvent(output[1]); + }); }); it('can include inline user in feature event', async () => { const config = { ...defaultConfig, inlineUsersInEvents: true }; - const ep = EventProcessor(platform, config, envId, null, mockEventSender); - const event = { - kind: 'feature', - creationDate: 1000, - key: 'flagkey', - user: user, - trackEvents: true, - }; - ep.enqueue(event); - await ep.flush(); - - expect(mockEventSender.calls.length).toEqual(1); - const output = mockEventSender.calls[0].events; - expect(output.length).toEqual(2); - checkFeatureEvent(output[0], event, false, user); - checkSummaryEvent(output[1]); + await withProcessorAndSender(config, async (ep, mockEventSender) => { + const event = { + kind: 'feature', + creationDate: 1000, + key: 'flagkey', + user: user, + trackEvents: true, + }; + ep.enqueue(event); + await ep.flush(); + + expect(mockEventSender.calls.length).toEqual(1); + const output = mockEventSender.calls[0].events; + expect(output.length).toEqual(2); + checkFeatureEvent(output[0], event, false, user); + checkSummaryEvent(output[1]); + }); }); it('can include reason in feature event', async () => { const config = { ...defaultConfig, inlineUsersInEvents: true }; const reason = { kind: 'FALLTHROUGH' }; - const ep = EventProcessor(platform, config, envId, null, mockEventSender); - const event = { - kind: 'feature', - creationDate: 1000, - key: 'flagkey', - user: user, - trackEvents: true, - reason: reason, - }; - ep.enqueue(event); - await ep.flush(); - - expect(mockEventSender.calls.length).toEqual(1); - const output = mockEventSender.calls[0].events; - expect(output.length).toEqual(2); - checkFeatureEvent(output[0], event, false, user); - checkSummaryEvent(output[1]); + await withProcessorAndSender(config, async (ep, mockEventSender) => { + const event = { + kind: 'feature', + creationDate: 1000, + key: 'flagkey', + user: user, + trackEvents: true, + reason: reason, + }; + ep.enqueue(event); + await ep.flush(); + + expect(mockEventSender.calls.length).toEqual(1); + const output = mockEventSender.calls[0].events; + expect(output.length).toEqual(2); + checkFeatureEvent(output[0], event, false, user); + checkSummaryEvent(output[1]); + }); }); it('filters user in feature event', async () => { const config = { ...defaultConfig, allAttributesPrivate: true, inlineUsersInEvents: true }; - const ep = EventProcessor(platform, config, envId, null, mockEventSender); - const event = { - kind: 'feature', - creationDate: 1000, - key: 'flagkey', - user: user, - trackEvents: true, - }; - ep.enqueue(event); - await ep.flush(); - - expect(mockEventSender.calls.length).toEqual(1); - const output = mockEventSender.calls[0].events; - expect(output.length).toEqual(2); - checkFeatureEvent(output[0], event, false, filteredUser); - checkSummaryEvent(output[1]); + await withProcessorAndSender(config, async (ep, mockEventSender) => { + const event = { + kind: 'feature', + creationDate: 1000, + key: 'flagkey', + user: user, + trackEvents: true, + }; + ep.enqueue(event); + await ep.flush(); + + expect(mockEventSender.calls.length).toEqual(1); + const output = mockEventSender.calls[0].events; + expect(output.length).toEqual(2); + checkFeatureEvent(output[0], event, false, filteredUser); + checkSummaryEvent(output[1]); + }); }); it('sets event kind to debug if event is temporarily in debug mode', async () => { - const ep = EventProcessor(platform, defaultConfig, envId, null, mockEventSender); - const 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.enqueue(e); - await ep.flush(); - - expect(mockEventSender.calls.length).toEqual(1); - const output = mockEventSender.calls[0].events; - expect(output.length).toEqual(2); - checkFeatureEvent(output[0], e, true, user); - checkSummaryEvent(output[1]); + await withProcessorAndSender(defaultConfig, async (ep, mockEventSender) => { + const 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.enqueue(e); + await ep.flush(); + + expect(mockEventSender.calls.length).toEqual(1); + const output = mockEventSender.calls[0].events; + expect(output.length).toEqual(2); + checkFeatureEvent(output[0], e, true, user); + checkSummaryEvent(output[1]); + }); }); it('can both track and debug an event', async () => { - const ep = EventProcessor(platform, defaultConfig, envId, null, mockEventSender); - 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.enqueue(e); - await ep.flush(); - - expect(mockEventSender.calls.length).toEqual(1); - const output = mockEventSender.calls[0].events; - expect(output.length).toEqual(3); - checkFeatureEvent(output[0], e, false); - checkFeatureEvent(output[1], e, true, user); - checkSummaryEvent(output[2]); + await withProcessorAndSender(defaultConfig, async (ep, mockEventSender) => { + 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.enqueue(e); + await ep.flush(); + + expect(mockEventSender.calls.length).toEqual(1); + const output = mockEventSender.calls[0].events; + expect(output.length).toEqual(3); + checkFeatureEvent(output[0], e, false); + checkFeatureEvent(output[1], e, true, user); + checkSummaryEvent(output[2]); + }); }); it('expires debug mode based on client time if client time is later than server time', async () => { - const ep = EventProcessor(platform, defaultConfig, envId, null, mockEventSender); - - // Pick a server time that is somewhat behind the client time - const serverTime = new Date().getTime() - 20000; - mockEventSender.serverTime = serverTime; - - // Send and flush an event we don't care about, just to set the last server time - ep.enqueue({ kind: 'identify', user: { key: 'otherUser' } }); - await ep.flush(); - - // 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. - 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.enqueue(e); - - // Should get a summary event only, not a full feature event - await ep.flush(); - expect(mockEventSender.calls.length).toEqual(2); - const output = mockEventSender.calls[1].events; - expect(output.length).toEqual(1); - checkSummaryEvent(output[0]); + await withProcessorAndSender(defaultConfig, async (ep, mockEventSender) => { + // Pick a server time that is somewhat behind the client time + const serverTime = new Date().getTime() - 20000; + mockEventSender.setServerTime(serverTime); + + // Send and flush an event we don't care about, just to set the last server time + ep.enqueue({ kind: 'identify', user: { key: 'otherUser' } }); + await ep.flush(); + + // 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. + 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.enqueue(e); + + // Should get a summary event only, not a full feature event + await ep.flush(); + expect(mockEventSender.calls.length).toEqual(2); + const output = mockEventSender.calls[1].events; + expect(output.length).toEqual(1); + checkSummaryEvent(output[0]); + }); }); it('expires debug mode based on server time if server time is later than client time', async () => { - const ep = EventProcessor(platform, defaultConfig, envId, null, mockEventSender); - - // Pick a server time that is somewhat ahead of the client time - const serverTime = new Date().getTime() + 20000; - mockEventSender.serverTime = serverTime; - - // Send and flush an event we don't care about, just to set the last server time - ep.enqueue({ kind: 'identify', user: { key: 'otherUser' } }); - await ep.flush(); - - // 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. - 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.enqueue(e); - - // Should get a summary event only, not a full feature event - await ep.flush(); - expect(mockEventSender.calls.length).toEqual(2); - const output = mockEventSender.calls[1].events; - expect(output.length).toEqual(1); - checkSummaryEvent(output[0]); - }); - - it('summarizes nontracked events', async () => { - const ep = EventProcessor(platform, defaultConfig, envId, null, mockEventSender); - function makeEvent(key, date, version, variation, value, defaultVal) { - return { + await withProcessorAndSender(defaultConfig, async (ep, mockEventSender) => { + // Pick a server time that is somewhat ahead of the client time + const serverTime = new Date().getTime() + 20000; + mockEventSender.setServerTime(serverTime); + + // Send and flush an event we don't care about, just to set the last server time + ep.enqueue({ kind: 'identify', user: { key: 'otherUser' } }); + await ep.flush(); + + // 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. + const debugUntil = serverTime - 1000; + const e = { kind: 'feature', - creationDate: date, + creationDate: 1000, user: user, - key: key, - version: version, - variation: variation, - value: value, - default: defaultVal, + key: 'flagkey', + version: 11, + variation: 1, + value: 'value', trackEvents: false, + debugEventsUntilDate: debugUntil, }; - } - const e1 = makeEvent('flagkey1', 1000, 11, 1, 'value1', 'default1'); - const e2 = makeEvent('flagkey2', 2000, 22, 1, 'value2', 'default2'); - ep.enqueue(e1); - ep.enqueue(e2); - await ep.flush(); - - expect(mockEventSender.calls.length).toEqual(1); - const output = mockEventSender.calls[0].events; - expect(output.length).toEqual(1); - const se = output[0]; - checkSummaryEvent(se); - expect(se.startDate).toEqual(1000); - expect(se.endDate).toEqual(2000); - expect(se.features).toEqual({ - flagkey1: { - default: 'default1', - counters: [{ version: 11, variation: 1, value: 'value1', count: 1 }], - }, - flagkey2: { - default: 'default2', - counters: [{ version: 22, variation: 1, value: 'value2', count: 1 }], - }, + ep.enqueue(e); + + // Should get a summary event only, not a full feature event + await ep.flush(); + expect(mockEventSender.calls.length).toEqual(2); + const output = mockEventSender.calls[1].events; + expect(output.length).toEqual(1); + checkSummaryEvent(output[0]); + }); + }); + + it('summarizes nontracked events', async () => { + await withProcessorAndSender(defaultConfig, async (ep, mockEventSender) => { + function makeEvent(key, date, version, variation, value, defaultVal) { + return { + kind: 'feature', + creationDate: date, + user: user, + key: key, + version: version, + variation: variation, + value: value, + default: defaultVal, + trackEvents: false, + }; + } + const e1 = makeEvent('flagkey1', 1000, 11, 1, 'value1', 'default1'); + const e2 = makeEvent('flagkey2', 2000, 22, 1, 'value2', 'default2'); + ep.enqueue(e1); + ep.enqueue(e2); + await ep.flush(); + + expect(mockEventSender.calls.length).toEqual(1); + const output = mockEventSender.calls[0].events; + expect(output.length).toEqual(1); + const se = output[0]; + checkSummaryEvent(se); + expect(se.startDate).toEqual(1000); + expect(se.endDate).toEqual(2000); + expect(se.features).toEqual({ + flagkey1: { + default: 'default1', + counters: [{ version: 11, variation: 1, value: 'value1', count: 1 }], + }, + flagkey2: { + default: 'default2', + counters: [{ version: 22, variation: 1, value: 'value2', count: 1 }], + }, + }); }); }); it('queues custom event', async () => { - const ep = EventProcessor(platform, defaultConfig, envId, null, mockEventSender); - const e = { - kind: 'custom', - creationDate: 1000, - user: user, - key: 'eventkey', - data: { thing: 'stuff' }, - metricValue: 1.5, - }; - ep.enqueue(e); - await ep.flush(); + await withProcessorAndSender(defaultConfig, async (ep, mockEventSender) => { + const e = { + kind: 'custom', + creationDate: 1000, + user: user, + key: 'eventkey', + data: { thing: 'stuff' }, + metricValue: 1.5, + }; + ep.enqueue(e); + await ep.flush(); - expect(mockEventSender.calls.length).toEqual(1); - const output = mockEventSender.calls[0].events; - expect(output.length).toEqual(1); - checkCustomEvent(output[0], e); + expect(mockEventSender.calls.length).toEqual(1); + const output = mockEventSender.calls[0].events; + expect(output.length).toEqual(1); + checkCustomEvent(output[0], e); + }); }); it('can include inline user in custom event', async () => { const config = { ...defaultConfig, inlineUsersInEvents: true }; - const ep = EventProcessor(platform, config, envId, null, mockEventSender); - const e = { - kind: 'custom', - creationDate: 1000, - user: user, - key: 'eventkey', - data: { thing: 'stuff' }, - }; - ep.enqueue(e); - await ep.flush(); + await withProcessorAndSender(config, async (ep, mockEventSender) => { + const e = { + kind: 'custom', + creationDate: 1000, + user: user, + key: 'eventkey', + data: { thing: 'stuff' }, + }; + ep.enqueue(e); + await ep.flush(); - expect(mockEventSender.calls.length).toEqual(1); - const output = mockEventSender.calls[0].events; - expect(output.length).toEqual(1); - checkCustomEvent(output[0], e, user); + expect(mockEventSender.calls.length).toEqual(1); + const output = mockEventSender.calls[0].events; + expect(output.length).toEqual(1); + checkCustomEvent(output[0], e, user); + }); }); it('filters user in custom event', async () => { const config = { ...defaultConfig, allAttributesPrivate: true, inlineUsersInEvents: true }; - const ep = EventProcessor(platform, config, envId, null, mockEventSender); - const e = { - kind: 'custom', - creationDate: 1000, - user: user, - key: 'eventkey', - data: { thing: 'stuff' }, - }; - ep.enqueue(e); - await ep.flush(); + await withProcessorAndSender(config, async (ep, mockEventSender) => { + const e = { + kind: 'custom', + creationDate: 1000, + user: user, + key: 'eventkey', + data: { thing: 'stuff' }, + }; + ep.enqueue(e); + await ep.flush(); - expect(mockEventSender.calls.length).toEqual(1); - const output = mockEventSender.calls[0].events; - expect(output.length).toEqual(1); - checkCustomEvent(output[0], e, filteredUser); + expect(mockEventSender.calls.length).toEqual(1); + const output = mockEventSender.calls[0].events; + expect(output.length).toEqual(1); + checkCustomEvent(output[0], e, filteredUser); + }); }); it('enforces event capacity', async () => { const config = { ...defaultConfig, eventCapacity: 1, logger: stubPlatform.logger() }; - const ep = EventProcessor(platform, config, envId, null, mockEventSender); const e0 = { kind: 'custom', creationDate: 1000, user: user, key: 'key0' }; const e1 = { kind: 'custom', creationDate: 1001, user: user, key: 'key1' }; const e2 = { kind: 'custom', creationDate: 1002, user: user, key: 'key2' }; - ep.enqueue(e0); - ep.enqueue(e1); - ep.enqueue(e2); - await ep.flush(); - - expect(mockEventSender.calls.length).toEqual(1); - const output = mockEventSender.calls[0].events; - expect(output.length).toEqual(1); - checkCustomEvent(output[0], e0); - - expect(config.logger.output.warn).toEqual([messages.eventCapacityExceeded()]); // warning is not repeated for e2 + await withProcessorAndSender(config, async (ep, mockEventSender) => { + ep.enqueue(e0); + ep.enqueue(e1); + ep.enqueue(e2); + await ep.flush(); + + expect(mockEventSender.calls.length).toEqual(1); + const output = mockEventSender.calls[0].events; + expect(output.length).toEqual(1); + checkCustomEvent(output[0], e0); + + expect(config.logger.output.warn).toEqual([messages.eventCapacityExceeded()]); // warning is not repeated for e2 + }); }); it('sends nothing if there are no events to flush', async () => { - const ep = EventProcessor(platform, defaultConfig, envId, null, mockEventSender); - await ep.flush(); - expect(mockEventSender.calls.length).toEqual(0); + await withProcessorAndSender(defaultConfig, async (ep, mockEventSender) => { + await ep.flush(); + expect(mockEventSender.calls.length).toEqual(0); + }); }); async function verifyUnrecoverableHttpError(status) { - const ep = EventProcessor(platform, defaultConfig, envId, null, mockEventSender); - const e = { kind: 'identify', creationDate: 1000, user: user }; - ep.enqueue(e); - mockEventSender.status = status; - await ep.flush(); + await withProcessorAndSender(defaultConfig, async (ep, mockEventSender) => { + const e = { kind: 'identify', creationDate: 1000, user: user }; + ep.enqueue(e); + mockEventSender.setStatus(status); + await ep.flush(); - expect(mockEventSender.calls.length).toEqual(1); - ep.enqueue(e); - await ep.flush(); + expect(mockEventSender.calls.length).toEqual(1); + ep.enqueue(e); + await ep.flush(); - expect(mockEventSender.calls.length).toEqual(1); // still the one from our first flush + expect(mockEventSender.calls.length).toEqual(1); // still the one from our first flush + }); } async function verifyRecoverableHttpError(status) { - const ep = EventProcessor(platform, defaultConfig, envId, null, mockEventSender); - const e = { kind: 'identify', creationDate: 1000, user: user }; - ep.enqueue(e); - mockEventSender.status = status; - await ep.flush(); + await withProcessorAndSender(defaultConfig, async (ep, mockEventSender) => { + const e = { kind: 'identify', creationDate: 1000, user: user }; + ep.enqueue(e); + mockEventSender.setStatus(status); + await ep.flush(); - expect(mockEventSender.calls.length).toEqual(1); - ep.enqueue(e); - await ep.flush(); + expect(mockEventSender.calls.length).toEqual(1); + ep.enqueue(e); + await ep.flush(); - expect(mockEventSender.calls.length).toEqual(2); + expect(mockEventSender.calls.length).toEqual(2); + }); } it('stops sending events after a 401 error', () => verifyUnrecoverableHttpError(401)); diff --git a/src/__tests__/EventSender-test.js b/src/__tests__/EventSender-test.js index 3f91933..1a9aec2 100644 --- a/src/__tests__/EventSender-test.js +++ b/src/__tests__/EventSender-test.js @@ -1,29 +1,27 @@ +import EventSender from '../EventSender'; +import * as utils from '../utils'; + import * as base64 from 'base64-js'; +import { respond, networkError } from './mockHttp'; import * as stubPlatform from './stubPlatform'; -import { errorResponse, makeDefaultServer } from './testUtils'; -import EventSender from '../EventSender'; -import * as utils from '../utils'; + +// These tests verify that EventSender executes the expected HTTP requests to deliver events. Since +// the js-sdk-common package uses an abstraction of HTTP requests, these tests do not use HTTP but +// rather use a test implementation of our HTTP abstraction; the individual platform-specific SDKs +// are responsible for verifying that their own implementations of the same HTTP abstraction work +// correctly with real networking. describe('EventSender', () => { - const platform = stubPlatform.defaults(); - const platformWithoutCors = { ...platform, httpAllowsPost: () => false }; - let server; - const eventsUrl = '/fake-url'; + let platform; + let platformWithoutCors; const envId = 'env'; beforeEach(() => { - server = makeDefaultServer(); - }); - - afterEach(() => { - server.restore(); + platform = stubPlatform.defaults(); + platformWithoutCors = { ...platform, httpAllowsPost: () => false }; }); - function lastRequest() { - return server.requests[server.requests.length - 1]; - } - function fakeImageCreator() { const ret = function(url) { ret.urls.push(url); @@ -43,8 +41,8 @@ describe('EventSender', () => { return decodeURIComponent(escape(decodedStr)); } - function decodeOutputFromUrl(url) { - const prefix = eventsUrl + '/a/' + envId + '.gif?d='; + function decodeOutputFromUrl(url, baseUrl) { + const prefix = baseUrl + '/a/' + envId + '.gif?d='; if (!url.startsWith(prefix)) { throw 'URL "' + url + '" did not have expected prefix "' + prefix + '"'; } @@ -53,8 +51,9 @@ describe('EventSender', () => { describe('using image endpoint when CORS is not available', () => { it('should encode events in a single chunk if they fit', async () => { + const server = platform.testing.http.newServer(); const imageCreator = fakeImageCreator(); - const sender = EventSender(platformWithoutCors, eventsUrl, envId, imageCreator); + const sender = EventSender(platformWithoutCors, server.url, envId, imageCreator); const event1 = { kind: 'identify', key: 'userKey1' }; const event2 = { kind: 'identify', key: 'userKey2' }; const events = [event1, event2]; @@ -63,12 +62,15 @@ describe('EventSender', () => { const urls = imageCreator.urls; expect(urls.length).toEqual(1); - expect(decodeOutputFromUrl(urls[0])).toEqual(events); + expect(decodeOutputFromUrl(urls[0], server.url)).toEqual(events); + + expect(server.requests.length()).toEqual(0); }); it('should send events in multiple chunks if necessary', async () => { + const server = platform.testing.http.newServer(); const imageCreator = fakeImageCreator(); - const sender = EventSender(platformWithoutCors, eventsUrl, envId, imageCreator); + const sender = EventSender(platformWithoutCors, server.url, envId, imageCreator); const events = []; for (let i = 0; i < 80; i++) { events.push({ kind: 'identify', key: 'thisIsALongUserKey' + i }); @@ -78,94 +80,117 @@ describe('EventSender', () => { const urls = imageCreator.urls; expect(urls.length).toEqual(3); - expect(decodeOutputFromUrl(urls[0])).toEqual(events.slice(0, 31)); - expect(decodeOutputFromUrl(urls[1])).toEqual(events.slice(31, 62)); - expect(decodeOutputFromUrl(urls[2])).toEqual(events.slice(62, 80)); + expect(decodeOutputFromUrl(urls[0], server.url)).toEqual(events.slice(0, 31)); + expect(decodeOutputFromUrl(urls[1], server.url)).toEqual(events.slice(31, 61)); + expect(decodeOutputFromUrl(urls[2], server.url)).toEqual(events.slice(61, 80)); + + expect(server.requests.length()).toEqual(0); }); }); describe('using POST when CORS is available', () => { it('should send all events in request body', async () => { - const sender = EventSender(platform, eventsUrl, envId); + const server = platform.testing.http.newServer(); + server.byDefault(respond(202)); + const sender = EventSender(platform, server.url, envId); const events = []; for (let i = 0; i < 80; i++) { events.push({ kind: 'identify', key: 'thisIsALongUserKey' + i }); } await sender.sendEvents(events, false); - const r = lastRequest(); - expect(r.url).toEqual(eventsUrl + '/events/bulk/' + envId); - expect(r.method).toEqual('POST'); - expect(JSON.parse(r.requestBody)).toEqual(events); + + const r = await server.nextRequest(); + expect(r.path).toEqual('/events/bulk/' + envId); + expect(r.method).toEqual('post'); + expect(JSON.parse(r.body)).toEqual(events); }); it('should send custom user-agent header', async () => { - const sender = EventSender(platform, eventsUrl, envId); + const server = platform.testing.http.newServer(); + server.byDefault(respond(202)); + const sender = EventSender(platform, server.url, envId); const event = { kind: 'identify', key: 'userKey' }; await sender.sendEvents([event], false); - expect(lastRequest().requestHeaders['X-LaunchDarkly-User-Agent']).toEqual(utils.getLDUserAgentString(platform)); + + const r = await server.nextRequest(); + expect(r.headers['x-launchdarkly-user-agent']).toEqual(utils.getLDUserAgentString(platform)); }); const retryableStatuses = [400, 408, 429, 500, 503]; for (const i in retryableStatuses) { const status = retryableStatuses[i]; it('should retry on error ' + status, async () => { + const server = platform.testing.http.newServer(); let n = 0; - server.respondWith(req => { + server.byDefault((req, res) => { n++; - req.respond(n >= 2 ? 200 : status); + respond(n >= 2 ? 200 : status)(req, res); }); - const sender = EventSender(platform, eventsUrl, envId); + const sender = EventSender(platform, server.url, envId); const event = { kind: 'false', key: 'userKey' }; await sender.sendEvents([event], false); - expect(server.requests.length).toEqual(2); - expect(JSON.parse(server.requests[1].requestBody)).toEqual([event]); + + expect(server.requests.length()).toEqual(2); + await server.nextRequest(); + const r1 = await server.nextRequest(); + expect(JSON.parse(r1.body)).toEqual([event]); }); } it('should not retry more than once', async () => { + const server = platform.testing.http.newServer(); let n = 0; - server.respondWith(req => { + server.byDefault((req, res) => { n++; - req.respond(n >= 3 ? 200 : 503); + respond(n >= 3 ? 200 : 503)(req, res); }); - const sender = EventSender(platform, eventsUrl, envId); + const sender = EventSender(platform, server.url, envId); const event = { kind: 'false', key: 'userKey' }; await sender.sendEvents([event], false); - expect(server.requests.length).toEqual(2); + + expect(server.requests.length()).toEqual(2); }); it('should not retry on error 401', async () => { - server.respondWith(errorResponse(401)); - const sender = EventSender(platform, eventsUrl, envId); + const server = platform.testing.http.newServer(); + server.byDefault(respond(401)); + const sender = EventSender(platform, server.url, envId); const event = { kind: 'false', key: 'userKey' }; await sender.sendEvents([event], false); - expect(server.requests.length).toEqual(1); + + expect(server.requests.length()).toEqual(1); }); it('should retry on I/O error', async () => { + const server = platform.testing.http.newServer(); let n = 0; - server.respondWith(req => { + server.byDefault((req, res) => { n++; if (n >= 2) { - req.respond(200); + respond(200)(req, res); } else { - req.error(); + networkError()(req, res); } }); - const sender = EventSender(platform, eventsUrl, envId); + const sender = EventSender(platform, server.url, envId); const event = { kind: 'false', key: 'userKey' }; await sender.sendEvents([event], false); - expect(server.requests.length).toEqual(2); - expect(JSON.parse(server.requests[1].requestBody)).toEqual([event]); + + expect(server.requests.length()).toEqual(2); + await server.nextRequest(); + const r1 = await server.nextRequest(); + expect(JSON.parse(r1.body)).toEqual([event]); }); }); describe('When HTTP requests are not available at all', () => { it('should silently discard events', async () => { - const sender = EventSender(stubPlatform.withoutHttp(), eventsUrl, envId); + const server = platform.testing.http.newServer(); + const sender = EventSender(stubPlatform.withoutHttp(), server.url, envId); const event = { kind: 'false', key: 'userKey' }; await sender.sendEvents([event], false); - expect(server.requests.length).toEqual(0); + + expect(server.requests.length()).toEqual(0); }); }); }); diff --git a/src/__tests__/EventSummarizer-test.js b/src/__tests__/EventSummarizer-test.js index 98d323f..be5e065 100644 --- a/src/__tests__/EventSummarizer-test.js +++ b/src/__tests__/EventSummarizer-test.js @@ -1,5 +1,7 @@ import EventSummarizer from '../EventSummarizer'; +// These tests cover only the logic for counting feature requests in summary data. + describe('EventSummarizer', () => { const user = { key: 'key1' }; diff --git a/src/__tests__/LDClient-events-test.js b/src/__tests__/LDClient-events-test.js index f8de691..026f938 100644 --- a/src/__tests__/LDClient-events-test.js +++ b/src/__tests__/LDClient-events-test.js @@ -1,114 +1,136 @@ +import * as messages from '../messages'; + +import { withCloseable } from 'launchdarkly-js-test-helpers'; + +import { respondJson } from './mockHttp'; import * as stubPlatform from './stubPlatform'; -import { jsonResponse, makeBootstrap, makeDefaultServer, numericUser, stringifiedNumericUser } from './testUtils'; +import { makeBootstrap, numericUser, stringifiedNumericUser } from './testUtils'; + +// These tests verify that the client generates the appropriate analytics events for various scenarios. +// We use a mock event processor component so that the events are not sent anywhere. +// +// We also use a mock HTTP service in a few tests-- not to simulate an event-recorder instance, since +// we're not testing event delivery here, but to simulate the polling service in cases where the test +// logic involves a flag request. In all other cases we just start the client with bootstrap data. -describe('LDClient', () => { +describe('LDClient events', () => { const envName = 'UNKNOWN_ENVIRONMENT_ID'; const user = { key: 'user' }; const fakeUrl = 'http://fake'; let platform; - let server; beforeEach(() => { - server = makeDefaultServer(); platform = stubPlatform.defaults(); platform.testing.setCurrentUrl(fakeUrl); }); - afterEach(() => { - server.restore(); - }); - - describe('event generation', () => { - function stubEventProcessor() { - const ep = { events: [] }; - ep.start = function() {}; - ep.flush = function() {}; - ep.stop = function() {}; - ep.enqueue = function(e) { - ep.events.push(e); - }; - return ep; - } - - function expectIdentifyEvent(e, user) { - expect(e.kind).toEqual('identify'); - expect(e.user).toEqual(user); - } - - function expectFeatureEvent(e, key, value, variation, version, defaultVal, trackEvents, debugEventsUntilDate) { - expect(e.kind).toEqual('feature'); - expect(e.key).toEqual(key); - expect(e.value).toEqual(value); - expect(e.variation).toEqual(variation); - expect(e.version).toEqual(version); - expect(e.default).toEqual(defaultVal); - expect(e.trackEvents).toEqual(trackEvents); - expect(e.debugEventsUntilDate).toEqual(debugEventsUntilDate); - } - - it('sends an identify event at startup', async () => { - const ep = stubEventProcessor(); - const client = platform.testing.makeClient(envName, user, { eventProcessor: ep }); + function stubEventProcessor() { + const ep = { events: [] }; + ep.start = function() {}; + ep.flush = function() {}; + ep.stop = function() {}; + ep.enqueue = function(e) { + ep.events.push(e); + }; + return ep; + } + + async function withServer(asyncCallback) { + const server = platform.testing.http.newServer(); + server.byDefault(respondJson({})); + return await withCloseable(server, asyncCallback); + } + + async function withClientAndEventProcessor(user, extraConfig, asyncCallback) { + const ep = stubEventProcessor(); + const config = Object.assign({ baseUrl: 'shouldnt-use-this', bootstrap: {}, eventProcessor: ep }, extraConfig); + const client = platform.testing.makeClient(envName, user, config); + return await withCloseable(client, async () => await asyncCallback(client, ep)); + } + + function expectIdentifyEvent(e, user) { + expect(e.kind).toEqual('identify'); + expect(e.user).toEqual(user); + } + + function expectFeatureEvent(e, key, value, variation, version, defaultVal, trackEvents, debugEventsUntilDate) { + expect(e.kind).toEqual('feature'); + expect(e.key).toEqual(key); + expect(e.value).toEqual(value); + expect(e.variation).toEqual(variation); + expect(e.version).toEqual(version); + expect(e.default).toEqual(defaultVal); + expect(e.trackEvents).toEqual(trackEvents); + expect(e.debugEventsUntilDate).toEqual(debugEventsUntilDate); + } + + it('sends an identify event at startup', async () => { + await withClientAndEventProcessor(user, {}, async (client, ep) => { await client.waitForInitialization(); expect(ep.events.length).toEqual(1); expectIdentifyEvent(ep.events[0], user); }); + }); - it('stringifies user attributes in the identify event at startup', async () => { - // This just verifies that the event is being sent with the sanitized user, not the user that was passed in - const ep = stubEventProcessor(); - const client = platform.testing.makeClient(envName, numericUser, { eventProcessor: ep }); + it('stringifies user attributes in the identify event at startup', async () => { + // This just verifies that the event is being sent with the sanitized user, not the user that was passed in + await withClientAndEventProcessor(numericUser, {}, async (client, ep) => { await client.waitForInitialization(); expect(ep.events.length).toEqual(1); expectIdentifyEvent(ep.events[0], stringifiedNumericUser); }); + }); - it('sends an identify event when identify() is called', async () => { - const ep = stubEventProcessor(); - const client = platform.testing.makeClient(envName, user, { eventProcessor: ep }); - const user1 = { key: 'user1' }; - await client.waitForInitialization(); + it('sends an identify event when identify() is called', async () => { + // need a server because it'll do a polling request when we call identify + await withServer(async server => { + await withClientAndEventProcessor(user, { baseUrl: server.url }, async (client, ep) => { + const user1 = { key: 'user1' }; + await client.waitForInitialization(); - expect(ep.events.length).toEqual(1); - await client.identify(user1); - server.respond(); + expect(ep.events.length).toEqual(1); + await client.identify(user1); - expect(ep.events.length).toEqual(2); - expectIdentifyEvent(ep.events[1], user1); + expect(ep.events.length).toEqual(2); + expectIdentifyEvent(ep.events[1], user1); + }); }); + }); - it('stringifies user attributes in the identify event when identify() is called', async () => { - // This just verifies that the event is being sent with the sanitized user, not the user that was passed in - const ep = stubEventProcessor(); - const client = platform.testing.makeClient(envName, user, { eventProcessor: ep }); - await client.waitForInitialization(); + it('stringifies user attributes in the identify event when identify() is called', async () => { + // This just verifies that the event is being sent with the sanitized user, not the user that was passed in + await withServer(async server => { + await withClientAndEventProcessor(user, { baseUrl: server.url }, async (client, ep) => { + await client.waitForInitialization(); - expect(ep.events.length).toEqual(1); - await client.identify(numericUser); + expect(ep.events.length).toEqual(1); + await client.identify(numericUser); - expect(ep.events.length).toEqual(2); - expectIdentifyEvent(ep.events[1], stringifiedNumericUser); + expect(ep.events.length).toEqual(2); + expectIdentifyEvent(ep.events[1], stringifiedNumericUser); + }); }); + }); - it('does not send an identify event if doNotTrack is set', async () => { - platform.testing.setDoNotTrack(true); - const ep = stubEventProcessor(); - const client = platform.testing.makeClient(envName, user, { eventProcessor: ep }); - const user1 = { key: 'user1' }; + it('does not send an identify event if doNotTrack is set', async () => { + platform.testing.setDoNotTrack(true); + await withServer(async server => { + await withClientAndEventProcessor(user, { baseUrl: server.url }, async (client, ep) => { + const user1 = { key: 'user1' }; - await client.waitForInitialization(); - await client.identify(user1); + await client.waitForInitialization(); + await client.identify(user1); - expect(ep.events.length).toEqual(0); + expect(ep.events.length).toEqual(0); + }); }); + }); - it('sends a feature event for variation()', async () => { - const initFlags = makeBootstrap({ foo: { value: 'a', variation: 1, version: 2, flagVersion: 2000 } }); - const ep = stubEventProcessor(); - const client = platform.testing.makeClient(envName, user, { eventProcessor: ep, bootstrap: initFlags }); - + it('sends a feature event for variation()', async () => { + const initData = makeBootstrap({ foo: { value: 'a', variation: 1, version: 2, flagVersion: 2000 } }); + await withClientAndEventProcessor(user, { bootstrap: initData }, async (client, ep) => { await client.waitForInitialization(); client.variation('foo', 'x'); @@ -117,14 +139,13 @@ describe('LDClient', () => { expectIdentifyEvent(ep.events[0], user); expectFeatureEvent(ep.events[1], 'foo', 'a', 1, 2000, 'x'); }); + }); - it('sends a feature event with reason for variationDetail()', async () => { - const initFlags = makeBootstrap({ - foo: { value: 'a', variation: 1, version: 2, flagVersion: 2000, reason: { kind: 'OFF' } }, - }); - const ep = stubEventProcessor(); - const client = platform.testing.makeClient(envName, user, { eventProcessor: ep, bootstrap: initFlags }); - + it('sends a feature event with reason for variationDetail()', async () => { + const initData = makeBootstrap({ + foo: { value: 'a', variation: 1, version: 2, flagVersion: 2000, reason: { kind: 'OFF' } }, + }); + await withClientAndEventProcessor(user, { bootstrap: initData }, async (client, ep) => { await client.waitForInitialization(); client.variationDetail('foo', 'x'); @@ -133,14 +154,13 @@ describe('LDClient', () => { expectFeatureEvent(ep.events[1], 'foo', 'a', 1, 2000, 'x'); expect(ep.events[1].reason).toEqual({ kind: 'OFF' }); }); + }); - it('does not include reason in event for variation() even if reason is available', async () => { - const initFlags = makeBootstrap({ - foo: { value: 'a', variation: 1, version: 2, flagVersion: 2000, reason: { kind: 'OFF' } }, - }); - const ep = stubEventProcessor(); - const client = platform.testing.makeClient(envName, user, { eventProcessor: ep, bootstrap: initFlags }); - + it('does not include reason in event for variation() even if reason is available', async () => { + const initData = makeBootstrap({ + foo: { value: 'a', variation: 1, version: 2, flagVersion: 2000, reason: { kind: 'OFF' } }, + }); + await withClientAndEventProcessor(user, { bootstrap: initData }, async (client, ep) => { await client.waitForInitialization(); client.variation('foo', 'x'); @@ -149,14 +169,13 @@ describe('LDClient', () => { expectFeatureEvent(ep.events[1], 'foo', 'a', 1, 2000, 'x'); expect(ep.events[1].reason).toBe(undefined); }); + }); - it('sends a feature event with reason for variation() if trackReason is set', async () => { - const initFlags = makeBootstrap({ - foo: { value: 'a', variation: 1, version: 2, flagVersion: 2000, reason: { kind: 'OFF' }, trackReason: true }, - }); - const ep = stubEventProcessor(); - const client = platform.testing.makeClient(envName, user, { eventProcessor: ep, bootstrap: initFlags }); - + it('sends a feature event with reason for variation() if trackReason is set', async () => { + const initData = makeBootstrap({ + foo: { value: 'a', variation: 1, version: 2, flagVersion: 2000, reason: { kind: 'OFF' }, trackReason: true }, + }); + await withClientAndEventProcessor(user, { bootstrap: initData }, async (client, ep) => { await client.waitForInitialization(); client.variation('foo', 'x'); @@ -165,72 +184,72 @@ describe('LDClient', () => { expectFeatureEvent(ep.events[1], 'foo', 'a', 1, 2000, 'x'); expect(ep.events[1].reason).toEqual({ kind: 'OFF' }); }); + }); - it('sends a feature event on receiving a new flag value', async () => { - const ep = stubEventProcessor(); - const oldFlags = { foo: { value: 'a', variation: 1, version: 2, flagVersion: 2000 } }; - const newFlags = { foo: { value: 'b', variation: 2, version: 3, flagVersion: 2001 } }; - - server.respondWith(jsonResponse(oldFlags)); - - const client = platform.testing.makeClient(envName, user, { eventProcessor: ep }); - await client.waitForInitialization(); - - const user1 = { key: 'user1' }; - server.respondWith(jsonResponse(newFlags)); - await client.identify(user1); - - expect(ep.events.length).toEqual(3); - expectIdentifyEvent(ep.events[0], user); - expectIdentifyEvent(ep.events[1], user1); - expectFeatureEvent(ep.events[2], 'foo', 'b', 2, 2001); + it('sends a feature event on receiving a new flag value', async () => { + const oldFlags = { foo: { value: 'a', variation: 1, version: 2, flagVersion: 2000 } }; + const newFlags = { foo: { value: 'b', variation: 2, version: 3, flagVersion: 2001 } }; + const initData = makeBootstrap(oldFlags); + await withServer(async server => { + server.byDefault(respondJson(newFlags)); + await withClientAndEventProcessor(user, { baseUrl: server.url, bootstrap: initData }, async (client, ep) => { + await client.waitForInitialization(); + + const user1 = { key: 'user1' }; + await client.identify(user1); + + expect(ep.events.length).toEqual(3); + expectIdentifyEvent(ep.events[0], user); + expectIdentifyEvent(ep.events[1], user1); + expectFeatureEvent(ep.events[2], 'foo', 'b', 2, 2001); + }); }); + }); - it('does not send a feature event for a new flag value if sendEventsOnlyForVariation is set', async () => { - const ep = stubEventProcessor(); - const oldFlags = { foo: { value: 'a', variation: 1, version: 2, flagVersion: 2000 } }; - const newFlags = { foo: { value: 'b', variation: 2, version: 3, flagVersion: 2001 } }; - - server.respondWith(jsonResponse(oldFlags)); - - const client = platform.testing.makeClient(envName, user, { - eventProcessor: ep, - sendEventsOnlyForVariation: true, + it('does not send a feature event for a new flag value if sendEventsOnlyForVariation is set', async () => { + const oldFlags = { foo: { value: 'a', variation: 1, version: 2, flagVersion: 2000 } }; + const newFlags = { foo: { value: 'b', variation: 2, version: 3, flagVersion: 2001 } }; + const initData = makeBootstrap(oldFlags); + await withServer(async server => { + server.byDefault(respondJson(newFlags)); + const extraConfig = { sendEventsOnlyForVariation: true, baseUrl: server.url, bootstrap: initData }; + await withClientAndEventProcessor(user, extraConfig, async (client, ep) => { + await client.waitForInitialization(); + + const user1 = { key: 'user1' }; + await client.identify(user1); + + expect(ep.events.length).toEqual(2); + expectIdentifyEvent(ep.events[0], user); + expectIdentifyEvent(ep.events[1], user1); }); - await client.waitForInitialization(); - - const user1 = { key: 'user1' }; - server.respondWith(jsonResponse(newFlags)); - await client.identify(user1); - - expect(ep.events.length).toEqual(2); - expectIdentifyEvent(ep.events[0], user); - expectIdentifyEvent(ep.events[1], user1); }); + }); - it('does not send a feature event for a new flag value if there is a state provider', async () => { - const ep = stubEventProcessor(); - const oldFlags = { foo: { value: 'a', variation: 1, version: 2, flagVersion: 2000 } }; - const newFlags = { foo: { value: 'b', variation: 2, version: 3, flagVersion: 2001 } }; - const sp = stubPlatform.mockStateProvider({ environment: envName, user: user, flags: oldFlags }); - const client = platform.testing.makeClient(envName, user, { eventProcessor: ep, stateProvider: sp }); - - await client.waitForInitialization(); - - sp.emit('update', { flags: newFlags }); + it('does not send a feature event for a new flag value if there is a state provider', async () => { + const oldFlags = { foo: { value: 'a', variation: 1, version: 2, flagVersion: 2000 } }; + const newFlags = { foo: { value: 'b', variation: 2, version: 3, flagVersion: 2001 } }; + const sp = stubPlatform.mockStateProvider({ environment: envName, user: user, flags: oldFlags }); + await withServer(async server => { + server.byDefault(respondJson(newFlags)); + const extraConfig = { stateProvider: sp, baseUrl: server.url }; + await withClientAndEventProcessor(user, extraConfig, async (client, ep) => { + await client.waitForInitialization(); - expect(client.variation('foo')).toEqual('b'); - expect(ep.events.length).toEqual(1); - }); + sp.emit('update', { flags: newFlags }); - it('sends feature events for allFlags()', async () => { - const ep = stubEventProcessor(); - const initFlags = makeBootstrap({ - foo: { value: 'a', variation: 1, version: 2 }, - bar: { value: 'b', variation: 1, version: 3 }, + expect(client.variation('foo')).toEqual('b'); + expect(ep.events.length).toEqual(1); }); - const client = platform.testing.makeClient(envName, user, { eventProcessor: ep, bootstrap: initFlags }); + }); + }); + it('sends feature events for allFlags()', async () => { + const initData = makeBootstrap({ + foo: { value: 'a', variation: 1, version: 2 }, + bar: { value: 'b', variation: 1, version: 3 }, + }); + await withClientAndEventProcessor(user, { bootstrap: initData }, async (client, ep) => { await client.waitForInitialization(); client.allFlags(); @@ -239,31 +258,26 @@ describe('LDClient', () => { expectFeatureEvent(ep.events[1], 'foo', 'a', 1, 2, null); expectFeatureEvent(ep.events[2], 'bar', 'b', 1, 3, null); }); + }); - it('does not send feature events for allFlags() if sendEventsOnlyForVariation is set', async () => { - const ep = stubEventProcessor(); - const initFlags = makeBootstrap({ - foo: { value: 'a', variation: 1, version: 2 }, - bar: { value: 'b', variation: 1, version: 3 }, - }); - const client = platform.testing.makeClient(envName, user, { - eventProcessor: ep, - bootstrap: initFlags, - sendEventsOnlyForVariation: true, - }); - + it('does not send feature events for allFlags() if sendEventsOnlyForVariation is set', async () => { + const initData = makeBootstrap({ + foo: { value: 'a', variation: 1, version: 2 }, + bar: { value: 'b', variation: 1, version: 3 }, + }); + const extraConfig = { sendEventsOnlyForVariation: true, bootstrap: initData }; + await withClientAndEventProcessor(user, extraConfig, async (client, ep) => { await client.waitForInitialization(); client.allFlags(); expect(ep.events.length).toEqual(1); expectIdentifyEvent(ep.events[0], user); }); + }); - it('uses "version" instead of "flagVersion" in event if "flagVersion" is absent', async () => { - const ep = stubEventProcessor(); - const initFlags = makeBootstrap({ foo: { value: 'a', variation: 1, version: 2 } }); - const client = platform.testing.makeClient(envName, user, { eventProcessor: ep, bootstrap: initFlags }); - + it('uses "version" instead of "flagVersion" in event if "flagVersion" is absent', async () => { + const initData = makeBootstrap({ foo: { value: 'a', variation: 1, version: 2 } }); + await withClientAndEventProcessor(user, { bootstrap: initData }, async (client, ep) => { await client.waitForInitialization(); client.variation('foo', 'x'); @@ -271,11 +285,10 @@ describe('LDClient', () => { expectIdentifyEvent(ep.events[0], user); expectFeatureEvent(ep.events[1], 'foo', 'a', 1, 2, 'x'); }); + }); - it('omits event version if flag does not exist', async () => { - const ep = stubEventProcessor(); - const client = platform.testing.makeClient(envName, user, { eventProcessor: ep }); - + it('omits event version if flag does not exist', async () => { + await withClientAndEventProcessor(user, {}, async (client, ep) => { await client.waitForInitialization(); client.variation('foo', 'x'); @@ -283,31 +296,32 @@ describe('LDClient', () => { expectIdentifyEvent(ep.events[0], user); expectFeatureEvent(ep.events[1], 'foo', 'x', null, undefined, 'x'); }); + }); - it('can get metadata for events from bootstrap object', async () => { - const ep = stubEventProcessor(); - const initFlags = makeBootstrap({ - foo: { - value: 'bar', - variation: 1, - version: 2, - trackEvents: true, - debugEventsUntilDate: 1000, - }, + it('can get metadata for events from bootstrap object', async () => { + const initData = makeBootstrap({ + foo: { + value: 'bar', + variation: 1, + version: 2, + trackEvents: true, + debugEventsUntilDate: 1000, + }, + }); + await withClientAndEventProcessor(user, { bootstrap: initData }, async (client, ep) => { + await withCloseable(client, async () => { + await client.waitForInitialization(); + client.variation('foo', 'x'); + + expect(ep.events.length).toEqual(2); + expectIdentifyEvent(ep.events[0], user); + expectFeatureEvent(ep.events[1], 'foo', 'bar', 1, 2, 'x', true, 1000); }); - const client = platform.testing.makeClient(envName, user, { eventProcessor: ep, bootstrap: initFlags }); - - await client.waitForInitialization(); - client.variation('foo', 'x'); - - expect(ep.events.length).toEqual(2); - expectIdentifyEvent(ep.events[0], user); - expectFeatureEvent(ep.events[1], 'foo', 'bar', 1, 2, 'x', true, 1000); }); + }); - it('sends an event for track()', async () => { - const ep = stubEventProcessor(); - const client = platform.testing.makeClient(envName, user, { eventProcessor: ep }); + it('sends an event for track()', async () => { + await withClientAndEventProcessor(user, {}, async (client, ep) => { await client.waitForInitialization(); client.track('eventkey'); @@ -320,10 +334,10 @@ describe('LDClient', () => { expect(trackEvent.data).toEqual(undefined); expect(trackEvent.url).toEqual(fakeUrl); }); + }); - it('sends an event for track() with data', async () => { - const ep = stubEventProcessor(); - const client = platform.testing.makeClient(envName, user, { eventProcessor: ep }); + it('sends an event for track() with data', async () => { + await withClientAndEventProcessor(user, {}, async (client, ep) => { const eventData = { thing: 'stuff' }; await client.waitForInitialization(); client.track('eventkey', eventData); @@ -337,10 +351,10 @@ describe('LDClient', () => { expect(trackEvent.data).toEqual(eventData); expect(trackEvent.url).toEqual(fakeUrl); }); + }); - it('sends an event for track() with metric value', async () => { - const ep = stubEventProcessor(); - const client = platform.testing.makeClient(envName, user, { eventProcessor: ep }); + it('sends an event for track() with metric value', async () => { + await withClientAndEventProcessor(user, {}, async (client, ep) => { const eventData = { thing: 'stuff' }; const metricValue = 1.5; await client.waitForInitialization(); @@ -356,25 +370,53 @@ describe('LDClient', () => { expect(trackEvent.metricValue).toEqual(metricValue); expect(trackEvent.url).toEqual(fakeUrl); }); + }); - it('does not send an event for track() if doNotTrack is set', async () => { - platform.testing.setDoNotTrack(true); - const ep = stubEventProcessor(); - const client = platform.testing.makeClient(envName, user, { eventProcessor: ep }); + it('does not send an event for track() if doNotTrack is set', async () => { + platform.testing.setDoNotTrack(true); + await withClientAndEventProcessor(user, {}, async (client, ep) => { const eventData = { thing: 'stuff' }; await client.waitForInitialization(); client.track('eventkey', eventData); expect(ep.events.length).toEqual(0); }); + }); + + it('does not warn by default when tracking a custom event', async () => { + await withClientAndEventProcessor(user, {}, async client => { + await client.waitForInitialization(); - it('allows stateProvider to take over sending an event', async () => { - const ep = stubEventProcessor(); + client.track('known'); + expect(platform.testing.logger.output.warn).toEqual([]); + }); + }); + + it('emits an error when tracking a non-string custom event', async () => { + await withClientAndEventProcessor(user, {}, async client => { + await client.waitForInitialization(); + + const badCustomEventKeys = [123, [], {}, null, undefined]; + badCustomEventKeys.forEach(key => { + platform.testing.logger.reset(); + client.track(key); + expect(platform.testing.logger.output.error).toEqual([messages.unknownCustomEventKey(key)]); + }); + }); + }); + + it('should warn about missing user on first event', async () => { + await withClientAndEventProcessor(null, {}, async client => { + client.track('eventkey', null); + expect(platform.testing.logger.output.warn).toEqual([messages.eventWithoutUser()]); + }); + }); - const sp = stubPlatform.mockStateProvider({ environment: envName, user: user, flags: {} }); - const divertedEvents = []; - sp.enqueueEvent = event => divertedEvents.push(event); + it('allows stateProvider to take over sending an event', async () => { + const sp = stubPlatform.mockStateProvider({ environment: envName, user: user, flags: {} }); + const divertedEvents = []; + sp.enqueueEvent = event => divertedEvents.push(event); - const client = platform.testing.makeClient(envName, user, { eventProcessor: ep, stateProvider: sp }); + await withClientAndEventProcessor(user, { stateProvider: sp }, async (client, ep) => { await client.waitForInitialization(); client.track('eventkey'); diff --git a/src/__tests__/LDClient-localstorage-test.js b/src/__tests__/LDClient-localstorage-test.js index 4b0da64..d8b9ab4 100644 --- a/src/__tests__/LDClient-localstorage-test.js +++ b/src/__tests__/LDClient-localstorage-test.js @@ -1,139 +1,176 @@ -import * as stubPlatform from './stubPlatform'; -import { asyncSleep, errorResponse, jsonResponse, makeDefaultServer } from './testUtils'; import * as messages from '../messages'; import * as utils from '../utils'; +import { sleepAsync, withCloseable } from 'launchdarkly-js-test-helpers'; + +import { respond, respondJson } from './mockHttp'; +import * as stubPlatform from './stubPlatform'; + +// These tests cover the "bootstrap: 'localstorage'" mode. The actual implementation of local storage +// is provided by the platform-specific SDKs; we use a mock implementation here. + describe('LDClient local storage', () => { const envName = 'UNKNOWN_ENVIRONMENT_ID'; const user = { key: 'user' }; const lsKey = 'ld:' + envName + ':' + utils.btoa(JSON.stringify(user)); - let server; + let platform; beforeEach(() => { - server = makeDefaultServer(); + platform = stubPlatform.defaults(); }); - afterEach(() => { - server.restore(); - }); + async function withServer(asyncCallback) { + const server = platform.testing.http.newServer(); + server.byDefault(respondJson({})); + return await withCloseable(server, asyncCallback); + } + + async function withClient(user, extraConfig, asyncCallback) { + // We specify bootstrap: 'localstorage' for all tests in this file + const config = { baseUrl: 'shouldnt-use-this', bootstrap: 'localstorage', sendEvents: false, ...extraConfig }; + const client = platform.testing.makeClient(envName, user, config); + return await withCloseable(client, asyncCallback); + } describe('bootstrapping from local storage', () => { it('does not try to use local storage if the platform says it is unavailable', async () => { - const platform = stubPlatform.defaults(); platform.localStorage = null; - const client = platform.testing.makeClient(envName, user, { bootstrap: 'localstorage' }); - await client.waitForInitialization(); + await withServer(async server => { + await withClient(user, { baseUrl: server.url }, async client => { + await client.waitForInitialization(); - // should see a flag request to the server right away, as if bootstrap was not specified - expect(server.requests.length).toEqual(1); + // should see a flag request to the server right away, as if bootstrap was not specified + expect(server.requests.length()).toEqual(1); - expect(platform.testing.logger.output.warn).toEqual([messages.localStorageUnavailable()]); + expect(platform.testing.logger.output.warn).toEqual([messages.localStorageUnavailable()]); + }); + }); }); it('uses cached flags if available and requests flags from server after ready', async () => { - const platform = stubPlatform.defaults(); const json = '{"flag-key": 1}'; platform.testing.setLocalStorageImmediately(lsKey, json); - const client = platform.testing.makeClient(envName, user, { bootstrap: 'localstorage' }); - await client.waitForInitialization(); + await withServer(async server => { + // This no-op request handler means that the flags request will simply hang with no + // response, so we can be sure that we're seeing only the initial flags from local storage. + server.byDefault(() => {}); - expect(client.variation('flag-key')).toEqual(1); - expect(server.requests.length).toEqual(1); - }); + await withClient(user, { baseUrl: server.url }, async client => { + await client.waitForInitialization(); - it('starts with empty flags and requests them from server if there are no cached flags', async () => { - const platform = stubPlatform.defaults(); - server.respondWith(jsonResponse({ 'flag-key': { value: 1 } })); + expect(client.variation('flag-key')).toEqual(1); - const client = platform.testing.makeClient(envName, user, { bootstrap: 'localstorage' }); + await sleepAsync(0); // allow any pending async tasks to complete - // don't wait for ready event - verifying that variation() doesn't throw an error if called before ready - expect(client.variation('flag-key', 0)).toEqual(0); + expect(server.requests.length()).toEqual(1); + }); + }); + }); - // verify that the flags get requested from LD - await client.waitForInitialization(); - expect(client.variation('flag-key')).toEqual(1); + it('starts with empty flags and requests them from server if there are no cached flags', async () => { + const flags = { 'flag-key': { value: 1 } }; + + await withServer(async server => { + server.byDefault(respondJson(flags)); + await withClient(user, { baseUrl: server.url }, async client => { + // don't wait for ready event - verifying that variation() doesn't throw an error if called before ready + expect(client.variation('flag-key', 0)).toEqual(0); + + // verify that the flags get requested from LD + await client.waitForInitialization(); + expect(client.variation('flag-key')).toEqual(1); + }); + }); }); it('should handle localStorage.get returning an error', async () => { - const platform = stubPlatform.defaults(); platform.localStorage.get = () => Promise.reject(new Error()); - server.respondWith(jsonResponse({ 'enable-foo': { value: true } })); - - const client = platform.testing.makeClient(envName, user, { bootstrap: 'localstorage' }); - await client.waitForInitialization(); - - expect(platform.testing.logger.output.warn).toEqual([messages.localStorageUnavailable()]); + const flags = { 'enable-foo': { value: true } }; + + await withServer(async server => { + server.byDefault(respondJson(flags)); + await withClient(user, { baseUrl: server.url }, async client => { + await client.waitForInitialization(); + expect(platform.testing.logger.output.warn).toEqual([messages.localStorageUnavailable()]); + }); + }); }); it('should handle localStorage.set returning an error', async () => { - const platform = stubPlatform.defaults(); platform.localStorage.set = () => Promise.reject(new Error()); - server.respondWith(jsonResponse({ 'enable-foo': { value: true } })); + const flags = { 'enable-foo': { value: true } }; - const client = platform.testing.makeClient(envName, user, { bootstrap: 'localstorage' }); - await client.waitForInitialization(); + await withServer(async server => { + server.byDefault(respondJson(flags)); + await withClient(user, { baseUrl: server.url }, async client => { + await client.waitForInitialization(); - await asyncSleep(0); // allow any pending async tasks to complete + await sleepAsync(0); // allow any pending async tasks to complete - expect(platform.testing.logger.output.warn).toEqual([messages.localStorageUnavailable()]); + expect(platform.testing.logger.output.warn).toEqual([messages.localStorageUnavailable()]); + }); + }); }); it('should not update cached settings if there was an error fetching flags', async () => { - const platform = stubPlatform.defaults(); const json = '{"enable-foo": true}'; - server.respondWith(errorResponse(503)); platform.testing.setLocalStorageImmediately(lsKey, json); - const client = platform.testing.makeClient(envName, user, { bootstrap: 'localstorage' }); - await client.waitForInitialization(); + await withServer(async server => { + server.byDefault(respond(503)); + await withClient(user, { baseUrl: server.url }, async client => { + await client.waitForInitialization(); - await asyncSleep(0); // allow any pending async tasks to complete + await sleepAsync(0); // allow any pending async tasks to complete - const value = platform.testing.getLocalStorageImmediately(lsKey); - expect(value).toEqual(json); + const value = platform.testing.getLocalStorageImmediately(lsKey); + expect(value).toEqual(json); + }); + }); }); it('should use hash as localStorage key when secure mode is enabled', async () => { - const platform = stubPlatform.defaults(); - server.respondWith(jsonResponse({ 'enable-foo': { value: true } })); - const lsKeyHash = 'ld:UNKNOWN_ENVIRONMENT_ID:totallyLegitHash'; - const client = platform.testing.makeClient(envName, user, { - bootstrap: 'localstorage', - hash: 'totallyLegitHash', - }); - - await client.waitForInitialization(); - const value = platform.testing.getLocalStorageImmediately(lsKeyHash); - expect(JSON.parse(value)).toEqual({ - $schema: 1, - 'enable-foo': { value: true }, + const hash = 'totallyLegitHash'; + const lsKeyHash = 'ld:UNKNOWN_ENVIRONMENT_ID:' + hash; + const flags = { 'enable-foo': { value: true } }; + + await withServer(async server => { + server.byDefault(respondJson(flags)); + await withClient(user, { baseUrl: server.url, hash }, async client => { + await client.waitForInitialization(); + const value = platform.testing.getLocalStorageImmediately(lsKeyHash); + expect(JSON.parse(value)).toEqual({ + $schema: 1, + 'enable-foo': { value: true }, + }); + }); }); }); it('should clear localStorage when user context is changed', async () => { - const platform = stubPlatform.defaults(); const lsKey2 = 'ld:UNKNOWN_ENVIRONMENT_ID:' + utils.btoa('{"key":"user2"}'); - + const flags = { 'enable-foo': { value: true } }; const user2 = { key: 'user2' }; - const client = platform.testing.makeClient(envName, user, { bootstrap: 'localstorage' }); - - server.respondWith(jsonResponse({ 'enable-foo': { value: true } })); - await client.waitForInitialization(); + await withServer(async server => { + server.byDefault(respondJson(flags)); + await withClient(user, { baseUrl: server.url }, async client => { + await client.waitForInitialization(); - await asyncSleep(0); // allow any pending async tasks to complete + await sleepAsync(0); // allow any pending async tasks to complete - await client.identify(user2); + await client.identify(user2); - const value1 = platform.testing.getLocalStorageImmediately(lsKey); - expect(value1).not.toEqual(expect.anything()); - const value2 = platform.testing.getLocalStorageImmediately(lsKey2); - expect(JSON.parse(value2)).toEqual({ - $schema: 1, - 'enable-foo': { value: true }, + const value1 = platform.testing.getLocalStorageImmediately(lsKey); + expect(value1).not.toEqual(expect.anything()); + const value2 = platform.testing.getLocalStorageImmediately(lsKey2); + expect(JSON.parse(value2)).toEqual({ + $schema: 1, + 'enable-foo': { value: true }, + }); + }); }); }); }); diff --git a/src/__tests__/LDClient-streaming-test.js b/src/__tests__/LDClient-streaming-test.js index 2cd71d8..cef954a 100644 --- a/src/__tests__/LDClient-streaming-test.js +++ b/src/__tests__/LDClient-streaming-test.js @@ -1,17 +1,24 @@ -import EventSource, { sources } from './EventSource-mock'; - import * as utils from '../utils'; + +import { eventSink, sleepAsync, withCloseable } from 'launchdarkly-js-test-helpers'; + +import EventSource, { sources } from './EventSource-mock'; +import { respondJson } from './mockHttp'; import * as stubPlatform from './stubPlatform'; -import { asyncSleep, jsonResponse, makeBootstrap, makeDefaultServer, promiseListener } from './testUtils'; +import { makeBootstrap } from './testUtils'; + +// These tests verify the client's optional streaming behavior. The actual implementation of +// the SSE client is provided by the platform-specific SDKs (e.g. the browser SDK uses +// EventSource, other SDKs use the js-eventsource polyfill) so these tests use only a mock +// implementation, verifying that the SDK interacts properly with the stream abstraction. -describe('LDClient', () => { +describe('LDClient streaming', () => { const envName = 'UNKNOWN_ENVIRONMENT_ID'; const lsKey = 'ld:UNKNOWN_ENVIRONMENT_ID:' + utils.btoa('{"key":"user"}'); const user = { key: 'user' }; const encodedUser = 'eyJrZXkiOiJ1c2VyIn0'; const hash = '012345789abcde'; let platform; - let server; beforeEach(() => { Object.defineProperty(window, 'EventSource', { @@ -22,13 +29,16 @@ describe('LDClient', () => { delete sources[key]; } - server = makeDefaultServer(); platform = stubPlatform.defaults(); }); - afterEach(() => { - server.restore(); - }); + async function withClientAndServer(extraConfig, asyncCallback) { + const server = platform.testing.http.newServer(); + server.byDefault(respondJson({})); + const config = { ...extraConfig, baseUrl: server.url }; + const client = platform.testing.makeClient(envName, user, config); + return await withCloseable(client, async () => await asyncCallback(client, server)); + } describe('streaming/event listening', () => { const streamUrl = 'https://clientstream.launchdarkly.com'; @@ -47,516 +57,541 @@ describe('LDClient', () => { } it('does not connect to the stream by default', async () => { - const client = platform.testing.makeClient(envName, user); - await client.waitForInitialization(); + await withClientAndServer({}, async client => { + await client.waitForInitialization(); - expectNoStreamIsOpen(); + expectNoStreamIsOpen(); + }); }); it('connects to the stream if options.streaming is true', async () => { - const client = platform.testing.makeClient(envName, user, { streaming: true }); - await client.waitForInitialization(); + await withClientAndServer({ streaming: true }, async client => { + await client.waitForInitialization(); - expectStreamUrlIsOpen(fullStreamUrlWithUser); + expectStreamUrlIsOpen(fullStreamUrlWithUser); + }); }); describe('setStreaming()', () => { it('can connect to the stream', async () => { - const client = platform.testing.makeClient(envName, user); - await client.waitForInitialization(); + await withClientAndServer({}, async client => { + await client.waitForInitialization(); - client.setStreaming(true); - expectStreamUrlIsOpen(fullStreamUrlWithUser); + client.setStreaming(true); + expectStreamUrlIsOpen(fullStreamUrlWithUser); + }); }); it('can disconnect from the stream', async () => { - const client = platform.testing.makeClient(envName, user); - await client.waitForInitialization(); + await withClientAndServer({}, async client => { + await client.waitForInitialization(); - client.setStreaming(true); - expectStreamUrlIsOpen(fullStreamUrlWithUser); - client.setStreaming(false); - expectNoStreamIsOpen(); + client.setStreaming(true); + expectStreamUrlIsOpen(fullStreamUrlWithUser); + client.setStreaming(false); + expectNoStreamIsOpen(); + }); }); }); describe('on("change")', () => { it('connects to the stream if not otherwise overridden', async () => { - const client = platform.testing.makeClient(envName, user); - await client.waitForInitialization(); - client.on('change', () => {}); + await withClientAndServer({}, async client => { + await client.waitForInitialization(); + client.on('change', () => {}); - expectStreamUrlIsOpen(fullStreamUrlWithUser); + expectStreamUrlIsOpen(fullStreamUrlWithUser); + }); }); it('also connects if listening for a specific flag', async () => { - const client = platform.testing.makeClient(envName, user); - await client.waitForInitialization(); - client.on('change:flagkey', () => {}); + await withClientAndServer({}, async client => { + await client.waitForInitialization(); + client.on('change:flagkey', () => {}); - expectStreamUrlIsOpen(fullStreamUrlWithUser); + expectStreamUrlIsOpen(fullStreamUrlWithUser); + }); }); it('does not connect if some other kind of event was specified', async () => { - const client = platform.testing.makeClient(envName, user); - await client.waitForInitialization(); - client.on('error', () => {}); + await withClientAndServer({}, async client => { + await client.waitForInitialization(); + client.on('error', () => {}); - expectNoStreamIsOpen(); + expectNoStreamIsOpen(); + }); }); it('does not connect if options.streaming is explicitly set to false', async () => { - const client = platform.testing.makeClient(envName, user, { streaming: false }); - await client.waitForInitialization(); - client.on('change', () => {}); + await withClientAndServer({ streaming: false }, async client => { + await client.waitForInitialization(); + client.on('change', () => {}); - expectNoStreamIsOpen(); + expectNoStreamIsOpen(); + }); }); it('does not connect if setStreaming(false) was called', async () => { - const client = platform.testing.makeClient(envName, user); - await client.waitForInitialization(); - client.setStreaming(false); - client.on('change', () => {}); + await withClientAndServer({}, async client => { + await client.waitForInitialization(); + client.setStreaming(false); + client.on('change', () => {}); - expectNoStreamIsOpen(); + expectNoStreamIsOpen(); + }); }); }); describe('off("change")', () => { it('disconnects from the stream if all event listeners are removed', async () => { - const client = platform.testing.makeClient(envName, user); - const listener1 = () => {}; - const listener2 = () => {}; - await client.waitForInitialization(); + await withClientAndServer({}, async client => { + const listener1 = () => {}; + const listener2 = () => {}; + await client.waitForInitialization(); - client.on('change', listener1); - client.on('change:flagkey', listener2); - client.on('error', () => {}); - expectStreamUrlIsOpen(fullStreamUrlWithUser); + client.on('change', listener1); + client.on('change:flagKey', listener2); + client.on('error', () => {}); + expectStreamUrlIsOpen(fullStreamUrlWithUser); - client.off('change', listener1); - expectStreamUrlIsOpen(fullStreamUrlWithUser); + client.off('change', listener1); + expectStreamUrlIsOpen(fullStreamUrlWithUser); - client.off('change:flagkey', listener2); - expectNoStreamIsOpen(); + client.off('change:flagKey', listener2); + expectNoStreamIsOpen(); + }); }); it('does not disconnect if setStreaming(true) was called, but still removes event listener', async () => { const changes1 = []; const changes2 = []; - const client = platform.testing.makeClient(envName, user); - const listener1 = allValues => changes1.push(allValues); - const listener2 = newValue => changes2.push(newValue); - await client.waitForInitialization(); + await withClientAndServer({}, async client => { + const listener1 = allValues => changes1.push(allValues); + const listener2 = newValue => changes2.push(newValue); + await client.waitForInitialization(); - client.setStreaming(true); + client.setStreaming(true); - client.on('change', listener1); - client.on('change:flag', listener2); - expectStreamUrlIsOpen(fullStreamUrlWithUser); + client.on('change', listener1); + client.on('change:flagKey', listener2); + expectStreamUrlIsOpen(fullStreamUrlWithUser); - streamEvents().put({ - data: '{"flag":{"value":"a","version":1}}', - }); + streamEvents().put({ + data: '{"flagKey":{"value":"a","version":1}}', + }); - expect(changes1).toEqual([{ flag: { current: 'a', previous: undefined } }]); - expect(changes2).toEqual(['a']); + expect(changes1).toEqual([{ flagKey: { current: 'a', previous: undefined } }]); + expect(changes2).toEqual(['a']); - client.off('change', listener1); - expectStreamUrlIsOpen(fullStreamUrlWithUser); + client.off('change', listener1); + expectStreamUrlIsOpen(fullStreamUrlWithUser); - streamEvents().put({ - data: '{"flag":{"value":"b","version":1}}', - }); + streamEvents().put({ + data: '{"flagKey":{"value":"b","version":1}}', + }); - expect(changes1).toEqual([{ flag: { current: 'a', previous: undefined } }]); - expect(changes2).toEqual(['a', 'b']); + expect(changes1).toEqual([{ flagKey: { current: 'a', previous: undefined } }]); + expect(changes2).toEqual(['a', 'b']); - client.off('change:flag', listener2); - expectStreamUrlIsOpen(fullStreamUrlWithUser); + client.off('change:flagKey', listener2); + expectStreamUrlIsOpen(fullStreamUrlWithUser); - streamEvents().put({ - data: '{"flag":{"value":"c","version":1}}', - }); + streamEvents().put({ + data: '{"flagKey":{"value":"c","version":1}}', + }); - expect(changes1).toEqual([{ flag: { current: 'a', previous: undefined } }]); - expect(changes2).toEqual(['a', 'b']); + expect(changes1).toEqual([{ flagKey: { current: 'a', previous: undefined } }]); + expect(changes2).toEqual(['a', 'b']); + }); }); }); it('passes the secure mode hash in the stream URL if provided', async () => { - const client = platform.testing.makeClient(envName, user, { hash: hash }); - await client.waitForInitialization(); - client.on('change:flagkey', () => {}); + await withClientAndServer({ hash }, async client => { + await client.waitForInitialization(); + client.on('change:flagKey', () => {}); - expectStreamUrlIsOpen(fullStreamUrlWithUser + '?h=' + hash); + expectStreamUrlIsOpen(fullStreamUrlWithUser + '?h=' + hash); + }); }); it('passes withReasons parameter if provided', async () => { - const client = platform.testing.makeClient(envName, user, { evaluationReasons: true }); - await client.waitForInitialization(); - client.setStreaming(true); + await withClientAndServer({ evaluationReasons: true }, async client => { + await client.waitForInitialization(); + client.setStreaming(true); - expectStreamUrlIsOpen(fullStreamUrlWithUser + '?withReasons=true'); + expectStreamUrlIsOpen(fullStreamUrlWithUser + '?withReasons=true'); + }); }); it('passes secure mode hash and withReasons if provided', async () => { - const client = platform.testing.makeClient(envName, user, { hash: hash, evaluationReasons: true }); - await client.waitForInitialization(); - client.setStreaming(true); + await withClientAndServer({ hash, evaluationReasons: true }, async client => { + await client.waitForInitialization(); + client.setStreaming(true); - expectStreamUrlIsOpen(fullStreamUrlWithUser + '?h=' + hash + '&withReasons=true'); + expectStreamUrlIsOpen(fullStreamUrlWithUser + '?h=' + hash + '&withReasons=true'); + }); }); it('handles stream ping message by getting flags', async () => { - server.respondWith(jsonResponse({ 'enable-foo': { value: true, version: 1 } })); - - const client = platform.testing.makeClient(envName, user); - await client.waitForInitialization(); - client.setStreaming(true); + await withClientAndServer({}, async (client, server) => { + server.byDefault(respondJson({ flagKey: { value: true, version: 1 } })); + await client.waitForInitialization(); + client.setStreaming(true); - streamEvents().ping(); - await asyncSleep(20); // give response handler a chance to execute + streamEvents().ping(); + await sleepAsync(20); // give response handler a chance to execute - expect(client.variation('enable-foo')).toEqual(true); + expect(client.variation('flagKey')).toEqual(true); + }); }); it('handles stream put message by updating flags', async () => { - const client = platform.testing.makeClient(envName, user); - await client.waitForInitialization(); - client.setStreaming(true); + await withClientAndServer({}, async client => { + await client.waitForInitialization(); + client.setStreaming(true); - streamEvents().put({ - data: '{"enable-foo":{"value":true,"version":1}}', - }); + streamEvents().put({ + data: '{"flagKey":{"value":true,"version":1}}', + }); - expect(client.variation('enable-foo')).toEqual(true); + expect(client.variation('flagKey')).toEqual(true); + }); }); it('updates local storage for put message if using local storage', async () => { - const platform = stubPlatform.defaults(); - platform.testing.setLocalStorageImmediately(lsKey, '{"enable-foo":false}'); + platform.testing.setLocalStorageImmediately(lsKey, '{"flagKey":false}'); - const client = platform.testing.makeClient(envName, user, { bootstrap: 'localstorage' }, platform); - await client.waitForInitialization(); - client.setStreaming(true); + await withClientAndServer({ bootstrap: 'localstorage' }, async client => { + await client.waitForInitialization(); + client.setStreaming(true); - streamEvents().put({ - data: '{"enable-foo":{"value":true,"version":1}}', - }); + streamEvents().put({ + data: '{"flagKey":{"value":true,"version":1}}', + }); - expect(client.variation('enable-foo')).toEqual(true); - const storageData = JSON.parse(platform.testing.getLocalStorageImmediately(lsKey)); - expect(storageData).toMatchObject({ 'enable-foo': { value: true, version: 1 } }); + expect(client.variation('flagKey')).toEqual(true); + const storageData = JSON.parse(platform.testing.getLocalStorageImmediately(lsKey)); + expect(storageData).toMatchObject({ flagKey: { value: true, version: 1 } }); + }); }); it('fires global change event when flags are updated from put event', async () => { - const client = platform.testing.makeClient(envName, user, { bootstrap: { 'enable-foo': false } }); - await client.waitForInitialization(); + await withClientAndServer({ bootstrap: { flagKey: false } }, async client => { + await client.waitForInitialization(); - const receivedChange = promiseListener(); - client.on('change', receivedChange.callback); + const receivedChange = eventSink(client, 'change'); - streamEvents().put({ - data: '{"enable-foo":{"value":true,"version":1}}', - }); + streamEvents().put({ + data: '{"flagKey":{"value":true,"version":1}}', + }); - const changes = await receivedChange; - expect(changes).toEqual({ - 'enable-foo': { current: true, previous: false }, + const changes = await receivedChange.take(); + expect(changes).toEqual({ + flagKey: { current: true, previous: false }, + }); }); }); it('does not fire change event if new and old values are equivalent JSON objects', async () => { - const client = platform.testing.makeClient(envName, user, { + const config = { bootstrap: { 'will-change': 3, 'wont-change': { a: 1, b: 2 }, }, - }); - await client.waitForInitialization(); + }; + await withClientAndServer(config, async client => { + await client.waitForInitialization(); - const receivedChange = promiseListener(); - client.on('change', receivedChange.callback); + const receivedChange = eventSink(client, 'change'); - const putData = { - 'will-change': { value: 4, version: 2 }, - 'wont-change': { value: { b: 2, a: 1 }, version: 2 }, - }; - streamEvents().put({ data: JSON.stringify(putData) }); + const putData = { + 'will-change': { value: 4, version: 2 }, + 'wont-change': { value: { b: 2, a: 1 }, version: 2 }, + }; + streamEvents().put({ data: JSON.stringify(putData) }); - const changes = await receivedChange; - expect(changes).toEqual({ - 'will-change': { current: 4, previous: 3 }, + const changes = await receivedChange.take(); + expect(changes).toEqual({ + 'will-change': { current: 4, previous: 3 }, + }); }); }); it('fires individual change event when flags are updated from put event', async () => { - const client = platform.testing.makeClient(envName, user, { bootstrap: { 'enable-foo': false } }); - await client.waitForInitialization(); + await withClientAndServer({ bootstrap: { flagKey: false } }, async client => { + await client.waitForInitialization(); - const receivedChange = promiseListener(); - client.on('change:enable-foo', receivedChange.callback); + const receivedChange = eventSink(client, 'change:flagKey'); - streamEvents().put({ - data: '{"enable-foo":{"value":true,"version":1}}', - }); + streamEvents().put({ + data: '{"flagKey":{"value":true,"version":1}}', + }); - const args = await receivedChange; - expect(args).toEqual([true, false]); + const args = await receivedChange.take(); + expect(args).toEqual([true, false]); + }); }); it('handles patch message by updating flag', async () => { - const client = platform.testing.makeClient(envName, user, { bootstrap: { 'enable-foo': false } }); - await client.waitForInitialization(); - client.setStreaming(true); + await withClientAndServer({ bootstrap: { flagKey: false } }, async client => { + await client.waitForInitialization(); + client.setStreaming(true); - streamEvents().patch({ data: '{"key":"enable-foo","value":true,"version":1}' }); + streamEvents().patch({ data: '{"key":"flagKey","value":true,"version":1}' }); - expect(client.variation('enable-foo')).toEqual(true); + expect(client.variation('flagKey')).toEqual(true); + }); }); it('does not update flag if patch version < flag version', async () => { - const initData = makeBootstrap({ 'enable-foo': { value: 'a', version: 2 } }); - const client = platform.testing.makeClient(envName, user, { bootstrap: initData }); - await client.waitForInitialization(); + const initData = makeBootstrap({ flagKey: { value: 'a', version: 2 } }); + await withClientAndServer({ bootstrap: initData }, async client => { + await client.waitForInitialization(); - expect(client.variation('enable-foo')).toEqual('a'); + expect(client.variation('flagKey')).toEqual('a'); - client.setStreaming(true); + client.setStreaming(true); - streamEvents().patch({ data: '{"key":"enable-foo","value":"b","version":1}' }); + streamEvents().patch({ data: '{"key":"flagKey","value":"b","version":1}' }); - expect(client.variation('enable-foo')).toEqual('a'); + expect(client.variation('flagKey')).toEqual('a'); + }); }); it('does not update flag if patch version == flag version', async () => { - const initData = makeBootstrap({ 'enable-foo': { value: 'a', version: 2 } }); - const client = platform.testing.makeClient(envName, user, { bootstrap: initData }); - await client.waitForInitialization(); + const initData = makeBootstrap({ flagKey: { value: 'a', version: 2 } }); + await withClientAndServer({ bootstrap: initData }, async client => { + await client.waitForInitialization(); - expect(client.variation('enable-foo')).toEqual('a'); + expect(client.variation('flagKey')).toEqual('a'); - client.setStreaming(true); + client.setStreaming(true); - streamEvents().patch({ data: '{"key":"enable-foo","value":"b","version":2}' }); + streamEvents().patch({ data: '{"key":"flagKey","value":"b","version":2}' }); - expect(client.variation('enable-foo')).toEqual('a'); + expect(client.variation('flagKey')).toEqual('a'); + }); }); it('updates flag if patch has a version and flag has no version', async () => { - const initData = makeBootstrap({ 'enable-foo': { value: 'a' } }); - const client = platform.testing.makeClient(envName, user, { bootstrap: initData }); - await client.waitForInitialization(); + const initData = makeBootstrap({ flagKey: { value: 'a' } }); + await withClientAndServer({ bootstrap: initData }, async client => { + await client.waitForInitialization(); - expect(client.variation('enable-foo')).toEqual('a'); + expect(client.variation('flagKey')).toEqual('a'); - client.setStreaming(true); + client.setStreaming(true); - streamEvents().patch({ data: '{"key":"enable-foo","value":"b","version":1}' }); + streamEvents().patch({ data: '{"key":"flagKey","value":"b","version":1}' }); - expect(client.variation('enable-foo')).toEqual('b'); + expect(client.variation('flagKey')).toEqual('b'); + }); }); it('updates flag if flag has a version and patch has no version', async () => { - const initData = makeBootstrap({ 'enable-foo': { value: 'a', version: 2 } }); - const client = platform.testing.makeClient(envName, user, { bootstrap: initData }); - await client.waitForInitialization(); + const initData = makeBootstrap({ flagKey: { value: 'a', version: 2 } }); + await withClientAndServer({ bootstrap: initData }, async client => { + await client.waitForInitialization(); - expect(client.variation('enable-foo')).toEqual('a'); + expect(client.variation('flagKey')).toEqual('a'); - client.setStreaming(true); + client.setStreaming(true); - streamEvents().patch({ data: '{"key":"enable-foo","value":"b"}' }); + streamEvents().patch({ data: '{"key":"flagKey","value":"b"}' }); - expect(client.variation('enable-foo')).toEqual('b'); + expect(client.variation('flagKey')).toEqual('b'); + }); }); it('updates local storage for patch message if using local storage', async () => { - const platform = stubPlatform.defaults(); - platform.testing.setLocalStorageImmediately(lsKey, '{"enable-foo":false}'); + platform.testing.setLocalStorageImmediately(lsKey, '{"flagKey":false}'); - const client = platform.testing.makeClient(envName, user, { bootstrap: 'localstorage' }, platform); - await client.waitForInitialization(); - client.setStreaming(true); + await withClientAndServer({ bootstrap: 'localstorage' }, async client => { + await client.waitForInitialization(); + client.setStreaming(true); - streamEvents().put({ - data: '{"enable-foo":{"value":true,"version":1}}', - }); + streamEvents().put({ + data: '{"flagKey":{"value":true,"version":1}}', + }); - expect(client.variation('enable-foo')).toEqual(true); - const storageData = JSON.parse(platform.testing.getLocalStorageImmediately(lsKey)); - expect(storageData).toMatchObject({ 'enable-foo': { value: true, version: 1 } }); + expect(client.variation('flagKey')).toEqual(true); + const storageData = JSON.parse(platform.testing.getLocalStorageImmediately(lsKey)); + expect(storageData).toMatchObject({ flagKey: { value: true, version: 1 } }); + }); }); it('fires global change event when flag is updated from patch event', async () => { - const client = platform.testing.makeClient(envName, user, { bootstrap: { 'enable-foo': false } }); - await client.waitForInitialization(); + await withClientAndServer({ bootstrap: { flagKey: false } }, async client => { + await client.waitForInitialization(); - const receivedChange = promiseListener(); - client.on('change', receivedChange.callback); + const receivedChange = eventSink(client, 'change'); - streamEvents().patch({ - data: '{"key":"enable-foo","value":true,"version":1}', - }); + streamEvents().patch({ + data: '{"key":"flagKey","value":true,"version":1}', + }); - const changes = await receivedChange; - expect(changes).toEqual({ - 'enable-foo': { current: true, previous: false }, + const changes = await receivedChange.take(); + expect(changes).toEqual({ + flagKey: { current: true, previous: false }, + }); }); }); it('fires individual change event when flag is updated from patch event', async () => { - const client = platform.testing.makeClient(envName, user, { bootstrap: { 'enable-foo': false } }); - await client.waitForInitialization(); + await withClientAndServer({ bootstrap: { flagKey: false } }, async client => { + await client.waitForInitialization(); - const receivedChange = promiseListener(); - client.on('change:enable-foo', receivedChange.callback); + const receivedChange = eventSink(client, 'change:flagKey'); - streamEvents().patch({ - data: '{"key":"enable-foo","value":true,"version":1}', - }); + streamEvents().patch({ + data: '{"key":"flagKey","value":true,"version":1}', + }); - const args = await receivedChange; - expect(args).toEqual([true, false]); + const args = await receivedChange.take(); + expect(args).toEqual([true, false]); + }); }); it('fires global change event when flag is newly created from patch event', async () => { - const client = platform.testing.makeClient(envName, user); - await client.waitForInitialization(); + await withClientAndServer({}, async client => { + await client.waitForInitialization(); - const receivedChange = promiseListener(); - client.on('change', receivedChange.callback); + const receivedChange = eventSink(client, 'change'); - streamEvents().patch({ - data: '{"key":"enable-foo","value":true,"version":1}', - }); + streamEvents().patch({ + data: '{"key":"flagKey","value":true,"version":1}', + }); - const changes = await receivedChange; - expect(changes).toEqual({ - 'enable-foo': { current: true }, + const changes = await receivedChange.take(); + expect(changes).toEqual({ + flagKey: { current: true }, + }); }); }); it('fires individual change event when flag is newly created from patch event', async () => { - const client = platform.testing.makeClient(envName, user); - await client.waitForInitialization(); + await withClientAndServer({}, async client => { + await client.waitForInitialization(); - const receivedChange = promiseListener(); - client.on('change:enable-foo', receivedChange.callback); + const receivedChange = eventSink(client, 'change:flagKey'); - streamEvents().patch({ - data: '{"key":"enable-foo","value":true,"version":1}', - }); + streamEvents().patch({ + data: '{"key":"flagKey","value":true,"version":1}', + }); - const args = await receivedChange; - expect(args).toEqual([true, undefined]); + const args = await receivedChange.take(); + expect(args).toEqual([true, undefined]); + }); }); it('handles delete message by deleting flag', async () => { - const client = platform.testing.makeClient(envName, user, { bootstrap: { 'enable-foo': false } }); - await client.waitForInitialization(); - client.setStreaming(true); + await withClientAndServer({ bootstrap: { flagKey: false } }, async client => { + await client.waitForInitialization(); + client.setStreaming(true); - streamEvents().delete({ - data: '{"key":"enable-foo","version":1}', - }); + streamEvents().delete({ + data: '{"key":"flagKey","version":1}', + }); - expect(client.variation('enable-foo')).toBeUndefined(); + expect(client.variation('flagKey')).toBeUndefined(); + }); }); it('handles delete message for unknown flag by storing placeholder', async () => { - const client = platform.testing.makeClient(envName, user); - await client.waitForInitialization(); - client.setStreaming(true); + await withClientAndServer({}, async client => { + await client.waitForInitialization(); + client.setStreaming(true); - streamEvents().delete({ - data: '{"key":"mystery","version":3}', - }); + streamEvents().delete({ + data: '{"key":"mystery","version":3}', + }); - // The following patch message should be ignored because it has a lower version than the deleted placeholder - streamEvents().patch({ - data: '{"key":"mystery","value":"yes","version":2}', - }); + // The following patch message should be ignored because it has a lower version than the deleted placeholder + streamEvents().patch({ + data: '{"key":"mystery","value":"yes","version":2}', + }); - expect(client.variation('mystery')).toBeUndefined(); + expect(client.variation('mystery')).toBeUndefined(); + }); }); it('ignores delete message with lower version', async () => { - const initData = makeBootstrap({ flag: { value: 'yes', version: 3 } }); - const client = platform.testing.makeClient(envName, user, { bootstrap: initData }); - await client.waitForInitialization(); - client.setStreaming(true); + const initData = makeBootstrap({ flagKey: { value: 'yes', version: 3 } }); + await withClientAndServer({ bootstrap: initData }, async client => { + await client.waitForInitialization(); + client.setStreaming(true); - streamEvents().delete({ - data: '{"key":"flag","version":2}', - }); + streamEvents().delete({ + data: '{"key":"flagKey","version":2}', + }); - expect(client.variation('flag')).toEqual('yes'); + expect(client.variation('flagKey')).toEqual('yes'); + }); }); it('fires global change event when flag is deleted', async () => { - const client = platform.testing.makeClient(envName, user, { bootstrap: { 'enable-foo': true } }); - await client.waitForInitialization(); + await withClientAndServer({ bootstrap: { flagKey: true } }, async client => { + await client.waitForInitialization(); - const receivedChange = promiseListener(); - client.on('change', receivedChange.callback); + const receivedChange = eventSink(client, 'change'); - streamEvents().delete({ - data: '{"key":"enable-foo","version":1}', - }); + streamEvents().delete({ + data: '{"key":"flagKey","version":1}', + }); - const changes = await receivedChange; - expect(changes).toEqual({ - 'enable-foo': { previous: true }, + const changes = await receivedChange.take(); + expect(changes).toEqual({ + flagKey: { previous: true }, + }); }); }); it('fires individual change event when flag is deleted', async () => { - const client = platform.testing.makeClient(envName, user, { bootstrap: { 'enable-foo': true } }); - await client.waitForInitialization(); + await withClientAndServer({ bootstrap: { flagKey: true } }, async client => { + await client.waitForInitialization(); - const receivedChange = promiseListener(); - client.on('change:enable-foo', receivedChange.callback); + const receivedChange = eventSink(client, 'change:flagKey'); - streamEvents().delete({ - data: '{"key":"enable-foo","version":1}', - }); + streamEvents().delete({ + data: '{"key":"flagKey","version":1}', + }); - const args = await receivedChange; - expect(args).toEqual([undefined, true]); + const args = await receivedChange.take(); + expect(args).toEqual([undefined, true]); + }); }); it('updates local storage for delete message if using local storage', async () => { - const platform = stubPlatform.defaults(); - platform.testing.setLocalStorageImmediately(lsKey, '{"enable-foo":false}'); + platform.testing.setLocalStorageImmediately(lsKey, '{"flagKey":false}'); - const client = platform.testing.makeClient(envName, user, { bootstrap: 'localstorage' }, platform); - await client.waitForInitialization(); - client.setStreaming(true); + await withClientAndServer({ bootstrap: 'localstorage' }, async client => { + await client.waitForInitialization(); + client.setStreaming(true); - streamEvents().delete({ - data: '{"key":"enable-foo","version":1}', - }); + streamEvents().delete({ + data: '{"key":"flagKey","version":1}', + }); - expect(client.variation('enable-foo')).toEqual(undefined); - const storageData = JSON.parse(platform.testing.getLocalStorageImmediately(lsKey)); - expect(storageData).toMatchObject({ 'enable-foo': { version: 1, deleted: true } }); + expect(client.variation('flagKey')).toEqual(undefined); + const storageData = JSON.parse(platform.testing.getLocalStorageImmediately(lsKey)); + expect(storageData).toMatchObject({ flagKey: { version: 1, deleted: true } }); + }); }); it('reconnects to stream if the user changes', async () => { const user2 = { key: 'user2' }; const encodedUser2 = 'eyJrZXkiOiJ1c2VyMiJ9'; - const client = platform.testing.makeClient(envName, user); - await client.waitForInitialization(); - client.setStreaming(true); + await withClientAndServer({}, async client => { + await client.waitForInitialization(); + client.setStreaming(true); - expect(sources[streamUrl + '/eval/' + envName + '/' + encodedUser]).toBeDefined(); + expect(sources[streamUrl + '/eval/' + envName + '/' + encodedUser]).toBeDefined(); - await client.identify(user2); - expect(sources[streamUrl + '/eval/' + envName + '/' + encodedUser2]).toBeDefined(); + await client.identify(user2); + expect(sources[streamUrl + '/eval/' + envName + '/' + encodedUser2]).toBeDefined(); + }); }); }); }); diff --git a/src/__tests__/LDClient-test.js b/src/__tests__/LDClient-test.js index b8453da..13fe7b8 100644 --- a/src/__tests__/LDClient-test.js +++ b/src/__tests__/LDClient-test.js @@ -1,36 +1,37 @@ -import semverCompare from 'semver-compare'; - -import * as stubPlatform from './stubPlatform'; -import { - asyncify, - errorResponse, - jsonResponse, - makeBootstrap, - makeDefaultServer, - numericUser, - promiseListener, - stringifiedNumericUser, -} from './testUtils'; - import * as LDClient from '../index'; import * as errors from '../errors'; import * as messages from '../messages'; import * as utils from '../utils'; +import semverCompare from 'semver-compare'; +import { eventSink, promisifySingle, sleepAsync, withCloseable } from 'launchdarkly-js-test-helpers'; + +import { respond, respondJson } from './mockHttp'; +import * as stubPlatform from './stubPlatform'; +import { makeBootstrap, numericUser, stringifiedNumericUser } from './testUtils'; + describe('LDClient', () => { const envName = 'UNKNOWN_ENVIRONMENT_ID'; const user = { key: 'user' }; let platform; - let server; beforeEach(() => { - server = makeDefaultServer(); platform = stubPlatform.defaults(); }); - afterEach(() => { - server.restore(); - }); + async function withServers(asyncCallback) { + const pollServer = platform.testing.http.newServer(); + const eventsServer = platform.testing.http.newServer(); + pollServer.byDefault(respondJson({})); + eventsServer.byDefault(respond(202)); + const baseConfig = { baseUrl: pollServer.url, eventsUrl: eventsServer.url }; + return await asyncCallback(baseConfig, pollServer, eventsServer); + } + + async function withClient(user, extraConfig, asyncCallback) { + const client = platform.testing.makeClient(envName, user, extraConfig); + return await withCloseable(client, asyncCallback); + } it('should exist', () => { expect(LDClient).toBeDefined(); @@ -38,81 +39,98 @@ describe('LDClient', () => { describe('initialization', () => { it('should trigger the ready event', async () => { - const client = platform.testing.makeClient(envName, user); - const gotReady = promiseListener(); - client.on('ready', gotReady.callback); + await withServers(async baseConfig => { + await withClient(user, baseConfig, async client => { + const gotReady = eventSink(client, 'ready'); + await gotReady.take(); - await gotReady; - expect(platform.testing.logger.output.info).toEqual([messages.clientInitialized()]); + expect(platform.testing.logger.output.info).toEqual([messages.clientInitialized()]); + }); + }); }); it('should trigger the initialized event', async () => { - const client = platform.testing.makeClient(envName, user); - const gotInited = promiseListener(); - client.on('initialized', gotInited.callback); - - await gotInited; + await withServers(async baseConfig => { + await withClient(user, baseConfig, async client => { + const gotInited = eventSink(client, 'initialized'); + await gotInited.take(); + }); + }); }); it('should emit an error when initialize is called without an environment key', async () => { const client = platform.testing.makeClient('', user); - const gotError = promiseListener(); - client.on('error', gotError.callback); + const gotError = eventSink(client, 'error'); - const err = await gotError; + const err = await gotError.take(); expect(err.message).toEqual(messages.environmentNotSpecified()); }); it('should emit an error when an invalid environment key is specified', async () => { - server.respondWith(errorResponse(404)); - - const client = platform.testing.makeClient('abc', user); - const gotError = promiseListener(); - client.on('error', gotError.callback); + await withServers(async (baseConfig, pollServer) => { + pollServer.byDefault(respond(404)); + await withClient(user, baseConfig, async client => { + const gotError = eventSink(client, 'error'); - await expect(client.waitForInitialization()).rejects.toThrow(); + await expect(client.waitForInitialization()).rejects.toThrow(); - const err = await gotError; - expect(err).toEqual(new errors.LDInvalidEnvironmentIdError(messages.environmentNotFound())); + const err = await gotError.take(); + expect(err).toEqual(new errors.LDInvalidEnvironmentIdError(messages.environmentNotFound())); + }); + }); }); it('should emit a failure event when an invalid environment key is specified', async () => { - server.respondWith(errorResponse(404)); + await withServers(async (baseConfig, pollServer) => { + pollServer.byDefault(respond(404)); + await withClient(user, baseConfig, async client => { + const gotFailed = eventSink(client, 'failed'); - const client = platform.testing.makeClient('abc', user); - const gotFailed = promiseListener(); - client.on('failed', gotFailed.callback); + await expect(client.waitForInitialization()).rejects.toThrow(); - await expect(client.waitForInitialization()).rejects.toThrow(); - - const err = await gotFailed; - expect(err).toEqual(new errors.LDInvalidEnvironmentIdError(messages.environmentNotFound())); + const err = await gotFailed.take(); + expect(err).toEqual(new errors.LDInvalidEnvironmentIdError(messages.environmentNotFound())); + }); + }); }); it('returns default values when an invalid environment key is specified', async () => { - server.respondWith(errorResponse(404)); - - const client = platform.testing.makeClient('abc', user); - - await expect(client.waitForInitialization()).rejects.toThrow(); + await withServers(async (baseConfig, pollServer) => { + pollServer.byDefault(respond(404)); + await withClient(user, baseConfig, async client => { + await expect(client.waitForInitialization()).rejects.toThrow(); - expect(client.variation('flag-key', 1)).toEqual(1); + expect(client.variation('flag-key', 1)).toEqual(1); + }); + }); }); it('fetches flag settings if bootstrap is not provided (without reasons)', async () => { - const client = platform.testing.makeClient(envName, user); - await client.waitForInitialization(); - - expect(/sdk\/eval/.test(server.requests[0].url)).toEqual(true); - expect(/withReasons=true/.test(server.requests[0].url)).toEqual(false); + const flags = { flagKey: { value: true } }; + await withServers(async (baseConfig, pollServer) => { + pollServer.byDefault(respondJson(flags)); + await withClient(user, baseConfig, async client => { + await client.waitForInitialization(); + + const req = await pollServer.nextRequest(); + expect(req.path).toMatch(/sdk\/eval/); + expect(req.path).not.toMatch(/withReasons=true/); + }); + }); }); it('fetches flag settings if bootstrap is not provided (with reasons)', async () => { - const client = platform.testing.makeClient(envName, user, { evaluationReasons: true }); - await client.waitForInitialization(); - - expect(/sdk\/eval/.test(server.requests[0].url)).toEqual(true); - expect(/withReasons=true/.test(server.requests[0].url)).toEqual(true); + const flags = { flagKey: { value: true, variation: 1, reason: { kind: 'OFF' } } }; + await withServers(async (baseConfig, pollServer) => { + pollServer.byDefault(respondJson(flags)); + await withClient(user, { ...baseConfig, evaluationReasons: true }, async client => { + await client.waitForInitialization(); + + const req = await pollServer.nextRequest(); + expect(req.path).toMatch(/sdk\/eval/); + expect(req.path).toMatch(/withReasons=true/); + }); + }); }); it('should contain package version', () => { @@ -122,51 +140,29 @@ describe('LDClient', () => { expect(result).toEqual(1); }); - it('should not warn when tracking a custom event', async () => { - const client = platform.testing.makeClient(envName, user); - await client.waitForInitialization(); - - client.track('known'); - expect(platform.testing.logger.output.warn).toEqual([]); - }); - - it('should emit an error when tracking a non-string custom event', async () => { - const client = platform.testing.makeClient(envName, user); - await client.waitForInitialization(); - - const badCustomEventKeys = [123, [], {}, null, undefined]; - badCustomEventKeys.forEach(key => { - platform.testing.logger.reset(); - client.track(key); - expect(platform.testing.logger.output.error).toEqual([messages.unknownCustomEventKey(key)]); - }); - }); - it('should emit an error event if there was an error fetching flags', async () => { - server.respondWith(errorResponse(503)); - - const client = platform.testing.makeClient(envName, user); - - const gotError = promiseListener(); - client.on('error', gotError.callback); - - await expect(client.waitForInitialization()).rejects.toThrow(); - await gotError; - }); - - it('should warn about missing user on first event', () => { - const client = platform.testing.makeClient(envName, null); - client.track('eventkey', null); - expect(platform.testing.logger.output.warn).toEqual([messages.eventWithoutUser()]); + await withServers(async (baseConfig, pollServer) => { + pollServer.byDefault(respond(503)); + await withClient(user, baseConfig, async client => { + const gotError = eventSink(client, 'error'); + + await expect(client.waitForInitialization()).rejects.toThrow(); + const err = await gotError.take(); + expect(err).toEqual(new errors.LDFlagFetchError(messages.errorFetchingFlags(503))); + }); + }); }); async function verifyCustomHeader(sendLDHeaders, shouldGetHeaders) { - const client = platform.testing.makeClient(envName, user, { sendLDHeaders: sendLDHeaders }); - await client.waitForInitialization(); - const request = server.requests[0]; - expect(request.requestHeaders['X-LaunchDarkly-User-Agent']).toEqual( - shouldGetHeaders ? utils.getLDUserAgentString(platform) : undefined - ); + await withServers(async (baseConfig, pollServer) => { + await withClient(user, { ...baseConfig, sendLDHeaders }, async client => { + await client.waitForInitialization(); + const request = await pollServer.nextRequest(); + expect(request.headers['x-launchdarkly-user-agent']).toEqual( + shouldGetHeaders ? utils.getLDUserAgentString(platform) : undefined + ); + }); + }); } it('sends custom header by default', () => verifyCustomHeader(undefined, true)); @@ -176,297 +172,348 @@ describe('LDClient', () => { it('does not send custom header if sendLDHeaders is false', () => verifyCustomHeader(undefined, true)); it('sanitizes the user', async () => { - const client = platform.testing.makeClient(envName, numericUser); - await client.waitForInitialization(); - expect(client.getUser()).toEqual(stringifiedNumericUser); + await withServers(async baseConfig => { + await withClient(numericUser, baseConfig, async client => { + await client.waitForInitialization(); + expect(client.getUser()).toEqual(stringifiedNumericUser); + }); + }); }); it('provides a persistent key for an anonymous user with no key', async () => { const anonUser = { anonymous: true, country: 'US' }; - const client0 = platform.testing.makeClient(envName, anonUser); - await client0.waitForInitialization(); - - const newUser0 = client0.getUser(); - expect(newUser0.key).toEqual(expect.anything()); - expect(newUser0).toMatchObject(anonUser); - - const client1 = platform.testing.makeClient(envName, anonUser); - await client1.waitForInitialization(); - - const newUser1 = client1.getUser(); - expect(newUser1).toEqual(newUser0); + await withServers(async baseConfig => { + let generatedUser; + await withClient(anonUser, baseConfig, async client0 => { + await client0.waitForInitialization(); + + generatedUser = client0.getUser(); + expect(generatedUser.key).toEqual(expect.anything()); + expect(generatedUser).toMatchObject(anonUser); + }); + await withClient(anonUser, baseConfig, async client1 => { + await client1.waitForInitialization(); + + const newUser1 = client1.getUser(); + expect(newUser1).toEqual(generatedUser); + }); + }); }); it('provides a key for an anonymous user with no key, even if local storage is unavailable', async () => { platform.localStorage = null; - const anonUser = { anonymous: true, country: 'US' }; - const client0 = platform.testing.makeClient(envName, anonUser); - await client0.waitForInitialization(); - - const newUser0 = client0.getUser(); - expect(newUser0.key).toEqual(expect.anything()); - expect(newUser0).toMatchObject(anonUser); - const client1 = platform.testing.makeClient(envName, anonUser); - await client1.waitForInitialization(); - - const newUser1 = client1.getUser(); - expect(newUser1.key).toEqual(expect.anything()); - // This key is probably different from newUser0.key, but that depends on execution time, so we can't count on it. - expect(newUser1).toMatchObject(anonUser); + await withServers(async baseConfig => { + let generatedUser; + await withClient(anonUser, baseConfig, async client0 => { + await client0.waitForInitialization(); + + generatedUser = client0.getUser(); + expect(generatedUser.key).toEqual(expect.anything()); + expect(generatedUser).toMatchObject(anonUser); + }); + await sleepAsync(100); // so that the time-based UUID algorithm will produce a different result below + await withClient(anonUser, baseConfig, async client1 => { + await client1.waitForInitialization(); + + const newUser1 = client1.getUser(); + expect(newUser1.key).toEqual(expect.anything()); + expect(newUser1.key).not.toEqual(generatedUser.key); + expect(newUser1).toMatchObject(anonUser); + }); + }); }); }); describe('initialization with bootstrap object', () => { it('should not fetch flag settings', async () => { - const client = platform.testing.makeClient(envName, user, { bootstrap: {} }); - await client.waitForInitialization(); + await withServers(async (baseConfig, pollServer) => { + await withClient(user, { ...baseConfig, bootstrap: {} }, async client => { + await client.waitForInitialization(); - expect(server.requests.length).toEqual(0); + expect(pollServer.requests.length()).toEqual(0); + }); + }); }); it('makes flags available immediately before ready event', async () => { - const initData = makeBootstrap({ foo: { value: 'bar', version: 1 } }); - const client = platform.testing.makeClient(envName, user, { bootstrap: initData }); - - expect(client.variation('foo')).toEqual('bar'); + await withServers(async baseConfig => { + const initData = makeBootstrap({ foo: { value: 'bar', version: 1 } }); + await withClient(user, { ...baseConfig, bootstrap: initData }, async client => { + expect(client.variation('foo')).toEqual('bar'); + }); + }); }); it('logs warning when bootstrap object uses old format', async () => { - const client = platform.testing.makeClient(envName, user, { bootstrap: { foo: 'bar' } }); - await client.waitForInitialization(); + const initData = { foo: 'bar' }; + await withClient(user, { bootstrap: initData, sendEvents: false }, async client => { + await client.waitForInitialization(); - expect(platform.testing.logger.output.warn).toEqual([messages.bootstrapOldFormat()]); + expect(platform.testing.logger.output.warn).toEqual([messages.bootstrapOldFormat()]); + }); }); it('does not log warning when bootstrap object uses new format', async () => { const initData = makeBootstrap({ foo: { value: 'bar', version: 1 } }); - const client = platform.testing.makeClient(envName, user, { bootstrap: initData }); - await client.waitForInitialization(); + await withClient(user, { bootstrap: initData, sendEvents: false }, async client => { + await client.waitForInitialization(); - expect(platform.testing.logger.output.warn).toEqual([]); - expect(client.variation('foo')).toEqual('bar'); + expect(platform.testing.logger.output.warn).toEqual([]); + expect(client.variation('foo')).toEqual('bar'); + }); }); }); describe('waitUntilReady', () => { it('should resolve waitUntilReady promise when ready', async () => { - const client = platform.testing.makeClient(envName, user); - const gotReady = promiseListener(); - client.on('ready', gotReady.callback); + await withServers(async baseConfig => { + await withClient(user, baseConfig, async client => { + const gotReady = eventSink(client, 'ready'); - await gotReady; - await client.waitUntilReady(); + await gotReady.take(); + await client.waitUntilReady(); + }); + }); }); }); describe('waitForInitialization', () => { it('resolves promise on successful init', async () => { - const client = platform.testing.makeClient(envName, user); - const gotReady = promiseListener(); - client.on('ready', gotReady.callback); + await withServers(async baseConfig => { + await withClient(user, baseConfig, async client => { + const gotReady = eventSink(client, 'ready'); - await gotReady; - await client.waitForInitialization(); + await gotReady.take(); + await client.waitForInitialization(); + }); + }); }); it('rejects promise if flags request fails', async () => { - server.respondWith(errorResponse(404)); - - const client = platform.testing.makeClient('abc', user); - const err = new errors.LDInvalidEnvironmentIdError(messages.environmentNotFound()); - await expect(client.waitForInitialization()).rejects.toThrow(err); + await withServers(async (baseConfig, pollServer) => { + pollServer.byDefault(respond(404)); + await withClient(user, baseConfig, async client => { + const err = new errors.LDInvalidEnvironmentIdError(messages.environmentNotFound()); + await expect(client.waitForInitialization()).rejects.toThrow(err); + }); + }); }); }); describe('variation', () => { it('returns value for an existing flag - from bootstrap', async () => { - const client = platform.testing.makeClient(envName, user, { + const config = { bootstrap: makeBootstrap({ foo: { value: 'bar', version: 1 } }), - }); - await client.waitForInitialization(); + sendEvents: false, + }; + await withClient(user, config, async client => { + await client.waitForInitialization(); - expect(client.variation('foo')).toEqual('bar'); + expect(client.variation('foo')).toEqual('bar'); + }); }); it('returns value for an existing flag - from bootstrap with old format', async () => { - const client = platform.testing.makeClient(envName, user, { - bootstrap: { foo: 'bar' }, - }); - await client.waitForInitialization(); + const config = { + bootstrap: makeBootstrap({ foo: { value: 'bar', version: 1 } }), + sendEvents: false, + }; + await withClient(user, config, async client => { + await client.waitForInitialization(); - expect(client.variation('foo')).toEqual('bar'); + expect(client.variation('foo')).toEqual('bar'); + }); }); it('returns value for an existing flag - from polling', async () => { - server.respondWith(jsonResponse({ 'enable-foo': { value: true, version: 1, variation: 2 } })); - - const client = platform.testing.makeClient(envName, user); - await client.waitForInitialization(); - - expect(client.variation('enable-foo', 1)).toEqual(true); + const flags = { 'enable-foo': { value: true, version: 1, variation: 2 } }; + await withServers(async (baseConfig, pollServer) => { + pollServer.byDefault(respondJson(flags)); + await withClient(user, baseConfig, async client => { + await client.waitForInitialization(); + + expect(client.variation('enable-foo', 1)).toEqual(true); + }); + }); }); it('returns default value for flag that had null value', async () => { - server.respondWith(jsonResponse({ 'enable-foo': { value: null, version: 1 } })); - - const client = platform.testing.makeClient(envName, user); - await client.waitForInitialization(); - - expect(client.variation('foo', 'default')).toEqual('default'); + const flags = { 'enable-foo': { value: null, version: 1, variation: 2 } }; + await withServers(async (baseConfig, pollServer) => { + pollServer.byDefault(respondJson(flags)); + await withClient(user, baseConfig, async client => { + await client.waitForInitialization(); + + expect(client.variation('foo', 'default')).toEqual('default'); + }); + }); }); it('returns default value for unknown flag', async () => { - const client = platform.testing.makeClient(envName, user); - await client.waitForInitialization(); + await withServers(async (baseConfig, pollServer) => { + pollServer.byDefault(respondJson({})); + await withClient(user, baseConfig, async client => { + await client.waitForInitialization(); - expect(client.variation('foo', 'default')).toEqual('default'); + expect(client.variation('foo', 'default')).toEqual('default'); + }); + }); }); }); describe('variationDetail', () => { const reason = { kind: 'FALLTHROUGH' }; it('returns details for an existing flag - from bootstrap', async () => { - const client = platform.testing.makeClient(envName, user, { + const config = { bootstrap: makeBootstrap({ foo: { value: 'bar', version: 1, variation: 2, reason: reason } }), - }); - await client.waitForInitialization(); + }; + await withClient(user, config, async client => { + await client.waitForInitialization(); - expect(client.variationDetail('foo')).toEqual({ value: 'bar', variationIndex: 2, reason: reason }); + expect(client.variationDetail('foo')).toEqual({ value: 'bar', variationIndex: 2, reason: reason }); + }); }); it('returns details for an existing flag - from bootstrap with old format', async () => { - const client = platform.testing.makeClient(envName, user, { - bootstrap: { foo: 'bar' }, - }); - await client.waitForInitialization(); + const config = { bootstrap: { foo: 'bar' } }; + await withClient(user, config, async client => { + await client.waitForInitialization(); - expect(client.variationDetail('foo')).toEqual({ value: 'bar', variationIndex: null, reason: null }); + expect(client.variationDetail('foo')).toEqual({ value: 'bar', variationIndex: null, reason: null }); + }); }); it('returns details for an existing flag - from polling', async () => { - const pollData = { foo: { value: 'bar', version: 1, variation: 2, reason: reason } }; - server.respondWith(jsonResponse(pollData)); - - const client = platform.testing.makeClient(envName, user); - await client.waitForInitialization(); - - expect(client.variationDetail('foo', 'default')).toEqual({ value: 'bar', variationIndex: 2, reason: reason }); + const flags = { foo: { value: 'bar', version: 1, variation: 2, reason: reason } }; + await withServers(async (baseConfig, pollServer) => { + pollServer.byDefault(respondJson(flags)); + await withClient(user, baseConfig, async client => { + await client.waitForInitialization(); + + expect(client.variationDetail('foo', 'default')).toEqual({ value: 'bar', variationIndex: 2, reason: reason }); + }); + }); }); it('returns default value for flag that had null value', async () => { - server.respondWith(jsonResponse({ foo: { value: null, version: 1 } })); - - const client = platform.testing.makeClient(envName, user); - await client.waitForInitialization(); - - expect(client.variationDetail('foo', 'default')).toEqual({ - value: 'default', - variationIndex: null, - reason: null, + const flags = { foo: { value: null, version: 1 } }; + await withServers(async (baseConfig, pollServer) => { + pollServer.byDefault(respondJson(flags)); + await withClient(user, baseConfig, async client => { + await client.waitForInitialization(); + + expect(client.variationDetail('foo', 'default')).toEqual({ + value: 'default', + variationIndex: null, + reason: null, + }); + }); }); }); it('returns default value and error for unknown flag', async () => { - const client = platform.testing.makeClient(envName, user); - await client.waitForInitialization(); - - expect(client.variationDetail('foo', 'default')).toEqual({ - value: 'default', - variationIndex: null, - reason: { kind: 'ERROR', errorKind: 'FLAG_NOT_FOUND' }, + await withServers(async (baseConfig, pollServer) => { + pollServer.byDefault(respondJson({})); + await withClient(user, baseConfig, async client => { + expect(client.variationDetail('foo', 'default')).toEqual({ + value: 'default', + variationIndex: null, + reason: { kind: 'ERROR', errorKind: 'FLAG_NOT_FOUND' }, + }); + }); }); }); }); describe('allFlags', () => { it('returns flag values', async () => { - const initData = makeBootstrap({ key1: { value: 'value1' }, key2: { value: 'value2' } }); - const client = platform.testing.makeClient(envName, user, { bootstrap: initData }); - await client.waitForInitialization(); - - expect(client.allFlags()).toEqual({ key1: 'value1', key2: 'value2' }); + const flags = { key1: { value: 'value1' }, key2: { value: 'value2' } }; + await withServers(async (baseConfig, pollServer) => { + pollServer.byDefault(respondJson(flags)); + await withClient(user, baseConfig, async client => { + await client.waitForInitialization(); + + expect(client.allFlags()).toEqual({ key1: 'value1', key2: 'value2' }); + }); + }); }); - it('returns empty map if client is not initialized', () => { - const client = platform.testing.makeClient(envName, user); - expect(client.allFlags()).toEqual({}); + it('returns empty map if client is not initialized', async () => { + const flags = { key1: { value: 'value1' }, key2: { value: 'value2' } }; + await withServers(async (baseConfig, pollServer) => { + pollServer.byDefault(respondJson(flags)); + await withClient(user, baseConfig, async client => { + expect(client.allFlags()).toEqual({}); + }); + }); }); }); describe('identify', () => { it('updates flag values when the user changes', async () => { - const user2 = { key: 'user2' }; - const client = platform.testing.makeClient(envName, user); - await client.waitForInitialization(); - - server.respondWith(jsonResponse({ 'enable-foo': { value: true } })); - - await client.identify(user2); - expect(client.variation('enable-foo')).toEqual(true); - }); - - it('yields map of flag values as the result of identify()', async () => { - const user2 = { key: 'user2' }; - const client = platform.testing.makeClient(envName, user); - await client.waitForInitialization(); - - server.respondWith(jsonResponse({ 'enable-foo': { value: true } })); + const flags0 = { 'enable-foo': { value: false } }; + const flags1 = { 'enable-foo': { value: true } }; + const user1 = { key: 'user1' }; + await withServers(async (baseConfig, pollServer) => { + pollServer.byDefault(respondJson(flags0)); + await withClient(user, baseConfig, async client => { + await client.waitForInitialization(); - const flagMap = await client.identify(user2); - expect(flagMap).toEqual({ 'enable-foo': true }); - }); + expect(client.variation('enable-foo')).toBe(false); - it('returns an error when identify is called with null user', async () => { - const client = platform.testing.makeClient(envName, user); - await client.waitForInitialization(); + pollServer.byDefault(respondJson(flags1)); - await expect(client.identify(null)).rejects.toThrow(); - }); + const newFlagsMap = await client.identify(user1); - it('returns an error when identify is called with user with no key', async () => { - const client = platform.testing.makeClient(envName, user); - await client.waitForInitialization(); + expect(client.variation('enable-foo')).toBe(true); - await expect(client.identify({ country: 'US' })).rejects.toThrow(); + expect(newFlagsMap).toEqual({ 'enable-foo': true }); + }); + }); }); - it('does not change flag values after identify is called with null user', async () => { - const initData = { foo: 'bar' }; - const client = platform.testing.makeClient(envName, user, { bootstrap: initData }); - await client.waitForInitialization(); + it('returns an error and does not update flags when identify is called with invalid user', async () => { + const flags0 = { 'enable-foo': { value: false } }; + const flags1 = { 'enable-foo': { value: true } }; + await withServers(async (baseConfig, pollServer) => { + pollServer.byDefault(respondJson(flags0)); + await withClient(user, baseConfig, async client => { + await client.waitForInitialization(); - expect(client.variation('foo', 'x')).toEqual('bar'); + expect(client.variation('enable-foo')).toBe(false); + expect(pollServer.requests.length()).toEqual(1); - await expect(client.identify(null)).rejects.toThrow(); + pollServer.byDefault(respondJson(flags1)); - expect(client.variation('foo', 'x')).toEqual('bar'); - }); + await expect(client.identify(null)).rejects.toThrow(); - it('does not change flag values after identify is called with invalid user', async () => { - const initData = { foo: 'bar' }; - const client = platform.testing.makeClient(envName, user, { bootstrap: initData }); - await client.waitForInitialization(); + expect(client.variation('enable-foo')).toBe(false); + expect(pollServer.requests.length()).toEqual(1); - expect(client.variation('foo', 'x')).toEqual('bar'); + const userWithNoKey = { country: 'US' }; + await expect(client.identify(userWithNoKey)).rejects.toThrow(); - await expect(client.identify({ country: 'US' })).rejects.toThrow(); - - expect(client.variation('foo', 'x')).toEqual('bar'); + expect(client.variation('enable-foo')).toBe(false); + expect(pollServer.requests.length()).toEqual(1); + }); + }); }); it('provides a persistent key for an anonymous user with no key', async () => { - const initData = { foo: 'bar' }; - const client = platform.testing.makeClient(envName, user, { bootstrap: initData }); - await client.waitForInitialization(); + await withServers(async baseConfig => { + await withClient(user, baseConfig, async client => { + await client.waitForInitialization(); - const anonUser = { anonymous: true, country: 'US' }; - await client.identify(anonUser); + const anonUser = { anonymous: true, country: 'US' }; + await client.identify(anonUser); - const newUser = client.getUser(); - expect(newUser.key).toEqual(expect.anything()); - expect(newUser).toMatchObject(anonUser); + const newUser = client.getUser(); + expect(newUser.key).toEqual(expect.anything()); + expect(newUser).toMatchObject(anonUser); + }); + }); }); }); @@ -480,18 +527,24 @@ describe('LDClient', () => { }; const sp = stubPlatform.mockStateProvider(state); - const client = platform.testing.makeClient(null, null, { stateProvider: sp }); - await client.waitForInitialization(); + await withServers(async (baseConfig, pollServer) => { + await withClient(null, { ...baseConfig, stateProvider: sp }, async client => { + await client.waitForInitialization(); - expect(client.variation('flagkey')).toEqual('value'); - expect(server.requests.length).toEqual(0); + expect(client.variation('flagkey')).toEqual('value'); + expect(pollServer.requests.length()).toEqual(0); + }); + }); }); - it('defers initialization if initial state not available, and does not make an HTTP request', () => { + it('defers initialization if initial state not available, and does not make an HTTP request', async () => { const sp = stubPlatform.mockStateProvider(null); - platform.testing.makeClient(null, null, { stateProvider: sp }); - expect(server.requests.length).toEqual(0); + await withServers(async (baseConfig, pollServer) => { + await withClient(null, { ...baseConfig, stateProvider: sp }, async () => { + expect(pollServer.requests.length()).toEqual(0); + }); + }); }); it('finishes initialization on receiving init event', async () => { @@ -503,12 +556,12 @@ describe('LDClient', () => { }; const sp = stubPlatform.mockStateProvider(null); - const client = platform.testing.makeClient(null, null, { stateProvider: sp }); + await withClient(null, { stateProvider: sp, sendEvents: false }, async client => { + sp.emit('init', state); - sp.emit('init', state); - - await client.waitForInitialization(); - expect(client.variation('flagkey')).toEqual('value'); + await client.waitForInitialization(); + expect(client.variation('flagkey')).toEqual('value'); + }); }); it('updates flags on receiving update event', async () => { @@ -520,22 +573,22 @@ describe('LDClient', () => { }; const sp = stubPlatform.mockStateProvider(state0); - const client = platform.testing.makeClient(null, null, { stateProvider: sp }); - await client.waitForInitialization(); + await withClient(null, { stateProvider: sp, sendEvents: false }, async client => { + await client.waitForInitialization(); - expect(client.variation('flagkey')).toEqual('value0'); + expect(client.variation('flagkey')).toEqual('value0'); - const state1 = { - flags: { flagkey: { value: 'value1' } }, - }; + const state1 = { + flags: { flagkey: { value: 'value1' } }, + }; - const gotChange = promiseListener(); - client.on('change:flagkey', gotChange.callback); + const gotChange = eventSink(client, 'change:flagkey'); - sp.emit('update', state1); + sp.emit('update', state1); - const args = await gotChange; - expect(args).toEqual(['value1', 'value0']); + const args = await gotChange.take(); + expect(args).toEqual(['value1', 'value0']); + }); }); it('disables identify()', async () => { @@ -544,16 +597,18 @@ describe('LDClient', () => { const state = { environment: 'env', user: user, flags: { flagkey: { value: 'value' } } }; const sp = stubPlatform.mockStateProvider(state); - const client = platform.testing.makeClient(null, null, { stateProvider: sp }); + await withServers(async (baseConfig, pollServer) => { + await withClient(null, { ...baseConfig, stateProvider: sp }, async client => { + sp.emit('init', state); - sp.emit('init', state); + await client.waitForInitialization(); + const newFlags = await client.identify(user1); - await client.waitForInitialization(); - const newFlags = await client.identify(user1); - - expect(newFlags).toEqual({ flagkey: 'value' }); - expect(server.requests.length).toEqual(0); - expect(platform.testing.logger.output.warn).toEqual([messages.identifyDisabled()]); + expect(newFlags).toEqual({ flagkey: 'value' }); + expect(pollServer.requests.length()).toEqual(0); + expect(platform.testing.logger.output.warn).toEqual([messages.identifyDisabled()]); + }); + }); }); it('copies data from state provider to avoid unintentional object-sharing', async () => { @@ -565,65 +620,76 @@ describe('LDClient', () => { }; const sp = stubPlatform.mockStateProvider(null); - const client = platform.testing.makeClient(null, null, { stateProvider: sp }); - - sp.emit('init', state); + await withClient(null, { stateProvider: sp, sendEvents: false }, async client => { + sp.emit('init', state); - await client.waitForInitialization(); - expect(client.variation('flagkey')).toEqual('value'); + await client.waitForInitialization(); + expect(client.variation('flagkey')).toEqual('value'); - state.flags.flagkey = { value: 'secondValue' }; - expect(client.variation('flagkey')).toEqual('value'); + state.flags.flagkey = { value: 'secondValue' }; + expect(client.variation('flagkey')).toEqual('value'); - sp.emit('update', state); - expect(client.variation('flagkey')).toEqual('secondValue'); + sp.emit('update', state); + expect(client.variation('flagkey')).toEqual('secondValue'); - state.flags.flagkey = { value: 'thirdValue' }; - expect(client.variation('flagkey')).toEqual('secondValue'); + state.flags.flagkey = { value: 'thirdValue' }; + expect(client.variation('flagkey')).toEqual('secondValue'); + }); }); }); describe('close()', () => { it('flushes events', async () => { - const client = platform.testing.makeClient(envName, user, { bootstrap: {}, flushInterval: 100000 }); - await client.waitForInitialization(); - - await client.close(); - - expect(server.requests.length).toEqual(1); - const data = JSON.parse(server.requests[0].requestBody); - expect(data.length).toEqual(1); - expect(data[0].kind).toEqual('identify'); + await withServers(async (baseConfig, pollServer, eventsServer) => { + await withClient(user, { ...baseConfig, flushInterval: 100000 }, async client => { + await client.waitForInitialization(); + }); + + expect(eventsServer.requests.length()).toEqual(1); + const req = await eventsServer.nextRequest(); + const data = JSON.parse(req.body); + expect(data.length).toEqual(1); + expect(data[0].kind).toEqual('identify'); + }); }); it('does nothing if called twice', async () => { - const client = platform.testing.makeClient(envName, user, { bootstrap: {}, flushInterval: 100000 }); - await client.waitForInitialization(); + await withServers(async (baseConfig, pollServer, eventsServer) => { + await withClient(user, { ...baseConfig, flushInterval: 100000 }, async client => { + await client.waitForInitialization(); - await client.close(); + await client.close(); - expect(server.requests.length).toEqual(1); + expect(eventsServer.requests.length()).toEqual(1); - await client.close(); + await client.close(); - expect(server.requests.length).toEqual(1); + expect(eventsServer.requests.length()).toEqual(1); + }); + }); }); it('is not rejected if flush fails', async () => { - server.respondWith(errorResponse(401)); - const client = platform.testing.makeClient(envName, user, { bootstrap: {}, flushInterval: 100000 }); - await client.waitForInitialization(); + await withServers(async (baseConfig, pollServer, eventsServer) => { + eventsServer.byDefault(respond(404)); + await withClient(user, { ...baseConfig, flushInterval: 100000 }, async client => { + await client.waitForInitialization(); - await client.close(); // shouldn't throw or have an unhandled rejection + await client.close(); // shouldn't throw or have an unhandled rejection + }); + }); }); it('can take a callback instead of returning a promise', async () => { - const client = platform.testing.makeClient(envName, user, { bootstrap: {}, flushInterval: 100000 }); - await client.waitForInitialization(); + await withServers(async (baseConfig, pollServer, eventsServer) => { + await withClient(user, { ...baseConfig }, async client => { + await client.waitForInitialization(); - await asyncify(cb => client.close(cb)); + await promisifySingle(client.close)(); - expect(server.requests.length).toEqual(1); + expect(eventsServer.requests.length()).toEqual(1); + }); + }); }); }); }); diff --git a/src/__tests__/Requestor-test.js b/src/__tests__/Requestor-test.js index 1641e50..5e42f92 100644 --- a/src/__tests__/Requestor-test.js +++ b/src/__tests__/Requestor-test.js @@ -1,255 +1,289 @@ -import * as stubPlatform from './stubPlatform'; -import { errorResponse, jsonResponse, makeDefaultServer } from './testUtils'; import Requestor from '../Requestor'; import * as errors from '../errors'; import * as messages from '../messages'; import * as utils from '../utils'; +import { fakeNetworkErrorValue, networkError, respond, respondJson } from './mockHttp'; +import * as stubPlatform from './stubPlatform'; + +// These tests verify that Requestor executes the expected HTTP requests to retrieve flags. Since +// the js-sdk-common package uses an abstraction of HTTP requests, these tests do not use HTTP but +// rather use a test implementation of our HTTP abstraction; the individual platform-specific SDKs +// are responsible for verifying that their own implementations of the same HTTP abstraction work +// correctly with real networking. + describe('Requestor', () => { - const baseUrl = 'http://requestee'; const user = { key: 'foo' }; const encodedUser = 'eyJrZXkiOiJmb28ifQ'; const env = 'FAKE_ENV'; const platform = stubPlatform.defaults(); - const logger = stubPlatform.logger(); - const defaultConfig = { - baseUrl: baseUrl, - logger: logger, - }; - let server; - - beforeEach(() => { - server = makeDefaultServer(); - }); - afterEach(() => { - server.restore(); - }); + async function withServer(asyncCallback) { + const server = platform.testing.http.newServer(); + server.byDefault(respondJson({})); + const baseConfig = { baseUrl: server.url, logger: stubPlatform.logger() }; + return await asyncCallback(baseConfig, server); + } it('resolves on success', async () => { - const requestor = Requestor(platform, defaultConfig, 'FAKE_ENV'); - await requestor.fetchFlagSettings({ key: 'user1' }, 'hash1'); - await requestor.fetchFlagSettings({ key: 'user2' }, 'hash2'); + await withServer(async (baseConfig, server) => { + const requestor = Requestor(platform, baseConfig, env); + + await requestor.fetchFlagSettings({ key: 'user1' }, 'hash1'); + await requestor.fetchFlagSettings({ key: 'user2' }, 'hash2'); - expect(server.requests).toHaveLength(2); + expect(server.requests.length()).toEqual(2); + }); }); it('makes requests with the GET verb if useReport is disabled', async () => { - const config = { ...defaultConfig, useReport: false }; - const requestor = Requestor(platform, config, env); + await withServer(async (baseConfig, server) => { + const requestor = Requestor(platform, { ...baseConfig, useReport: false }, env); - await requestor.fetchFlagSettings(user, 'hash1'); + await requestor.fetchFlagSettings(user, 'hash1'); - expect(server.requests).toHaveLength(1); - expect(server.requests[0].method).toEqual('GET'); + expect(server.requests.length()).toEqual(1); + const req = await server.requests.take(); + expect(req.method).toEqual('get'); + }); }); it('makes requests with the REPORT verb with a payload if useReport is enabled', async () => { - const config = { ...defaultConfig, useReport: true }; - const requestor = Requestor(platform, config, env); + await withServer(async (baseConfig, server) => { + const requestor = Requestor(platform, { ...baseConfig, useReport: true }, env); - await requestor.fetchFlagSettings(user, 'hash1'); + await requestor.fetchFlagSettings(user, 'hash1'); - expect(server.requests).toHaveLength(1); - expect(server.requests[0].method).toEqual('REPORT'); - expect(server.requests[0].requestBody).toEqual(JSON.stringify(user)); + expect(server.requests.length()).toEqual(1); + const req = await server.requests.take(); + expect(req.method).toEqual('report'); + expect(JSON.parse(req.body)).toEqual(user); + }); }); it('includes environment and user in GET URL', async () => { - const requestor = Requestor(platform, defaultConfig, env); + await withServer(async (baseConfig, server) => { + const requestor = Requestor(platform, baseConfig, env); - await requestor.fetchFlagSettings(user, null); + await requestor.fetchFlagSettings(user, null); - expect(server.requests).toHaveLength(1); - expect(server.requests[0].url).toEqual(`${baseUrl}/sdk/evalx/${env}/users/${encodedUser}`); + expect(server.requests.length()).toEqual(1); + const req = await server.requests.take(); + expect(req.path).toEqual(`/sdk/evalx/${env}/users/${encodedUser}`); + }); }); it('includes environment, user, and hash in GET URL', async () => { - const requestor = Requestor(platform, defaultConfig, env); + await withServer(async (baseConfig, server) => { + const requestor = Requestor(platform, baseConfig, env); - await requestor.fetchFlagSettings(user, 'hash1'); + await requestor.fetchFlagSettings(user, 'hash1'); - expect(server.requests).toHaveLength(1); - expect(server.requests[0].url).toEqual(`${baseUrl}/sdk/evalx/${env}/users/${encodedUser}?h=hash1`); + expect(server.requests.length()).toEqual(1); + const req = await server.requests.take(); + expect(req.path).toEqual(`/sdk/evalx/${env}/users/${encodedUser}?h=hash1`); + }); }); it('includes environment, user, and withReasons in GET URL', async () => { - const config = { ...defaultConfig, evaluationReasons: true }; - const requestor = Requestor(platform, config, env); + await withServer(async (baseConfig, server) => { + const requestor = Requestor(platform, { ...baseConfig, evaluationReasons: true }, env); - await requestor.fetchFlagSettings(user, null); + await requestor.fetchFlagSettings(user, null); - expect(server.requests).toHaveLength(1); - expect(server.requests[0].url).toEqual(`${baseUrl}/sdk/evalx/${env}/users/${encodedUser}?withReasons=true`); + expect(server.requests.length()).toEqual(1); + const req = await server.requests.take(); + expect(req.path).toEqual(`/sdk/evalx/${env}/users/${encodedUser}?withReasons=true`); + }); }); it('includes environment, user, hash, and withReasons in GET URL', async () => { - const config = { ...defaultConfig, evaluationReasons: true }; - const requestor = Requestor(platform, config, env); + await withServer(async (baseConfig, server) => { + const requestor = Requestor(platform, { ...baseConfig, evaluationReasons: true }, env); - await requestor.fetchFlagSettings(user, 'hash1'); + await requestor.fetchFlagSettings(user, 'hash1'); - expect(server.requests).toHaveLength(1); - expect(server.requests[0].url).toEqual(`${baseUrl}/sdk/evalx/${env}/users/${encodedUser}?h=hash1&withReasons=true`); + expect(server.requests.length()).toEqual(1); + const req = await server.requests.take(); + expect(req.path).toEqual(`/sdk/evalx/${env}/users/${encodedUser}?h=hash1&withReasons=true`); + }); }); it('includes environment in REPORT URL', async () => { - const config = { ...defaultConfig, useReport: true }; - const requestor = Requestor(platform, config, env); + await withServer(async (baseConfig, server) => { + const requestor = Requestor(platform, { ...baseConfig, useReport: true }, env); - await requestor.fetchFlagSettings(user, null); + await requestor.fetchFlagSettings(user, null); - expect(server.requests).toHaveLength(1); - expect(server.requests[0].url).toEqual(`${baseUrl}/sdk/evalx/${env}/user`); + expect(server.requests.length()).toEqual(1); + const req = await server.requests.take(); + expect(req.path).toEqual(`/sdk/evalx/${env}/user`); + }); }); it('includes environment and hash in REPORT URL', async () => { - const config = { ...defaultConfig, useReport: true }; - const requestor = Requestor(platform, config, env); + await withServer(async (baseConfig, server) => { + const requestor = Requestor(platform, { ...baseConfig, useReport: true }, env); - await requestor.fetchFlagSettings(user, 'hash1'); + await requestor.fetchFlagSettings(user, 'hash1'); - expect(server.requests).toHaveLength(1); - expect(server.requests[0].url).toEqual(`${baseUrl}/sdk/evalx/${env}/user?h=hash1`); + expect(server.requests.length()).toEqual(1); + const req = await server.requests.take(); + expect(req.path).toEqual(`/sdk/evalx/${env}/user?h=hash1`); + }); }); it('includes environment and withReasons in REPORT URL', async () => { - const config = { ...defaultConfig, useReport: true, evaluationReasons: true }; - const requestor = Requestor(platform, config, env); + await withServer(async (baseConfig, server) => { + const requestor = Requestor(platform, { ...baseConfig, useReport: true, evaluationReasons: true }, env); - await requestor.fetchFlagSettings(user, null); + await requestor.fetchFlagSettings(user, null); - expect(server.requests).toHaveLength(1); - expect(server.requests[0].url).toEqual(`${baseUrl}/sdk/evalx/${env}/user?withReasons=true`); + expect(server.requests.length()).toEqual(1); + const req = await server.requests.take(); + expect(req.path).toEqual(`/sdk/evalx/${env}/user?withReasons=true`); + }); }); it('includes environment, hash, and withReasons in REPORT URL', async () => { - const config = { ...defaultConfig, useReport: true, evaluationReasons: true }; - const requestor = Requestor(platform, config, env); + await withServer(async (baseConfig, server) => { + const requestor = Requestor(platform, { ...baseConfig, useReport: true, evaluationReasons: true }, env); - await requestor.fetchFlagSettings(user, 'hash1'); + await requestor.fetchFlagSettings(user, 'hash1'); - expect(server.requests).toHaveLength(1); - expect(server.requests[0].url).toEqual(`${baseUrl}/sdk/evalx/${env}/user?h=hash1&withReasons=true`); + expect(server.requests.length()).toEqual(1); + const req = await server.requests.take(); + expect(req.path).toEqual(`/sdk/evalx/${env}/user?h=hash1&withReasons=true`); + }); }); it('sends custom user-agent header in GET mode when sendLDHeaders is true', async () => { - const config = { ...defaultConfig, sendLDHeaders: true }; - const requestor = Requestor(platform, config, env); - await requestor.fetchFlagSettings(user, 'hash1'); + await withServer(async (baseConfig, server) => { + const requestor = Requestor(platform, { ...baseConfig, sendLDHeaders: true }, env); - expect(server.requests.length).toEqual(1); - expect(server.requests[0].requestHeaders['X-LaunchDarkly-User-Agent']).toEqual( - utils.getLDUserAgentString(platform) - ); + await requestor.fetchFlagSettings(user); + + expect(server.requests.length()).toEqual(1); + const req = await server.requests.take(); + expect(req.headers['x-launchdarkly-user-agent']).toEqual(utils.getLDUserAgentString(platform)); + }); }); it('sends custom user-agent header in REPORT mode when sendLDHeaders is true', async () => { - const config = { ...defaultConfig, useReport: true, sendLDHeaders: true }; - const requestor = Requestor(platform, config, env); - await requestor.fetchFlagSettings(user, 'hash1'); + await withServer(async (baseConfig, server) => { + const requestor = Requestor(platform, { ...baseConfig, useReport: true, sendLDHeaders: true }, env); + + await requestor.fetchFlagSettings(user, 'hash1'); - expect(server.requests.length).toEqual(1); - expect(server.requests[0].requestHeaders['X-LaunchDarkly-User-Agent']).toEqual( - utils.getLDUserAgentString(platform) - ); + expect(server.requests.length()).toEqual(1); + const req = await server.requests.take(); + expect(req.headers['x-launchdarkly-user-agent']).toEqual(utils.getLDUserAgentString(platform)); + }); }); it('does NOT send custom user-agent header when sendLDHeaders is false', async () => { - const config = { ...defaultConfig, useReport: true, sendLDHeaders: false }; - const requestor = Requestor(platform, config, env); + await withServer(async (baseConfig, server) => { + const requestor = Requestor(platform, { ...baseConfig, sendLDHeaders: false }, env); - await requestor.fetchFlagSettings(user, 'hash1'); + await requestor.fetchFlagSettings(user); - expect(server.requests.length).toEqual(1); - expect(server.requests[0].requestHeaders['X-LaunchDarkly-User-Agent']).toEqual(undefined); + expect(server.requests.length()).toEqual(1); + const req = await server.requests.take(); + expect(req.headers['x-launchdarkly-user-agent']).toBeUndefined(); + }); }); it('returns parsed JSON response on success', async () => { - const requestor = Requestor(platform, defaultConfig, env); - const data = { foo: 'bar' }; - server.respondWith(jsonResponse(data)); + await withServer(async (baseConfig, server) => { + server.byDefault(respondJson(data)); + const requestor = Requestor(platform, baseConfig, env); - const result = await requestor.fetchFlagSettings(user); - expect(result).toEqual(data); + const result = await requestor.fetchFlagSettings(user); + expect(result).toEqual(data); + }); }); it('returns error for non-JSON content type', async () => { - const requestor = Requestor(platform, defaultConfig, env); - - server.respondWith([200, { 'Content-Type': 'text/html' }, '']); + await withServer(async (baseConfig, server) => { + server.byDefault(respond(200, { 'content-type': 'text/plain' }, 'sorry')); + const requestor = Requestor(platform, baseConfig, env); - const err = new errors.LDFlagFetchError(messages.invalidContentType('text/html')); - await expect(requestor.fetchFlagSettings(user)).rejects.toThrow(err); + const err = new errors.LDFlagFetchError(messages.invalidContentType('text/plain')); + await expect(requestor.fetchFlagSettings(user)).rejects.toThrow(err); + }); }); it('returns error for unspecified content type', async () => { - const requestor = Requestor(platform, defaultConfig, env); - - server.respondWith([200, {}, '{}']); + await withServer(async (baseConfig, server) => { + server.byDefault(respond(200, {}, '')); + const requestor = Requestor(platform, baseConfig, env); - const err = new errors.LDFlagFetchError(messages.invalidContentType('')); - await expect(requestor.fetchFlagSettings(user)).rejects.toThrow(err); + const err = new errors.LDFlagFetchError(messages.invalidContentType('')); + await expect(requestor.fetchFlagSettings(user)).rejects.toThrow(err); + }); }); it('signals specific error for 404 response', async () => { - const requestor = Requestor(platform, defaultConfig, env); - - server.respondWith(errorResponse(404)); + await withServer(async (baseConfig, server) => { + server.byDefault(respond(404)); + const requestor = Requestor(platform, baseConfig, env); - const err = new errors.LDInvalidEnvironmentIdError(messages.environmentNotFound()); - await expect(requestor.fetchFlagSettings(user)).rejects.toThrow(err); + const err = new errors.LDInvalidEnvironmentIdError(messages.environmentNotFound()); + await expect(requestor.fetchFlagSettings(user)).rejects.toThrow(err); + }); }); it('signals general error for non-404 error status', async () => { - const requestor = Requestor(platform, defaultConfig, env); + await withServer(async (baseConfig, server) => { + server.byDefault(respond(500)); + const requestor = Requestor(platform, baseConfig, env); - server.respondWith(errorResponse(500)); - - const err = new errors.LDFlagFetchError(messages.errorFetchingFlags('500')); - await expect(requestor.fetchFlagSettings(user)).rejects.toThrow(err); + const err = new errors.LDFlagFetchError(messages.errorFetchingFlags('500')); + await expect(requestor.fetchFlagSettings(user)).rejects.toThrow(err); + }); }); it('signals general error for network error', async () => { - const requestor = Requestor(platform, defaultConfig, env); + await withServer(async (baseConfig, server) => { + server.byDefault(networkError()); + const requestor = Requestor(platform, baseConfig, env); - server.respondWith(req => req.error()); - - const err = new errors.LDFlagFetchError(messages.networkError(new Error())); - await expect(requestor.fetchFlagSettings(user)).rejects.toThrow(err); + const err = new errors.LDFlagFetchError(messages.networkError(fakeNetworkErrorValue)); + await expect(requestor.fetchFlagSettings(user)).rejects.toThrow(err); + }); }); it('coalesces multiple requests so all callers get the latest result', async () => { - const requestor = Requestor(platform, defaultConfig, env); + await withServer(async (baseConfig, server) => { + let n = 0; + server.byDefault((req, res) => { + n++; + respondJson({ value: n })(req, res); + }); - let n = 0; - server.autoRespond = false; - server.respondWith(req => { - n++; - req.respond(...jsonResponse({ value: n })); - }); + const requestor = Requestor(platform, baseConfig, env); - const r1 = requestor.fetchFlagSettings(user); - const r2 = requestor.fetchFlagSettings(user); + const r1 = requestor.fetchFlagSettings(user); + const r2 = requestor.fetchFlagSettings(user); - server.respond(); - server.respond(); - // Note that we should only get a single response, { value: 1 } - Sinon does not call our respondWith - // function for the first request, because it's already been cancelled by the time the server looks - // at the request queue. The important thing is just that both requests get the same value. + const result1 = await r1; + const result2 = await r2; - const result1 = await r1; - const result2 = await r2; + expect(result1).toEqual({ value: 2 }); + expect(result2).toEqual({ value: 2 }); - expect(result1).toEqual({ value: n }); - expect(result2).toEqual({ value: n }); + expect(server.requests.length()).toEqual(2); + }); }); describe('When HTTP requests are not available at all', () => { it('fails on fetchFlagSettings', async () => { - const requestor = Requestor(stubPlatform.withoutHttp(), defaultConfig, env); - await expect(requestor.fetchFlagSettings(user, null)).rejects.toThrow(messages.httpUnavailable()); + await withServer(async (baseConfig, server) => { + const requestor = Requestor(stubPlatform.withoutHttp(), baseConfig, env); + await expect(requestor.fetchFlagSettings(user, null)).rejects.toThrow(messages.httpUnavailable()); + expect(server.requests.length()).toEqual(0); + }); }); }); }); diff --git a/src/__tests__/Stream-test.js b/src/__tests__/Stream-test.js index 394191f..335fdac 100644 --- a/src/__tests__/Stream-test.js +++ b/src/__tests__/Stream-test.js @@ -1,9 +1,10 @@ -import EventSource, { sources, resetSources } from './EventSource-mock'; -import * as stubPlatform from './stubPlatform'; -import { asyncify, asyncSleep } from './testUtils'; import * as messages from '../messages'; import Stream from '../Stream'; +import { sleepAsync } from 'launchdarkly-js-test-helpers'; +import EventSource, { sources, resetSources } from './EventSource-mock'; +import * as stubPlatform from './stubPlatform'; + const noop = () => {}; describe('Stream', () => { @@ -41,13 +42,15 @@ describe('Stream', () => { return sources[keys[0]]; } - function onNewEventSource(f) { - const factory = platform.eventSourceFactory; - platform.eventSourceFactory = (url, options) => { - const es = factory(url, options); - f(es, url, options); - return es; - }; + async function onNewEventSource() { + return new Promise(resolve => { + const factory = platform.eventSourceFactory; + platform.eventSourceFactory = (url, options) => { + const es = factory(url, options); + resolve(es, url, options); + return es; + }; + }); } it('should not throw on EventSource when it does not exist', () => { @@ -138,7 +141,7 @@ describe('Stream', () => { const nAttempts = 5; for (let i = 0; i < nAttempts; i++) { - const newEventSourcePromise = asyncify(onNewEventSource); + const newEventSourcePromise = onNewEventSource(); es.mockError('test error'); const es1 = await newEventSourcePromise; @@ -147,7 +150,7 @@ describe('Stream', () => { expect(es1.readyState).toBe(EventSource.CONNECTING); es1.mockOpen(); - await asyncSleep(0); // make sure the stream logic has a chance to catch up with the new EventSource state + await sleepAsync(0); // make sure the stream logic has a chance to catch up with the new EventSource state expect(stream.isConnected()).toBe(true); @@ -165,7 +168,7 @@ describe('Stream', () => { const nAttempts = 5; for (let i = 0; i < nAttempts; i++) { - const newEventSourcePromise = asyncify(onNewEventSource); + const newEventSourcePromise = onNewEventSource(); es.mockError('test error'); es = await newEventSourcePromise; @@ -189,7 +192,7 @@ describe('Stream', () => { const nAttempts = 5; for (let i = 0; i < nAttempts; i++) { - const newEventSourcePromise = asyncify(onNewEventSource); + const newEventSourcePromise = onNewEventSource(); es.mockError('test error #1'); es = await newEventSourcePromise; @@ -201,7 +204,7 @@ describe('Stream', () => { expect(fakePut).toHaveBeenCalled(); for (let i = 0; i < nAttempts; i++) { - const newEventSourcePromise = asyncify(onNewEventSource); + const newEventSourcePromise = onNewEventSource(); es.mockError('test error #2'); es = await newEventSourcePromise; diff --git a/src/__tests__/mockHttp.js b/src/__tests__/mockHttp.js new file mode 100644 index 0000000..c0d7dc3 --- /dev/null +++ b/src/__tests__/mockHttp.js @@ -0,0 +1,122 @@ +import * as url from 'url'; +import { AsyncQueue } from 'launchdarkly-js-test-helpers'; + +// The js-sdk-common package does not do any HTTP requests itself, because the implementation of +// HTTP is platform-dependent and must be provided by the individual SDKs (e.g. the browser SDK, +// which uses XMLHttpRequest, versus the Electron SDK, which uses Node HTTP). So, for testing +// this package, there is no point in using an HTTP capture tool like Sinon or a real embedded +// HTTP server. Instead we use this simple implementation of the abstraction, which lets us set +// up test handlers with a syntax that imitates our launchdarkly-js-test-helpers HTTP server. + +let lastServerId = 0; + +export function MockHttpState() { + const servers = {}; + + return { + newServer: () => { + lastServerId++; + const hostname = 'mock-server-' + lastServerId; + const server = newMockServer(hostname); + servers[hostname] = server; + return server; + }, + + doRequest: (method, requestUrl, headers, body, synchronous) => { + const urlParams = url.parse(requestUrl); + const server = servers[urlParams.host]; + if (!server) { + return { promise: Promise.reject('unknown host: ' + urlParams.host) }; + } + return server._doRequest(method, urlParams, headers, body, synchronous); + }, + }; +} + +function newMockServer(hostname) { + let defaultHandler = respond(404); + const matchers = []; + const requests = new AsyncQueue(); + + function dispatch(req, resp) { + for (const i in matchers) { + if (matchers[i](req, resp)) { + return; + } + } + defaultHandler(req, resp); + } + + const server = { + url: 'http://' + hostname, + + requests, + + nextRequest: async () => await requests.take(), + + byDefault: handler => { + defaultHandler = handler; + return server; + }, + + forMethodAndPath: (method, path, handler) => { + const matcher = (req, resp) => { + if (req.method === method.toLowerCase() && req.path === path) { + handler(req, resp); + return true; + } + return false; + }; + matchers.push(matcher); + return server; + }, + + close: () => {}, // currently we don't need to clean up the server state + + _doRequest: (method, urlParams, headers, body) => { + const transformedHeaders = {}; + Object.keys(headers || {}).forEach(key => { + transformedHeaders[key.toLowerCase()] = headers[key]; + }); + const req = { + method: method.toLowerCase(), + path: urlParams.path, + headers: transformedHeaders, + body, + }; + requests.add(req); + const ret = {}; + ret.promise = new Promise((resolve, reject) => { + const resp = { resolve, reject }; + dispatch(req, resp); + }); + return ret; + }, + }; + + return server; +} + +export function respond(status, headers, body) { + return (req, resp) => { + const respProps = { + // these are the properties our HTTP abstraction expects + status, + header: name => headers && headers[name.toLowerCase()], + body, + }; + resp.resolve(respProps); + }; +} + +export function respondJson(data) { + return respond(200, { 'content-type': 'application/json' }, JSON.stringify(data)); +} + +export const fakeNetworkErrorValue = new Error('fake network error'); + +export function networkError() { + return (req, resp) => { + resp.reject(fakeNetworkErrorValue); + }; +} diff --git a/src/__tests__/stubPlatform.js b/src/__tests__/stubPlatform.js index 7275a00..8c10557 100644 --- a/src/__tests__/stubPlatform.js +++ b/src/__tests__/stubPlatform.js @@ -1,10 +1,8 @@ -import sinon from 'sinon'; -import EventSource from './EventSource-mock'; import * as LDClient from '../index'; import EventEmitter from '../EventEmitter'; -const sinonXhr = sinon.useFakeXMLHttpRequest(); -sinonXhr.restore(); +import EventSource from './EventSource-mock'; +import { MockHttpState } from './mockHttp'; // This file provides a stub implementation of the internal platform API for use in tests. // @@ -30,11 +28,12 @@ sinonXhr.restore(); export function defaults() { const localStore = {}; + const mockHttpState = MockHttpState(); let currentUrl = null; let doNotTrack = false; const p = { - httpRequest: newHttpRequest, + httpRequest: mockHttpState.doRequest, httpAllowsPost: () => true, httpAllowsSync: () => true, getCurrentUrl: () => currentUrl, @@ -67,6 +66,8 @@ export function defaults() { testing: { logger: logger(), + http: mockHttpState, + makeClient: (env, user, options = {}) => { const config = { logger: p.testing.logger, ...options }; return LDClient.initialize(env, user, config, p).client; @@ -113,52 +114,3 @@ export function mockStateProvider(initialState) { sp.getInitialState = () => initialState; return sp; } - -// This HTTP implementation is basically the same one that's used in the browser client, but it's -// made to interact with Sinon, so that the tests can use the familiar Sinon API. -// -// It'd be nice to be able to reuse this same logic in the browser client instead of copying it, -// but it's not of any use in Node or Electron so it doesn't really belong in the common package. - -function newHttpRequest(method, url, headers, body, synchronous) { - const xhr = new sinonXhr(); - xhr.open(method, url, !synchronous); - for (const key in headers || {}) { - if (headers.hasOwnProperty(key)) { - xhr.setRequestHeader(key, headers[key]); - } - } - if (synchronous) { - const p = new Promise(resolve => { - xhr.send(body); - resolve(); - }); - return { promise: p }; - } else { - let cancelled; - const p = new Promise((resolve, reject) => { - xhr.addEventListener('load', () => { - if (cancelled) { - return; - } - resolve({ - status: xhr.status, - header: key => xhr.getResponseHeader(key), - body: xhr.responseText, - }); - }); - xhr.addEventListener('error', () => { - if (cancelled) { - return; - } - reject(new Error()); - }); - xhr.send(body); - }); - const cancel = () => { - cancelled = true; - xhr.abort(); - }; - return { promise: p, cancel: cancel }; - } -} diff --git a/src/__tests__/testUtils.js b/src/__tests__/testUtils.js index e4a944d..b86fc55 100644 --- a/src/__tests__/testUtils.js +++ b/src/__tests__/testUtils.js @@ -1,31 +1,3 @@ -import sinon from 'sinon'; - -export function asyncSleep(delay) { - return new Promise(resolve => { - setTimeout(resolve, delay); - }); -} - -export function asyncify(f) { - return new Promise(resolve => f(resolve)); -} - -export function errorResponse(status) { - return [status, {}, '']; -} - -export function jsonResponse(data) { - return [200, { 'Content-Type': 'application/json' }, JSON.stringify(data)]; -} - -export function makeDefaultServer() { - const server = sinon.createFakeServer(); - server.autoRespond = true; - server.autoRespondAfter = 0; - server.respondWith(jsonResponse({})); // default 200 response for tests that don't specify otherwise - return server; -} - export const numericUser = { key: 1, secondary: 2,