Browse Source

Merge pull request #199 from vendure-ecommerce/testing-tools

This merge adds a new package, `@vendure/testing` which provides tools to ease e2e testing of custom plugins. Closes #198
Michael Bromley 6 years ago
parent
commit
610eb1ffab
90 changed files with 2092 additions and 1628 deletions
  1. 5 3
      .github/workflows/build_and_test.yml
  2. 2 2
      .lintstagedrc.json
  3. 6 0
      docs/assets/styles/_markdown.scss
  4. 101 0
      docs/content/docs/developer-guide/testing.md
  5. 7 14
      package.json
  6. 1 0
      packages/admin-ui-plugin/package.json
  7. 2 1
      packages/admin-ui/package.json
  8. 2 1
      packages/asset-server-plugin/package.json
  9. 3 1
      packages/common/package.json
  10. 20 1
      packages/common/src/shared-utils.spec.ts
  11. 12 0
      packages/common/src/shared-utils.ts
  12. 72 0
      packages/common/src/simple-deep-clone.spec.ts
  13. 6 1
      packages/common/src/simple-deep-clone.ts
  14. 21 14
      packages/core/e2e/administrator.e2e-spec.ts
  15. 30 29
      packages/core/e2e/auth.e2e-spec.ts
  16. 106 98
      packages/core/e2e/collection.e2e-spec.ts
  17. 6 51
      packages/core/e2e/config/test-config.ts
  18. 36 23
      packages/core/e2e/country.e2e-spec.ts
  19. 95 94
      packages/core/e2e/custom-fields.e2e-spec.ts
  20. 37 37
      packages/core/e2e/customer.e2e-spec.ts
  21. 16 21
      packages/core/e2e/default-search-plugin.e2e-spec.ts
  22. 7 8
      packages/core/e2e/entity-id-strategy.e2e-spec.ts
  23. 15 16
      packages/core/e2e/entity-uuid-strategy.e2e-spec.ts
  24. 50 39
      packages/core/e2e/facet.e2e-spec.ts
  25. 126 0
      packages/core/e2e/fixtures/test-payment-methods.ts
  26. 31 20
      packages/core/e2e/import.e2e-spec.ts
  27. 27 24
      packages/core/e2e/localization.e2e-spec.ts
  28. 24 30
      packages/core/e2e/order-promotion.e2e-spec.ts
  29. 30 104
      packages/core/e2e/order.e2e-spec.ts
  30. 27 30
      packages/core/e2e/plugin.e2e-spec.ts
  31. 17 13
      packages/core/e2e/product-option.e2e-spec.ts
  32. 8 9
      packages/core/e2e/product.e2e-spec.ts
  33. 40 40
      packages/core/e2e/promotion.e2e-spec.ts
  34. 37 28
      packages/core/e2e/role.e2e-spec.ts
  35. 41 43
      packages/core/e2e/shipping-method.e2e-spec.ts
  36. 95 103
      packages/core/e2e/shop-auth.e2e-spec.ts
  37. 9 11
      packages/core/e2e/shop-catalog.e2e-spec.ts
  38. 8 9
      packages/core/e2e/shop-customer.e2e-spec.ts
  39. 30 63
      packages/core/e2e/shop-order.e2e-spec.ts
  40. 20 41
      packages/core/e2e/stock-control.e2e-spec.ts
  41. 0 34
      packages/core/e2e/test-client.ts
  42. 3 2
      packages/core/e2e/utils/await-running-jobs.ts
  43. 0 9
      packages/core/e2e/utils/test-environment.ts
  44. 6 23
      packages/core/e2e/utils/test-order-utils.ts
  45. 32 25
      packages/core/e2e/zone.e2e-spec.ts
  46. 0 14
      packages/core/mock-data/populate-customers.ts
  47. 5 2
      packages/core/package.json
  48. 0 5
      packages/core/src/common/types/common-types.ts
  49. 2 4
      packages/core/src/config/config-helpers.ts
  50. 0 1
      packages/core/src/config/default-config.ts
  51. 2 0
      packages/core/src/config/index.ts
  52. 21 7
      packages/core/src/config/merge-config.spec.ts
  53. 25 20
      packages/core/src/config/merge-config.ts
  54. 14 2
      packages/core/src/config/vendure-config.ts
  55. 0 50
      packages/core/src/data-import/import-cli.ts
  56. 0 4
      packages/core/typings.d.ts
  57. 3 1
      packages/create/package.json
  58. 1 0
      packages/dev-server/.gitignore
  59. 0 1
      packages/dev-server/load-testing/graphql/shop/deep-query.graphql
  60. 14 5
      packages/dev-server/load-testing/init-load-test.ts
  61. 29 8
      packages/dev-server/load-testing/load-test-config.ts
  62. 0 0
      packages/dev-server/load-testing/static/.gitkeep
  63. 1 0
      packages/dev-server/package.json
  64. 21 19
      packages/dev-server/populate-dev-server.ts
  65. 3 1
      packages/elasticsearch-plugin/package.json
  66. 3 1
      packages/email-plugin/package.json
  67. 1 0
      packages/testing/.gitignore
  68. 5 0
      packages/testing/README.md
  69. 47 0
      packages/testing/package.json
  70. 63 0
      packages/testing/src/config/test-config.ts
  71. 1 1
      packages/testing/src/config/testing-asset-preview-strategy.ts
  72. 1 2
      packages/testing/src/config/testing-asset-storage-strategy.ts
  73. 1 1
      packages/testing/src/config/testing-entity-id-strategy.ts
  74. 75 0
      packages/testing/src/create-test-environment.ts
  75. 4 8
      packages/testing/src/data-population/clear-all-tables.ts
  76. 3 5
      packages/testing/src/data-population/mock-data.service.ts
  77. 23 0
      packages/testing/src/data-population/populate-customers.ts
  78. 12 22
      packages/testing/src/data-population/populate-for-testing.ts
  79. 6 0
      packages/testing/src/index.ts
  80. 37 26
      packages/testing/src/simple-graphql-client.ts
  81. 53 38
      packages/testing/src/test-server.ts
  82. 45 0
      packages/testing/src/types.ts
  83. 6 4
      packages/testing/src/utils/create-upload-post-data.spec.ts
  84. 0 0
      packages/testing/src/utils/create-upload-post-data.ts
  85. 1 2
      packages/testing/src/utils/get-default-channel-token.ts
  86. 12 0
      packages/testing/tsconfig.build.json
  87. 10 0
      packages/testing/tsconfig.json
  88. 1 0
      scripts/docs/generate-typescript-docs.ts
  89. 1 0
      scripts/publish-to-verdaccio.sh
  90. 271 259
      yarn.lock

+ 5 - 3
.github/workflows/build_and_test.yml

@@ -27,8 +27,10 @@ jobs:
       run: |
       run: |
         yarn install
         yarn install
         yarn bootstrap
         yarn bootstrap
-        yarn lerna run build
+        yarn build
       env:
       env:
         CI: true
         CI: true
-    - name: Test
-      run: yarn test:all
+    - name: Unit tests
+      run: yarn test
+    - name: e2e tests
+      run: yarn e2e

+ 2 - 2
.lintstagedrc.json

@@ -1,6 +1,6 @@
 {
 {
   "admin-ui/**/*.ts": [
   "admin-ui/**/*.ts": [
-    "yarn lint:admin-ui",
+    "yarn lint",
     "yarn format",
     "yarn format",
     "git add"
     "git add"
   ],
   ],
@@ -9,7 +9,7 @@
     "git add"
     "git add"
   ],
   ],
   "packages/**/*.ts": [
   "packages/**/*.ts": [
-    "yarn lint:packages",
+    "yarn lint",
     "yarn format",
     "yarn format",
     "git add"
     "git add"
   ]
   ]

+ 6 - 0
docs/assets/styles/_markdown.scss

@@ -96,6 +96,12 @@ $block-border-radius: 4px;
         color: $color-code-text;
         color: $color-code-text;
         border-radius: $block-border-radius;
         border-radius: $block-border-radius;
         border: 1px solid $color-code-border;
         border: 1px solid $color-code-border;
+
+
+    }
+
+    a > code:not([data-lang]) {
+        color: $color-link;
     }
     }
 
 
     pre:not(.chroma) {
     pre:not(.chroma) {

+ 101 - 0
docs/content/docs/developer-guide/testing.md

@@ -0,0 +1,101 @@
+---
+title: "Testing"
+showtoc: true
+---
+
+# Testing
+
+Vendure plugins allow you to extend all aspects of the standard Vendure server. When a plugin gets somewhat complex (defining new entities, extending the GraphQL schema, implementing custom resolvers), you may wish to create automated tests to ensure your plugin is correct.
+
+The `@vendure/testing` package gives you some simple but powerful tooling for creating end-to-end tests for your custom Vendure code.
+
+## Usage
+
+### Install dependencies
+
+* [`@vendure/testing`](https://www.npmjs.com/package/@vendure/testing)
+* [`jest`](https://www.npmjs.com/package/jest) You'll need to install a testing framework. In this example we will use [Jest](https://jestjs.io/), but any other framework such as Jasmine should work too.
+* [`graphql-tag`](https://www.npmjs.com/package/graphql-tag) This is not strictly required but makes it much easier to create the DocumentNodes needed to query your server.
+
+Please see the [Jest documentation](https://jestjs.io/docs/en/getting-started) on how to get set up. The remainder of this article will assume a working Jest setup configured to work with TypeScript.
+
+### Create a test environment
+
+The `@vendure/testing` package exports a [`createTestEnvironment` function]({{< relref "create-test-environment" >}}) which is used to set up a Vendure server and GraphQL clients to interact with both the Shop and Admin APIs:
+
+```TypeScript
+import { createTestEnvironment, testConfig } from '@vendure/testing';
+import { MyPlugin } from '../my-plugin.ts';
+
+describe('my plugin', () => {
+
+    const { server, adminClient, shopClient } = createTestEnvironment({
+        ...testConfig,
+        plugins: [MyPlugin],
+    });
+
+});
+```
+
+Notice that we pass a [`VendureConfig`]({{< relref "vendure-config" >}}) object into the `createTestEnvironment` function. The testing package provides a special [`testConfig`]({{< relref "test-config" >}}) which is pre-configured for e2e tests, but any aspect can be overridden for your tests. Here we are configuring the server to load the plugin under test, `MyPlugin`. 
+
+{{% alert "warning" %}}
+**Note**: If you need to deeply merge in some custom configuration, use the [`mergeConfig` function]({{< relref "merge-config" >}}) which is provided by `@vendure/core`.
+{{% /alert %}}
+
+### Initialize the server
+
+The [`TestServer`]({{< relref "test-server" >}}) needs to be initialized before it can be used. The `TestServer.init()` method takes an options object which defines how to populate the server:
+
+```TypeScript
+import { myInitialData } from './fixtures/my-initial-data.ts';
+
+// ...
+
+beforeAll(async () => {
+    await server.init({
+        productsCsvPath: path.join(__dirname, 'fixtures/e2e-products.csv'),
+        initialData: myInitialData,
+        dataDir: path.join(__dirname, '__data__'),
+        customerCount: 2,
+    });
+    await adminClient.asSuperAdmin();
+}, 60000);
+
+afterAll(async () => {
+    await server.destroy();
+});
+```
+
+An explanation of the options:
+
+* `productsCsvPath` This is a path to a CSV file containing product data. The format is as-yet undocumented and may be subject to change, but you can see [an example used in the Vendure e2e tests](https://github.com/vendure-ecommerce/vendure/blob/master/packages/core/e2e/fixtures/e2e-products-full.csv) to get an idea of how it works. To start with you can just copy this file directly and use it as-is.
+* `initialData` This is an object which defines how other non-product data (Collections, ShippingMethods, Countries etc.) is populated. Again, the best idea is to [copy this example from the Vendure e2e tests](https://github.com/vendure-ecommerce/vendure/blob/master/packages/core/e2e/fixtures/e2e-initial-data.ts)
+* `dataDir` The first time this test suite is run, the data populated by the options above will be saved into an SQLite file, stored in the directory specified by this options. On subsequent runs of the test suite, the data-population step will be skipped and the data directly loaded from the SQLite file. This method of caching significantly speeds up the e2e test runs. All the .sqlite files created in the `dataDir` can safely be deleted at any time.
+* `customerCount` Specifies the number of fake Customers to create. Defaults to 10 if not specified.
+
+### Write your tests
+
+Now we are all set up to create a test. Let's test one of the GraphQL queries used by our fictional plugin:
+
+```TypeScript
+import gql from 'graphql-tag';
+
+it('myNewQuery returns the expected result', async () => {
+    adminClient.asSuperAdmin(); // log in as the SuperAdmin user
+
+    const query = gql`
+        query MyNewQuery($id: ID!) {
+            myNewQuery(id: $id) {
+                field1
+                field2
+            }
+        }
+    `;
+    const result = await adminClient.query(query, { id: 123 });
+
+    expect(result.myNewQuery).toEqual({ /* ... */ })
+});
+```
+
+Running the test will then assert that your new query works as expected.

+ 7 - 14
package.json

@@ -6,7 +6,9 @@
     "node": ">= 10.12.0 < 11 || >= 12.00"
     "node": ">= 10.12.0 < 11 || >= 12.00"
   },
   },
   "scripts": {
   "scripts": {
-    "core:watch": "concurrently -n tsc,gulp,common \"cd packages/core && yarn tsc:watch\" \"cd packages/core && yarn gulp:watch\" \"cd packages/common && yarn watch\"",
+    "watch": "lerna run watch --parallel",
+    "lint": "yarn tslint --fix",
+    "format": "prettier --write --html-whitespace-sensitivity ignore",
     "bootstrap": "lerna bootstrap",
     "bootstrap": "lerna bootstrap",
     "docs:watch": "concurrently --restart-tries 5 -n docgen,hugo,webpack -c green,blue,cyan \"yarn generate-graphql-docs && yarn generate-typescript-docs -w\" \"cd docs && hugo server\" \"cd docs && yarn webpack -w\"",
     "docs:watch": "concurrently --restart-tries 5 -n docgen,hugo,webpack -c green,blue,cyan \"yarn generate-graphql-docs && yarn generate-typescript-docs -w\" \"cd docs && hugo server\" \"cd docs && yarn webpack -w\"",
     "docs:build": "yarn generate-graphql-docs && yarn generate-typescript-docs && cd docs && yarn webpack --prod && node build.js && hugo",
     "docs:build": "yarn generate-graphql-docs && yarn generate-typescript-docs && cd docs && yarn webpack --prod && node build.js && hugo",
@@ -14,19 +16,10 @@
     "codegen": "ts-node scripts/codegen/generate-graphql-types.ts",
     "codegen": "ts-node scripts/codegen/generate-graphql-types.ts",
     "generate-typescript-docs": "ts-node scripts/docs/generate-typescript-docs.ts",
     "generate-typescript-docs": "ts-node scripts/docs/generate-typescript-docs.ts",
     "generate-graphql-docs": "ts-node scripts/docs/generate-graphql-docs.ts --api=shop && ts-node scripts/docs/generate-graphql-docs.ts --api=admin",
     "generate-graphql-docs": "ts-node scripts/docs/generate-graphql-docs.ts --api=shop && ts-node scripts/docs/generate-graphql-docs.ts --api=admin",
-    "format": "prettier --write --html-whitespace-sensitivity ignore",
-    "lint:packages": "yarn tslint --fix",
-    "lint:admin-ui": "cd packages/admin-ui && yarn lint --fix",
     "version": "yarn check-imports && yarn build && yarn generate-changelog && git add CHANGELOG.md",
     "version": "yarn check-imports && yarn build && yarn generate-changelog && git add CHANGELOG.md",
     "dev-server:start": "cd packages/dev-server && yarn start",
     "dev-server:start": "cd packages/dev-server && yarn start",
-    "dev-server:populate": "cd packages/dev-server && yarn populate",
-    "test:all": "yarn test:admin-ui && yarn test:common && yarn test:core && yarn test:email-plugin && yarn test:elasticsearch-plugin && yarn test:e2e",
-    "test:common": "jest --config packages/common/jest.config.js",
-    "test:core": "jest --config packages/core/jest.config.js",
-    "test:email-plugin": "jest --config packages/email-plugin/jest.config.js --maxWorkers=1",
-    "test:elasticsearch-plugin": "jest --config packages/elasticsearch-plugin/jest.config.js",
-    "test:e2e": "jest --config packages/core/e2e/config/jest-e2e.json --runInBand",
-    "test:admin-ui": "cd packages/admin-ui && yarn test --watch=false --browsers=ChromeHeadlessCI --progress=false",
+    "test": "lerna run test --stream --no-bail",
+    "e2e": "lerna run e2e --stream --no-bail",
     "build": "lerna run build",
     "build": "lerna run build",
     "check-imports": "ts-node scripts/check-imports.ts",
     "check-imports": "ts-node scripts/check-imports.ts",
     "generate-changelog": "ts-node scripts/changelogs/generate-changelog.ts",
     "generate-changelog": "ts-node scripts/changelogs/generate-changelog.ts",
@@ -53,7 +46,7 @@
     "husky": "^3.0.0",
     "husky": "^3.0.0",
     "jest": "^24.5.0",
     "jest": "^24.5.0",
     "klaw-sync": "^6.0.0",
     "klaw-sync": "^6.0.0",
-    "lerna": "^3.14.1",
+    "lerna": "^3.16.4",
     "lint-staged": "^9.2.0",
     "lint-staged": "^9.2.0",
     "prettier": "^1.15.2",
     "prettier": "^1.15.2",
     "ts-jest": "^24.1.0",
     "ts-jest": "^24.1.0",
@@ -89,7 +82,7 @@
       "commit-msg": "commitlint -e $HUSKY_GIT_PARAMS",
       "commit-msg": "commitlint -e $HUSKY_GIT_PARAMS",
       "post-commit": "git update-index --again",
       "post-commit": "git update-index --again",
       "pre-commit": "lint-staged",
       "pre-commit": "lint-staged",
-      "pre-push": "yarn test:all && cd packages/admin-ui && yarn build"
+      "pre-push": "yarn build && yarn test && yarn e2e"
     }
     }
   }
   }
 }
 }

+ 1 - 0
packages/admin-ui-plugin/package.json

@@ -10,6 +10,7 @@
   "scripts": {
   "scripts": {
     "build": "rimraf lib && node -r ts-node/register build.ts && yarn compile",
     "build": "rimraf lib && node -r ts-node/register build.ts && yarn compile",
     "watch": "tsc -p ./tsconfig.build.json --watch",
     "watch": "tsc -p ./tsconfig.build.json --watch",
+    "lint": "tslint --fix --project ./",
     "compile": "tsc -p ./tsconfig.build.json"
     "compile": "tsc -p ./tsconfig.build.json"
   },
   },
   "publishConfig": {
   "publishConfig": {

+ 2 - 1
packages/admin-ui/package.json

@@ -5,8 +5,9 @@
     "ng": "ng",
     "ng": "ng",
     "start": "ng serve",
     "start": "ng serve",
     "build": "yarn reset-extensions && ng build --prod && yarn build:devkit",
     "build": "yarn reset-extensions && ng build --prod && yarn build:devkit",
+    "watch": "ng build --watch=true",
     "build:devkit": "tsc -p tsconfig.devkit.json",
     "build:devkit": "tsc -p tsconfig.devkit.json",
-    "test": "ng test",
+    "test": "ng test --watch=false --browsers=ChromeHeadlessCI --progress=false",
     "lint": "tslint --fix",
     "lint": "tslint --fix",
     "reset-extensions": "rimraf ./src/app/extensions/modules && rimraf ./src/app/extensions/*.generated && rimraf ./src/app/extensions/*.temp",
     "reset-extensions": "rimraf ./src/app/extensions/modules && rimraf ./src/app/extensions/*.generated && rimraf ./src/app/extensions/*.temp",
     "extract-translations": "ngx-translate-extract --input ./src --output ./src/i18n-messages/en.json --clean --sort --format namespaced-json --format-indentation \"  \" -m _"
     "extract-translations": "ngx-translate-extract --input ./src --output ./src/i18n-messages/en.json --clean --sort --format namespaced-json --format-indentation \"  \" -m _"

+ 2 - 1
packages/asset-server-plugin/package.json

@@ -9,7 +9,8 @@
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "watch": "tsc -p ./tsconfig.build.json --watch",
     "watch": "tsc -p ./tsconfig.build.json --watch",
-    "build": "rimraf lib && tsc -p ./tsconfig.build.json"
+    "build": "rimraf lib && tsc -p ./tsconfig.build.json",
+    "lint": "tslint --fix --project ./"
   },
   },
   "publishConfig": {
   "publishConfig": {
     "access": "public"
     "access": "public"

+ 3 - 1
packages/common/package.json

@@ -5,7 +5,9 @@
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "watch": "tsc -p ./tsconfig.build.json -w",
     "watch": "tsc -p ./tsconfig.build.json -w",
-    "build": "rimraf dist && tsc -p ./tsconfig.build.json"
+    "build": "rimraf dist && tsc -p ./tsconfig.build.json",
+    "lint": "tslint --fix --project ./",
+    "test": "jest --config ./jest.config.js"
   },
   },
   "publishConfig": {
   "publishConfig": {
     "access": "public"
     "access": "public"

+ 20 - 1
packages/common/src/shared-utils.spec.ts

@@ -1,4 +1,4 @@
-import { generateAllCombinations } from './shared-utils';
+import { generateAllCombinations, isClassInstance } from './shared-utils';
 
 
 describe('generateAllCombinations()', () => {
 describe('generateAllCombinations()', () => {
     it('works with an empty input array', () => {
     it('works with an empty input array', () => {
@@ -28,3 +28,22 @@ describe('generateAllCombinations()', () => {
         expect(result).toEqual([['red'], ['green'], ['blue']]);
         expect(result).toEqual([['red'], ['green'], ['blue']]);
     });
     });
 });
 });
+
+describe('isClassInstance()', () => {
+    it('returns true for class instances', () => {
+        expect(isClassInstance(new Date())).toBe(true);
+        expect(isClassInstance(new Foo())).toBe(true);
+        // tslint:disable-next-line:no-construct
+        expect(isClassInstance(new Number(1))).toBe(true);
+    });
+
+    it('returns false for not class instances', () => {
+        expect(isClassInstance(Date)).toBe(false);
+        expect(isClassInstance(1)).toBe(false);
+        expect(isClassInstance(Number)).toBe(false);
+        expect(isClassInstance({ a: 1 })).toBe(false);
+        expect(isClassInstance([1, 2, 3])).toBe(false);
+    });
+
+    class Foo {}
+});

+ 12 - 0
packages/common/src/shared-utils.ts

@@ -13,6 +13,18 @@ export function assertNever(value: never): never {
     throw new Error(`Expected never, got ${typeof value}`);
     throw new Error(`Expected never, got ${typeof value}`);
 }
 }
 
 
+/**
+ * Simple object check.
+ * From https://stackoverflow.com/a/34749873/772859
+ */
+export function isObject(item: any): item is object {
+    return item && typeof item === 'object' && !Array.isArray(item);
+}
+
+export function isClassInstance(item: any): boolean {
+    return isObject(item) && item.constructor.name !== 'Object';
+}
+
 /**
 /**
  * Given an array of option arrays `[['red, 'blue'], ['small', 'large']]`, this method returns a new array
  * Given an array of option arrays `[['red, 'blue'], ['small', 'large']]`, this method returns a new array
  * containing all the combinations of those options:
  * containing all the combinations of those options:

+ 72 - 0
packages/common/src/simple-deep-clone.spec.ts

@@ -0,0 +1,72 @@
+import { simpleDeepClone } from './simple-deep-clone';
+
+describe('simpleDeepClone()', () => {
+    it('clones a simple flat object', () => {
+        const target = { a: 1, b: 2 };
+        const result = simpleDeepClone(target);
+
+        expect(result).toEqual(target);
+        expect(result).not.toBe(target);
+    });
+
+    it('clones a simple deep object', () => {
+        const target = { a: 1, b: { c: 3, d: [1, 2, 3] } };
+        const result = simpleDeepClone(target);
+
+        expect(result).toEqual(target);
+        expect(result).not.toBe(target);
+    });
+
+    it('clones a simple flat array', () => {
+        const target = [1, 2, 3];
+        const result = simpleDeepClone(target);
+
+        expect(result).toEqual(target);
+        expect(result).not.toBe(target);
+    });
+
+    it('clones a simple deep array', () => {
+        const target = [1, [2, 3], [4, [5, [6]]]];
+        const result = simpleDeepClone(target);
+
+        expect(result).toEqual(target);
+        expect(result).not.toBe(target);
+    });
+
+    it('passes through primitive types', () => {
+        expect(simpleDeepClone(1)).toBe(1);
+        expect(simpleDeepClone('a')).toBe('a');
+        expect(simpleDeepClone(true as any)).toBe(true);
+        expect(simpleDeepClone(null as any)).toBe(null);
+        expect(simpleDeepClone(undefined as any)).toBe(undefined);
+    });
+
+    it('does not clone class instance', () => {
+        const target = new Foo();
+        const result = simpleDeepClone(target);
+
+        expect(result).toBe(target);
+    });
+
+    it('does not clone class instance in array', () => {
+        const foo = new Foo();
+        const target = [foo];
+        const result = simpleDeepClone(target);
+
+        expect(result).toEqual(target);
+        expect(result).not.toBe(target);
+        expect(result[0]).toBe(target[0]);
+    });
+
+    it('does not clone class instance in object', () => {
+        const foo = new Foo();
+        const target = { a: foo };
+        const result = simpleDeepClone(target);
+
+        expect(result).toEqual(target);
+        expect(result).not.toBe(target);
+        expect(result.a).toBe(target.a);
+    });
+});
+
+class Foo {}

+ 6 - 1
packages/common/src/simple-deep-clone.ts

@@ -1,3 +1,5 @@
+import { isClassInstance } from './shared-utils';
+
 /**
 /**
  * An extremely fast function for deep-cloning an object which only contains simple
  * An extremely fast function for deep-cloning an object which only contains simple
  * values, i.e. primitives, arrays and nested simple objects.
  * values, i.e. primitives, arrays and nested simple objects.
@@ -18,10 +20,13 @@ export function simpleDeepClone<T extends string | number | any[] | object>(inpu
         }
         }
         return output;
         return output;
     }
     }
+    if (isClassInstance(input)) {
+        return input;
+    }
     // handle case: object
     // handle case: object
     output = {};
     output = {};
     for (i in input) {
     for (i in input) {
-        if ((input).hasOwnProperty(i)) {
+        if (input.hasOwnProperty(i)) {
             output[i] = simpleDeepClone((input as any)[i]);
             output[i] = simpleDeepClone((input as any)[i]);
         }
         }
     }
     }

+ 21 - 14
packages/core/e2e/administrator.e2e-spec.ts

@@ -1,25 +1,32 @@
+import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
-import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
+import { initialData } from './fixtures/e2e-initial-data';
 import { ADMINISTRATOR_FRAGMENT } from './graphql/fragments';
 import { ADMINISTRATOR_FRAGMENT } from './graphql/fragments';
-import { Administrator, CreateAdministrator, GetAdministrator, GetAdministrators, UpdateAdministrator } from './graphql/generated-e2e-admin-types';
+import {
+    Administrator,
+    CreateAdministrator,
+    GetAdministrator,
+    GetAdministrators,
+    UpdateAdministrator,
+} from './graphql/generated-e2e-admin-types';
 import { CREATE_ADMINISTRATOR } from './graphql/shared-definitions';
 import { CREATE_ADMINISTRATOR } from './graphql/shared-definitions';
-import { TestAdminClient } from './test-client';
-import { TestServer } from './test-server';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 
 
 describe('Administrator resolver', () => {
 describe('Administrator resolver', () => {
-    const client = new TestAdminClient();
-    const server = new TestServer();
+    const { server, adminClient } = createTestEnvironment(testConfig);
     let createdAdmin: Administrator.Fragment;
     let createdAdmin: Administrator.Fragment;
 
 
     beforeAll(async () => {
     beforeAll(async () => {
-        const token = await server.init({
+        await server.init({
+            dataDir,
+            initialData,
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
             customerCount: 1,
             customerCount: 1,
         });
         });
-        await client.init();
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
     }, TEST_SETUP_TIMEOUT_MS);
 
 
     afterAll(async () => {
     afterAll(async () => {
@@ -27,7 +34,7 @@ describe('Administrator resolver', () => {
     });
     });
 
 
     it('administrators', async () => {
     it('administrators', async () => {
-        const result = await client.query<GetAdministrators.Query, GetAdministrators.Variables>(
+        const result = await adminClient.query<GetAdministrators.Query, GetAdministrators.Variables>(
             GET_ADMINISTRATORS,
             GET_ADMINISTRATORS,
         );
         );
         expect(result.administrators.items.length).toBe(1);
         expect(result.administrators.items.length).toBe(1);
@@ -35,7 +42,7 @@ describe('Administrator resolver', () => {
     });
     });
 
 
     it('createAdministrator', async () => {
     it('createAdministrator', async () => {
-        const result = await client.query<CreateAdministrator.Mutation, CreateAdministrator.Variables>(
+        const result = await adminClient.query<CreateAdministrator.Mutation, CreateAdministrator.Variables>(
             CREATE_ADMINISTRATOR,
             CREATE_ADMINISTRATOR,
             {
             {
                 input: {
                 input: {
@@ -53,7 +60,7 @@ describe('Administrator resolver', () => {
     });
     });
 
 
     it('administrator', async () => {
     it('administrator', async () => {
-        const result = await client.query<GetAdministrator.Query, GetAdministrator.Variables>(
+        const result = await adminClient.query<GetAdministrator.Query, GetAdministrator.Variables>(
             GET_ADMINISTRATOR,
             GET_ADMINISTRATOR,
             {
             {
                 id: createdAdmin.id,
                 id: createdAdmin.id,
@@ -63,7 +70,7 @@ describe('Administrator resolver', () => {
     });
     });
 
 
     it('updateAdministrator', async () => {
     it('updateAdministrator', async () => {
-        const result = await client.query<UpdateAdministrator.Mutation, UpdateAdministrator.Variables>(
+        const result = await adminClient.query<UpdateAdministrator.Mutation, UpdateAdministrator.Variables>(
             UPDATE_ADMINISTRATOR,
             UPDATE_ADMINISTRATOR,
             {
             {
                 input: {
                 input: {
@@ -80,7 +87,7 @@ describe('Administrator resolver', () => {
     });
     });
 
 
     it('updateAdministrator works with partial input', async () => {
     it('updateAdministrator works with partial input', async () => {
-        const result = await client.query<UpdateAdministrator.Mutation, UpdateAdministrator.Variables>(
+        const result = await adminClient.query<UpdateAdministrator.Mutation, UpdateAdministrator.Variables>(
             UPDATE_ADMINISTRATOR,
             UPDATE_ADMINISTRATOR,
             {
             {
                 input: {
                 input: {
@@ -98,7 +105,7 @@ describe('Administrator resolver', () => {
         'updateAdministrator throws with invalid roleId',
         'updateAdministrator throws with invalid roleId',
         assertThrowsWithMessage(
         assertThrowsWithMessage(
             () =>
             () =>
-                client.query<UpdateAdministrator.Mutation, UpdateAdministrator.Variables>(
+                adminClient.query<UpdateAdministrator.Mutation, UpdateAdministrator.Variables>(
                     UPDATE_ADMINISTRATOR,
                     UPDATE_ADMINISTRATOR,
                     {
                     {
                         input: {
                         input: {

+ 30 - 29
packages/core/e2e/auth.e2e-spec.ts

@@ -1,10 +1,12 @@
 /* tslint:disable:no-non-null-assertion */
 /* tslint:disable:no-non-null-assertion */
 import { SUPER_ADMIN_USER_IDENTIFIER, SUPER_ADMIN_USER_PASSWORD } from '@vendure/common/lib/shared-constants';
 import { SUPER_ADMIN_USER_IDENTIFIER, SUPER_ADMIN_USER_PASSWORD } from '@vendure/common/lib/shared-constants';
+import { createTestEnvironment } from '@vendure/testing';
 import { DocumentNode } from 'graphql';
 import { DocumentNode } from 'graphql';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
-import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
+import { initialData } from './fixtures/e2e-initial-data';
 import { CURRENT_USER_FRAGMENT } from './graphql/fragments';
 import { CURRENT_USER_FRAGMENT } from './graphql/fragments';
 import {
 import {
     CreateAdministrator,
     CreateAdministrator,
@@ -23,20 +25,19 @@ import {
     GET_PRODUCT_LIST,
     GET_PRODUCT_LIST,
     UPDATE_PRODUCT,
     UPDATE_PRODUCT,
 } from './graphql/shared-definitions';
 } from './graphql/shared-definitions';
-import { TestAdminClient } from './test-client';
-import { TestServer } from './test-server';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 
 
 describe('Authorization & permissions', () => {
 describe('Authorization & permissions', () => {
-    const client = new TestAdminClient();
-    const server = new TestServer();
+    const { server, adminClient } = createTestEnvironment(testConfig);
 
 
     beforeAll(async () => {
     beforeAll(async () => {
-        const token = await server.init({
+        await server.init({
+            dataDir,
+            initialData,
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
             customerCount: 1,
             customerCount: 1,
         });
         });
-        await client.init();
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
     }, TEST_SETUP_TIMEOUT_MS);
 
 
     afterAll(async () => {
     afterAll(async () => {
@@ -46,13 +47,13 @@ describe('Authorization & permissions', () => {
     describe('admin permissions', () => {
     describe('admin permissions', () => {
         describe('Anonymous user', () => {
         describe('Anonymous user', () => {
             beforeAll(async () => {
             beforeAll(async () => {
-                await client.asAnonymousUser();
+                await adminClient.asAnonymousUser();
             });
             });
 
 
             it(
             it(
                 'me is not permitted',
                 'me is not permitted',
                 assertThrowsWithMessage(async () => {
                 assertThrowsWithMessage(async () => {
-                    await client.query<Me.Query>(ME);
+                    await adminClient.query<Me.Query>(ME);
                 }, 'You are not currently authorized to perform this action'),
                 }, 'You are not currently authorized to perform this action'),
             );
             );
 
 
@@ -67,15 +68,15 @@ describe('Authorization & permissions', () => {
 
 
         describe('ReadCatalog permission', () => {
         describe('ReadCatalog permission', () => {
             beforeAll(async () => {
             beforeAll(async () => {
-                await client.asSuperAdmin();
+                await adminClient.asSuperAdmin();
                 const { identifier, password } = await createAdministratorWithPermissions('ReadCatalog', [
                 const { identifier, password } = await createAdministratorWithPermissions('ReadCatalog', [
                     Permission.ReadCatalog,
                     Permission.ReadCatalog,
                 ]);
                 ]);
-                await client.asUserWithCredentials(identifier, password);
+                await adminClient.asUserWithCredentials(identifier, password);
             });
             });
 
 
             it('me returns correct permissions', async () => {
             it('me returns correct permissions', async () => {
-                const { me } = await client.query<Me.Query>(ME);
+                const { me } = await adminClient.query<Me.Query>(ME);
 
 
                 expect(me!.channels[0].permissions).toEqual([
                 expect(me!.channels[0].permissions).toEqual([
                     Permission.Authenticated,
                     Permission.Authenticated,
@@ -107,18 +108,18 @@ describe('Authorization & permissions', () => {
 
 
         describe('CRUD on Customers permissions', () => {
         describe('CRUD on Customers permissions', () => {
             beforeAll(async () => {
             beforeAll(async () => {
-                await client.asSuperAdmin();
+                await adminClient.asSuperAdmin();
                 const { identifier, password } = await createAdministratorWithPermissions('CRUDCustomer', [
                 const { identifier, password } = await createAdministratorWithPermissions('CRUDCustomer', [
                     Permission.CreateCustomer,
                     Permission.CreateCustomer,
                     Permission.ReadCustomer,
                     Permission.ReadCustomer,
                     Permission.UpdateCustomer,
                     Permission.UpdateCustomer,
                     Permission.DeleteCustomer,
                     Permission.DeleteCustomer,
                 ]);
                 ]);
-                await client.asUserWithCredentials(identifier, password);
+                await adminClient.asUserWithCredentials(identifier, password);
             });
             });
 
 
             it('me returns correct permissions', async () => {
             it('me returns correct permissions', async () => {
-                const { me } = await client.query<Me.Query>(ME);
+                const { me } = await adminClient.query<Me.Query>(ME);
 
 
                 expect(me!.channels[0].permissions).toEqual([
                 expect(me!.channels[0].permissions).toEqual([
                     Permission.Authenticated,
                     Permission.Authenticated,
@@ -156,7 +157,7 @@ describe('Authorization & permissions', () => {
 
 
     async function assertRequestAllowed<V>(operation: DocumentNode, variables?: V) {
     async function assertRequestAllowed<V>(operation: DocumentNode, variables?: V) {
         try {
         try {
-            const status = await client.queryStatus(operation, variables);
+            const status = await adminClient.queryStatus(operation, variables);
             expect(status).toBe(200);
             expect(status).toBe(200);
         } catch (e) {
         } catch (e) {
             const errorCode = getErrorCode(e);
             const errorCode = getErrorCode(e);
@@ -170,7 +171,7 @@ describe('Authorization & permissions', () => {
 
 
     async function assertRequestForbidden<V>(operation: DocumentNode, variables: V) {
     async function assertRequestForbidden<V>(operation: DocumentNode, variables: V) {
         try {
         try {
-            const status = await client.query(operation, variables);
+            const status = await adminClient.query(operation, variables);
             fail(`Should have thrown`);
             fail(`Should have thrown`);
         } catch (e) {
         } catch (e) {
             expect(getErrorCode(e)).toBe('FORBIDDEN');
             expect(getErrorCode(e)).toBe('FORBIDDEN');
@@ -185,7 +186,7 @@ describe('Authorization & permissions', () => {
         code: string,
         code: string,
         permissions: Permission[],
         permissions: Permission[],
     ): Promise<{ identifier: string; password: string }> {
     ): Promise<{ identifier: string; password: string }> {
-        const roleResult = await client.query<CreateRole.Mutation, CreateRole.Variables>(CREATE_ROLE, {
+        const roleResult = await adminClient.query<CreateRole.Mutation, CreateRole.Variables>(CREATE_ROLE, {
             input: {
             input: {
                 code,
                 code,
                 description: '',
                 description: '',
@@ -200,18 +201,18 @@ describe('Authorization & permissions', () => {
             .substr(2, 8)}`;
             .substr(2, 8)}`;
         const password = `test`;
         const password = `test`;
 
 
-        const adminResult = await client.query<CreateAdministrator.Mutation, CreateAdministrator.Variables>(
-            CREATE_ADMINISTRATOR,
-            {
-                input: {
-                    emailAddress: identifier,
-                    firstName: code,
-                    lastName: 'Admin',
-                    password,
-                    roleIds: [role.id],
-                },
+        const adminResult = await adminClient.query<
+            CreateAdministrator.Mutation,
+            CreateAdministrator.Variables
+        >(CREATE_ADMINISTRATOR, {
+            input: {
+                emailAddress: identifier,
+                firstName: code,
+                lastName: 'Admin',
+                password,
+                roleIds: [role.id],
             },
             },
-        );
+        });
         const admin = adminResult.createAdministrator;
         const admin = adminResult.createAdministrator;
 
 
         return {
         return {

+ 106 - 98
packages/core/e2e/collection.e2e-spec.ts

@@ -1,15 +1,17 @@
 /* tslint:disable:no-non-null-assertion */
 /* tslint:disable:no-non-null-assertion */
 import { ROOT_COLLECTION_NAME } from '@vendure/common/lib/shared-constants';
 import { ROOT_COLLECTION_NAME } from '@vendure/common/lib/shared-constants';
+import {
+    facetValueCollectionFilter,
+    variantNameCollectionFilter,
+} from '@vendure/core/dist/config/collection/default-collection-filters';
+import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
 import { pick } from '../../common/lib/pick';
 import { pick } from '../../common/lib/pick';
-import {
-    facetValueCollectionFilter,
-    variantNameCollectionFilter,
-} from '../src/config/collection/default-collection-filters';
 
 
-import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
+import { initialData } from './fixtures/e2e-initial-data';
 import { COLLECTION_FRAGMENT, FACET_VALUE_FRAGMENT } from './graphql/fragments';
 import { COLLECTION_FRAGMENT, FACET_VALUE_FRAGMENT } from './graphql/fragments';
 import {
 import {
     Collection,
     Collection,
@@ -45,14 +47,12 @@ import {
     UPDATE_PRODUCT,
     UPDATE_PRODUCT,
     UPDATE_PRODUCT_VARIANTS,
     UPDATE_PRODUCT_VARIANTS,
 } from './graphql/shared-definitions';
 } from './graphql/shared-definitions';
-import { TestAdminClient } from './test-client';
-import { TestServer } from './test-server';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 import { awaitRunningJobs } from './utils/await-running-jobs';
 import { awaitRunningJobs } from './utils/await-running-jobs';
 
 
 describe('Collection resolver', () => {
 describe('Collection resolver', () => {
-    const client = new TestAdminClient();
-    const server = new TestServer();
+    const { server, adminClient } = createTestEnvironment(testConfig);
+
     let assets: GetAssetList.Items[];
     let assets: GetAssetList.Items[];
     let facetValues: FacetValueFragment[];
     let facetValues: FacetValueFragment[];
     let electronicsCollection: Collection.Fragment;
     let electronicsCollection: Collection.Fragment;
@@ -60,20 +60,25 @@ describe('Collection resolver', () => {
     let pearCollection: Collection.Fragment;
     let pearCollection: Collection.Fragment;
 
 
     beforeAll(async () => {
     beforeAll(async () => {
-        const token = await server.init({
+        await server.init({
+            dataDir,
+            initialData,
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-collections.csv'),
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-collections.csv'),
             customerCount: 1,
             customerCount: 1,
         });
         });
-        await client.init();
-        const assetsResult = await client.query<GetAssetList.Query, GetAssetList.Variables>(GET_ASSET_LIST, {
-            options: {
-                sort: {
-                    name: SortOrder.ASC,
+        await adminClient.asSuperAdmin();
+        const assetsResult = await adminClient.query<GetAssetList.Query, GetAssetList.Variables>(
+            GET_ASSET_LIST,
+            {
+                options: {
+                    sort: {
+                        name: SortOrder.ASC,
+                    },
                 },
                 },
             },
             },
-        });
+        );
         assets = assetsResult.assets.items;
         assets = assetsResult.assets.items;
-        const facetValuesResult = await client.query<GetFacetValues.Query>(GET_FACET_VALUES);
+        const facetValuesResult = await adminClient.query<GetFacetValues.Query>(GET_FACET_VALUES);
         facetValues = facetValuesResult.facets.items.reduce(
         facetValues = facetValuesResult.facets.items.reduce(
             (values, facet) => [...values, ...facet.values],
             (values, facet) => [...values, ...facet.values],
             [] as FacetValueFragment[],
             [] as FacetValueFragment[],
@@ -88,7 +93,7 @@ describe('Collection resolver', () => {
      * Test case for https://github.com/vendure-ecommerce/vendure/issues/97
      * Test case for https://github.com/vendure-ecommerce/vendure/issues/97
      */
      */
     it('collection breadcrumbs works after bootstrap', async () => {
     it('collection breadcrumbs works after bootstrap', async () => {
-        const result = await client.query<GetCollectionBreadcrumbs.Query>(GET_COLLECTION_BREADCRUMBS, {
+        const result = await adminClient.query<GetCollectionBreadcrumbs.Query>(GET_COLLECTION_BREADCRUMBS, {
             id: 'T_1',
             id: 'T_1',
         });
         });
         expect(result.collection!.breadcrumbs[0].name).toBe(ROOT_COLLECTION_NAME);
         expect(result.collection!.breadcrumbs[0].name).toBe(ROOT_COLLECTION_NAME);
@@ -96,7 +101,7 @@ describe('Collection resolver', () => {
 
 
     describe('createCollection', () => {
     describe('createCollection', () => {
         it('creates a root collection', async () => {
         it('creates a root collection', async () => {
-            const result = await client.query<CreateCollection.Mutation, CreateCollection.Variables>(
+            const result = await adminClient.query<CreateCollection.Mutation, CreateCollection.Variables>(
                 CREATE_COLLECTION,
                 CREATE_COLLECTION,
                 {
                 {
                     input: {
                     input: {
@@ -132,7 +137,7 @@ describe('Collection resolver', () => {
         });
         });
 
 
         it('creates a nested collection', async () => {
         it('creates a nested collection', async () => {
-            const result = await client.query<CreateCollection.Mutation, CreateCollection.Variables>(
+            const result = await adminClient.query<CreateCollection.Mutation, CreateCollection.Variables>(
                 CREATE_COLLECTION,
                 CREATE_COLLECTION,
                 {
                 {
                     input: {
                     input: {
@@ -163,7 +168,7 @@ describe('Collection resolver', () => {
         });
         });
 
 
         it('creates a 2nd level nested collection', async () => {
         it('creates a 2nd level nested collection', async () => {
-            const result = await client.query<CreateCollection.Mutation, CreateCollection.Variables>(
+            const result = await adminClient.query<CreateCollection.Mutation, CreateCollection.Variables>(
                 CREATE_COLLECTION,
                 CREATE_COLLECTION,
                 {
                 {
                     input: {
                     input: {
@@ -196,7 +201,7 @@ describe('Collection resolver', () => {
 
 
     describe('updateCollection', () => {
     describe('updateCollection', () => {
         it('updates with assets', async () => {
         it('updates with assets', async () => {
-            const { updateCollection } = await client.query<
+            const { updateCollection } = await adminClient.query<
                 UpdateCollection.Mutation,
                 UpdateCollection.Mutation,
                 UpdateCollection.Variables
                 UpdateCollection.Variables
             >(UPDATE_COLLECTION, {
             >(UPDATE_COLLECTION, {
@@ -212,7 +217,7 @@ describe('Collection resolver', () => {
         });
         });
 
 
         it('updating existing assets', async () => {
         it('updating existing assets', async () => {
-            const { updateCollection } = await client.query<
+            const { updateCollection } = await adminClient.query<
                 UpdateCollection.Mutation,
                 UpdateCollection.Mutation,
                 UpdateCollection.Variables
                 UpdateCollection.Variables
             >(UPDATE_COLLECTION, {
             >(UPDATE_COLLECTION, {
@@ -227,7 +232,7 @@ describe('Collection resolver', () => {
         });
         });
 
 
         it('removes all assets', async () => {
         it('removes all assets', async () => {
-            const { updateCollection } = await client.query<
+            const { updateCollection } = await adminClient.query<
                 UpdateCollection.Mutation,
                 UpdateCollection.Mutation,
                 UpdateCollection.Variables
                 UpdateCollection.Variables
             >(UPDATE_COLLECTION, {
             >(UPDATE_COLLECTION, {
@@ -243,7 +248,7 @@ describe('Collection resolver', () => {
     });
     });
 
 
     it('collection query', async () => {
     it('collection query', async () => {
-        const result = await client.query<GetCollection.Query, GetCollection.Variables>(GET_COLLECTION, {
+        const result = await adminClient.query<GetCollection.Query, GetCollection.Variables>(GET_COLLECTION, {
             id: computersCollection.id,
             id: computersCollection.id,
         });
         });
         if (!result.collection) {
         if (!result.collection) {
@@ -254,7 +259,7 @@ describe('Collection resolver', () => {
     });
     });
 
 
     it('parent field', async () => {
     it('parent field', async () => {
-        const result = await client.query<GetCollection.Query, GetCollection.Variables>(GET_COLLECTION, {
+        const result = await adminClient.query<GetCollection.Query, GetCollection.Variables>(GET_COLLECTION, {
             id: computersCollection.id,
             id: computersCollection.id,
         });
         });
         if (!result.collection) {
         if (!result.collection) {
@@ -265,7 +270,7 @@ describe('Collection resolver', () => {
     });
     });
 
 
     it('children field', async () => {
     it('children field', async () => {
-        const result = await client.query<GetCollection.Query, GetCollection.Variables>(GET_COLLECTION, {
+        const result = await adminClient.query<GetCollection.Query, GetCollection.Variables>(GET_COLLECTION, {
             id: electronicsCollection.id,
             id: electronicsCollection.id,
         });
         });
         if (!result.collection) {
         if (!result.collection) {
@@ -277,12 +282,12 @@ describe('Collection resolver', () => {
     });
     });
 
 
     it('breadcrumbs', async () => {
     it('breadcrumbs', async () => {
-        const result = await client.query<GetCollectionBreadcrumbs.Query, GetCollectionBreadcrumbs.Variables>(
-            GET_COLLECTION_BREADCRUMBS,
-            {
-                id: pearCollection.id,
-            },
-        );
+        const result = await adminClient.query<
+            GetCollectionBreadcrumbs.Query,
+            GetCollectionBreadcrumbs.Variables
+        >(GET_COLLECTION_BREADCRUMBS, {
+            id: pearCollection.id,
+        });
         if (!result.collection) {
         if (!result.collection) {
             fail(`did not return the collection`);
             fail(`did not return the collection`);
             return;
             return;
@@ -296,12 +301,12 @@ describe('Collection resolver', () => {
     });
     });
 
 
     it('breadcrumbs for root collection', async () => {
     it('breadcrumbs for root collection', async () => {
-        const result = await client.query<GetCollectionBreadcrumbs.Query, GetCollectionBreadcrumbs.Variables>(
-            GET_COLLECTION_BREADCRUMBS,
-            {
-                id: 'T_1',
-            },
-        );
+        const result = await adminClient.query<
+            GetCollectionBreadcrumbs.Query,
+            GetCollectionBreadcrumbs.Variables
+        >(GET_COLLECTION_BREADCRUMBS, {
+            id: 'T_1',
+        });
         if (!result.collection) {
         if (!result.collection) {
             fail(`did not return the collection`);
             fail(`did not return the collection`);
             return;
             return;
@@ -310,7 +315,7 @@ describe('Collection resolver', () => {
     });
     });
 
 
     it('collections.assets', async () => {
     it('collections.assets', async () => {
-        const { collections } = await client.query<GetCollectionsWithAssets.Query>(gql`
+        const { collections } = await adminClient.query<GetCollectionsWithAssets.Query>(gql`
             query GetCollectionsWithAssets {
             query GetCollectionsWithAssets {
                 collections {
                 collections {
                     items {
                     items {
@@ -327,7 +332,7 @@ describe('Collection resolver', () => {
 
 
     describe('moveCollection', () => {
     describe('moveCollection', () => {
         it('moves a collection to a new parent', async () => {
         it('moves a collection to a new parent', async () => {
-            const result = await client.query<MoveCollection.Mutation, MoveCollection.Variables>(
+            const result = await adminClient.query<MoveCollection.Mutation, MoveCollection.Variables>(
                 MOVE_COLLECTION,
                 MOVE_COLLECTION,
                 {
                 {
                     input: {
                     input: {
@@ -345,10 +350,10 @@ describe('Collection resolver', () => {
         });
         });
 
 
         it('re-evaluates Collection contents on move', async () => {
         it('re-evaluates Collection contents on move', async () => {
-            const result = await client.query<GetCollectionProducts.Query, GetCollectionProducts.Variables>(
-                GET_COLLECTION_PRODUCT_VARIANTS,
-                { id: pearCollection.id },
-            );
+            const result = await adminClient.query<
+                GetCollectionProducts.Query,
+                GetCollectionProducts.Variables
+            >(GET_COLLECTION_PRODUCT_VARIANTS, { id: pearCollection.id });
             expect(result.collection!.productVariants.items.map(i => i.name)).toEqual([
             expect(result.collection!.productVariants.items.map(i => i.name)).toEqual([
                 'Laptop 13 inch 8GB',
                 'Laptop 13 inch 8GB',
                 'Laptop 15 inch 8GB',
                 'Laptop 15 inch 8GB',
@@ -359,7 +364,7 @@ describe('Collection resolver', () => {
         });
         });
 
 
         it('alters the position in the current parent 1', async () => {
         it('alters the position in the current parent 1', async () => {
-            await client.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
+            await adminClient.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
                 input: {
                 input: {
                     collectionId: computersCollection.id,
                     collectionId: computersCollection.id,
                     parentId: electronicsCollection.id,
                     parentId: electronicsCollection.id,
@@ -372,7 +377,7 @@ describe('Collection resolver', () => {
         });
         });
 
 
         it('alters the position in the current parent 2', async () => {
         it('alters the position in the current parent 2', async () => {
-            await client.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
+            await adminClient.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
                 input: {
                 input: {
                     collectionId: pearCollection.id,
                     collectionId: pearCollection.id,
                     parentId: electronicsCollection.id,
                     parentId: electronicsCollection.id,
@@ -385,7 +390,7 @@ describe('Collection resolver', () => {
         });
         });
 
 
         it('corrects an out-of-bounds negative index value', async () => {
         it('corrects an out-of-bounds negative index value', async () => {
-            await client.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
+            await adminClient.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
                 input: {
                 input: {
                     collectionId: pearCollection.id,
                     collectionId: pearCollection.id,
                     parentId: electronicsCollection.id,
                     parentId: electronicsCollection.id,
@@ -398,7 +403,7 @@ describe('Collection resolver', () => {
         });
         });
 
 
         it('corrects an out-of-bounds positive index value', async () => {
         it('corrects an out-of-bounds positive index value', async () => {
-            await client.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
+            await adminClient.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
                 input: {
                 input: {
                     collectionId: pearCollection.id,
                     collectionId: pearCollection.id,
                     parentId: electronicsCollection.id,
                     parentId: electronicsCollection.id,
@@ -414,7 +419,7 @@ describe('Collection resolver', () => {
             'throws if attempting to move into self',
             'throws if attempting to move into self',
             assertThrowsWithMessage(
             assertThrowsWithMessage(
                 () =>
                 () =>
-                    client.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
+                    adminClient.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
                         input: {
                         input: {
                             collectionId: pearCollection.id,
                             collectionId: pearCollection.id,
                             parentId: pearCollection.id,
                             parentId: pearCollection.id,
@@ -429,7 +434,7 @@ describe('Collection resolver', () => {
             'throws if attempting to move into a decendant of self',
             'throws if attempting to move into a decendant of self',
             assertThrowsWithMessage(
             assertThrowsWithMessage(
                 () =>
                 () =>
-                    client.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
+                    adminClient.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
                         input: {
                         input: {
                             collectionId: pearCollection.id,
                             collectionId: pearCollection.id,
                             parentId: pearCollection.id,
                             parentId: pearCollection.id,
@@ -441,7 +446,7 @@ describe('Collection resolver', () => {
         );
         );
 
 
         async function getChildrenOf(parentId: string): Promise<Array<{ name: string; id: string }>> {
         async function getChildrenOf(parentId: string): Promise<Array<{ name: string; id: string }>> {
-            const result = await client.query<GetCollections.Query>(GET_COLLECTIONS);
+            const result = await adminClient.query<GetCollections.Query>(GET_COLLECTIONS);
             return result.collections.items.filter(i => i.parent!.id === parentId);
             return result.collections.items.filter(i => i.parent!.id === parentId);
         }
         }
     });
     });
@@ -452,7 +457,7 @@ describe('Collection resolver', () => {
         let laptopProductId: string;
         let laptopProductId: string;
 
 
         beforeAll(async () => {
         beforeAll(async () => {
-            const result1 = await client.query<CreateCollection.Mutation, CreateCollection.Variables>(
+            const result1 = await adminClient.query<CreateCollection.Mutation, CreateCollection.Variables>(
                 CREATE_COLLECTION,
                 CREATE_COLLECTION,
                 {
                 {
                     input: {
                     input: {
@@ -481,7 +486,7 @@ describe('Collection resolver', () => {
             );
             );
             collectionToDeleteParent = result1.createCollection;
             collectionToDeleteParent = result1.createCollection;
 
 
-            const result2 = await client.query<CreateCollection.Mutation, CreateCollection.Variables>(
+            const result2 = await adminClient.query<CreateCollection.Mutation, CreateCollection.Variables>(
                 CREATE_COLLECTION,
                 CREATE_COLLECTION,
                 {
                 {
                     input: {
                     input: {
@@ -499,14 +504,17 @@ describe('Collection resolver', () => {
         it(
         it(
             'throws for invalid collection id',
             'throws for invalid collection id',
             assertThrowsWithMessage(async () => {
             assertThrowsWithMessage(async () => {
-                await client.query<DeleteCollection.Mutation, DeleteCollection.Variables>(DELETE_COLLECTION, {
-                    id: 'T_999',
-                });
+                await adminClient.query<DeleteCollection.Mutation, DeleteCollection.Variables>(
+                    DELETE_COLLECTION,
+                    {
+                        id: 'T_999',
+                    },
+                );
             }, "No Collection with the id '999' could be found"),
             }, "No Collection with the id '999' could be found"),
         );
         );
 
 
         it('collection and product related prior to deletion', async () => {
         it('collection and product related prior to deletion', async () => {
-            const { collection } = await client.query<
+            const { collection } = await adminClient.query<
                 GetCollectionProducts.Query,
                 GetCollectionProducts.Query,
                 GetCollectionProducts.Variables
                 GetCollectionProducts.Variables
             >(GET_COLLECTION_PRODUCT_VARIANTS, {
             >(GET_COLLECTION_PRODUCT_VARIANTS, {
@@ -521,7 +529,7 @@ describe('Collection resolver', () => {
 
 
             laptopProductId = collection!.productVariants.items[0].productId;
             laptopProductId = collection!.productVariants.items[0].productId;
 
 
-            const { product } = await client.query<
+            const { product } = await adminClient.query<
                 GetProductCollections.Query,
                 GetProductCollections.Query,
                 GetProductCollections.Variables
                 GetProductCollections.Variables
             >(GET_PRODUCT_COLLECTIONS, {
             >(GET_PRODUCT_COLLECTIONS, {
@@ -538,7 +546,7 @@ describe('Collection resolver', () => {
         });
         });
 
 
         it('deleteCollection works', async () => {
         it('deleteCollection works', async () => {
-            const { deleteCollection } = await client.query<
+            const { deleteCollection } = await adminClient.query<
                 DeleteCollection.Mutation,
                 DeleteCollection.Mutation,
                 DeleteCollection.Variables
                 DeleteCollection.Variables
             >(DELETE_COLLECTION, {
             >(DELETE_COLLECTION, {
@@ -549,7 +557,7 @@ describe('Collection resolver', () => {
         });
         });
 
 
         it('deleted parent collection is null', async () => {
         it('deleted parent collection is null', async () => {
-            const { collection } = await client.query<GetCollection.Query, GetCollection.Variables>(
+            const { collection } = await adminClient.query<GetCollection.Query, GetCollection.Variables>(
                 GET_COLLECTION,
                 GET_COLLECTION,
                 {
                 {
                     id: collectionToDeleteParent.id,
                     id: collectionToDeleteParent.id,
@@ -559,7 +567,7 @@ describe('Collection resolver', () => {
         });
         });
 
 
         it('deleted child collection is null', async () => {
         it('deleted child collection is null', async () => {
-            const { collection } = await client.query<GetCollection.Query, GetCollection.Variables>(
+            const { collection } = await adminClient.query<GetCollection.Query, GetCollection.Variables>(
                 GET_COLLECTION,
                 GET_COLLECTION,
                 {
                 {
                     id: collectionToDeleteChild.id,
                     id: collectionToDeleteChild.id,
@@ -569,7 +577,7 @@ describe('Collection resolver', () => {
         });
         });
 
 
         it('product no longer lists collection', async () => {
         it('product no longer lists collection', async () => {
-            const { product } = await client.query<
+            const { product } = await adminClient.query<
                 GetProductCollections.Query,
                 GetProductCollections.Query,
                 GetProductCollections.Variables
                 GetProductCollections.Variables
             >(GET_PRODUCT_COLLECTIONS, {
             >(GET_PRODUCT_COLLECTIONS, {
@@ -586,7 +594,7 @@ describe('Collection resolver', () => {
 
 
     describe('filters', () => {
     describe('filters', () => {
         it('Collection with no filters has no productVariants', async () => {
         it('Collection with no filters has no productVariants', async () => {
-            const result = await client.query<
+            const result = await adminClient.query<
                 CreateCollectionSelectVariants.Mutation,
                 CreateCollectionSelectVariants.Mutation,
                 CreateCollectionSelectVariants.Variables
                 CreateCollectionSelectVariants.Variables
             >(CREATE_COLLECTION_SELECT_VARIANTS, {
             >(CREATE_COLLECTION_SELECT_VARIANTS, {
@@ -600,7 +608,7 @@ describe('Collection resolver', () => {
 
 
         describe('facetValue filter', () => {
         describe('facetValue filter', () => {
             it('electronics', async () => {
             it('electronics', async () => {
-                const result = await client.query<
+                const result = await adminClient.query<
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Variables
                     GetCollectionProducts.Variables
                 >(GET_COLLECTION_PRODUCT_VARIANTS, {
                 >(GET_COLLECTION_PRODUCT_VARIANTS, {
@@ -632,7 +640,7 @@ describe('Collection resolver', () => {
             });
             });
 
 
             it('computers', async () => {
             it('computers', async () => {
-                const result = await client.query<
+                const result = await adminClient.query<
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Variables
                     GetCollectionProducts.Variables
                 >(GET_COLLECTION_PRODUCT_VARIANTS, {
                 >(GET_COLLECTION_PRODUCT_VARIANTS, {
@@ -660,7 +668,7 @@ describe('Collection resolver', () => {
             });
             });
 
 
             it('photo AND pear', async () => {
             it('photo AND pear', async () => {
-                const result = await client.query<
+                const result = await adminClient.query<
                     CreateCollectionSelectVariants.Mutation,
                     CreateCollectionSelectVariants.Mutation,
                     CreateCollectionSelectVariants.Variables
                     CreateCollectionSelectVariants.Variables
                 >(CREATE_COLLECTION_SELECT_VARIANTS, {
                 >(CREATE_COLLECTION_SELECT_VARIANTS, {
@@ -690,8 +698,8 @@ describe('Collection resolver', () => {
                     } as CreateCollectionInput,
                     } as CreateCollectionInput,
                 });
                 });
 
 
-                await awaitRunningJobs(client);
-                const { collection } = await client.query<GetCollection.Query, GetCollection.Variables>(
+                await awaitRunningJobs(adminClient);
+                const { collection } = await adminClient.query<GetCollection.Query, GetCollection.Variables>(
                     GET_COLLECTION,
                     GET_COLLECTION,
                     {
                     {
                         id: result.createCollection.id,
                         id: result.createCollection.id,
@@ -702,7 +710,7 @@ describe('Collection resolver', () => {
             });
             });
 
 
             it('photo OR pear', async () => {
             it('photo OR pear', async () => {
-                const result = await client.query<
+                const result = await adminClient.query<
                     CreateCollectionSelectVariants.Mutation,
                     CreateCollectionSelectVariants.Mutation,
                     CreateCollectionSelectVariants.Variables
                     CreateCollectionSelectVariants.Variables
                 >(CREATE_COLLECTION_SELECT_VARIANTS, {
                 >(CREATE_COLLECTION_SELECT_VARIANTS, {
@@ -732,8 +740,8 @@ describe('Collection resolver', () => {
                     } as CreateCollectionInput,
                     } as CreateCollectionInput,
                 });
                 });
 
 
-                await awaitRunningJobs(client);
-                const { collection } = await client.query<GetCollection.Query, GetCollection.Variables>(
+                await awaitRunningJobs(adminClient);
+                const { collection } = await adminClient.query<GetCollection.Query, GetCollection.Variables>(
                     GET_COLLECTION,
                     GET_COLLECTION,
                     {
                     {
                         id: result.createCollection.id,
                         id: result.createCollection.id,
@@ -754,7 +762,7 @@ describe('Collection resolver', () => {
             });
             });
 
 
             it('bell OR pear in computers', async () => {
             it('bell OR pear in computers', async () => {
-                const result = await client.query<
+                const result = await adminClient.query<
                     CreateCollectionSelectVariants.Mutation,
                     CreateCollectionSelectVariants.Mutation,
                     CreateCollectionSelectVariants.Variables
                     CreateCollectionSelectVariants.Variables
                 >(CREATE_COLLECTION_SELECT_VARIANTS, {
                 >(CREATE_COLLECTION_SELECT_VARIANTS, {
@@ -787,8 +795,8 @@ describe('Collection resolver', () => {
                     } as CreateCollectionInput,
                     } as CreateCollectionInput,
                 });
                 });
 
 
-                await awaitRunningJobs(client);
-                const { collection } = await client.query<GetCollection.Query, GetCollection.Variables>(
+                await awaitRunningJobs(adminClient);
+                const { collection } = await adminClient.query<GetCollection.Query, GetCollection.Variables>(
                     GET_COLLECTION,
                     GET_COLLECTION,
                     {
                     {
                         id: result.createCollection.id,
                         id: result.createCollection.id,
@@ -811,7 +819,7 @@ describe('Collection resolver', () => {
                 operator: string,
                 operator: string,
                 term: string,
                 term: string,
             ): Promise<Collection.Fragment> {
             ): Promise<Collection.Fragment> {
-                const { createCollection } = await client.query<
+                const { createCollection } = await adminClient.query<
                     CreateCollection.Mutation,
                     CreateCollection.Mutation,
                     CreateCollection.Variables
                     CreateCollection.Variables
                 >(CREATE_COLLECTION, {
                 >(CREATE_COLLECTION, {
@@ -844,7 +852,7 @@ describe('Collection resolver', () => {
             it('contains operator', async () => {
             it('contains operator', async () => {
                 const collection = await createVariantNameFilteredCollection('contains', 'camera');
                 const collection = await createVariantNameFilteredCollection('contains', 'camera');
 
 
-                const result = await client.query<
+                const result = await adminClient.query<
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Variables
                     GetCollectionProducts.Variables
                 >(GET_COLLECTION_PRODUCT_VARIANTS, {
                 >(GET_COLLECTION_PRODUCT_VARIANTS, {
@@ -860,7 +868,7 @@ describe('Collection resolver', () => {
             it('startsWith operator', async () => {
             it('startsWith operator', async () => {
                 const collection = await createVariantNameFilteredCollection('startsWith', 'camera');
                 const collection = await createVariantNameFilteredCollection('startsWith', 'camera');
 
 
-                const result = await client.query<
+                const result = await adminClient.query<
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Variables
                     GetCollectionProducts.Variables
                 >(GET_COLLECTION_PRODUCT_VARIANTS, {
                 >(GET_COLLECTION_PRODUCT_VARIANTS, {
@@ -872,7 +880,7 @@ describe('Collection resolver', () => {
             it('endsWith operator', async () => {
             it('endsWith operator', async () => {
                 const collection = await createVariantNameFilteredCollection('endsWith', 'camera');
                 const collection = await createVariantNameFilteredCollection('endsWith', 'camera');
 
 
-                const result = await client.query<
+                const result = await adminClient.query<
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Variables
                     GetCollectionProducts.Variables
                 >(GET_COLLECTION_PRODUCT_VARIANTS, {
                 >(GET_COLLECTION_PRODUCT_VARIANTS, {
@@ -887,7 +895,7 @@ describe('Collection resolver', () => {
             it('doesNotContain operator', async () => {
             it('doesNotContain operator', async () => {
                 const collection = await createVariantNameFilteredCollection('doesNotContain', 'camera');
                 const collection = await createVariantNameFilteredCollection('doesNotContain', 'camera');
 
 
-                const result = await client.query<
+                const result = await adminClient.query<
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Variables
                     GetCollectionProducts.Variables
                 >(GET_COLLECTION_PRODUCT_VARIANTS, {
                 >(GET_COLLECTION_PRODUCT_VARIANTS, {
@@ -921,7 +929,7 @@ describe('Collection resolver', () => {
             let products: GetProductsWithVariantIds.Items[];
             let products: GetProductsWithVariantIds.Items[];
 
 
             beforeAll(async () => {
             beforeAll(async () => {
-                const result = await client.query<GetProductsWithVariantIds.Query>(gql`
+                const result = await adminClient.query<GetProductsWithVariantIds.Query>(gql`
                     query GetProductsWithVariantIds {
                     query GetProductsWithVariantIds {
                         products {
                         products {
                             items {
                             items {
@@ -938,7 +946,7 @@ describe('Collection resolver', () => {
             });
             });
 
 
             it('updates contents when Product is updated', async () => {
             it('updates contents when Product is updated', async () => {
-                await client.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
+                await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
                     input: {
                     input: {
                         id: products[1].id,
                         id: products[1].id,
                         facetValueIds: [
                         facetValueIds: [
@@ -949,9 +957,9 @@ describe('Collection resolver', () => {
                     },
                     },
                 });
                 });
 
 
-                await awaitRunningJobs(client);
+                await awaitRunningJobs(adminClient);
 
 
-                const result = await client.query<
+                const result = await adminClient.query<
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Variables
                     GetCollectionProducts.Variables
                 >(GET_COLLECTION_PRODUCT_VARIANTS, { id: pearCollection.id });
                 >(GET_COLLECTION_PRODUCT_VARIANTS, { id: pearCollection.id });
@@ -968,7 +976,7 @@ describe('Collection resolver', () => {
 
 
             it('updates contents when ProductVariant is updated', async () => {
             it('updates contents when ProductVariant is updated', async () => {
                 const gamingPcFirstVariant = products.find(p => p.name === 'Gaming PC')!.variants[0];
                 const gamingPcFirstVariant = products.find(p => p.name === 'Gaming PC')!.variants[0];
-                await client.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
+                await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
                     UPDATE_PRODUCT_VARIANTS,
                     UPDATE_PRODUCT_VARIANTS,
                     {
                     {
                         input: [
                         input: [
@@ -980,9 +988,9 @@ describe('Collection resolver', () => {
                     },
                     },
                 );
                 );
 
 
-                await awaitRunningJobs(client);
+                await awaitRunningJobs(adminClient);
 
 
-                const result = await client.query<
+                const result = await adminClient.query<
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Variables
                     GetCollectionProducts.Variables
                 >(GET_COLLECTION_PRODUCT_VARIANTS, { id: pearCollection.id });
                 >(GET_COLLECTION_PRODUCT_VARIANTS, { id: pearCollection.id });
@@ -1000,7 +1008,7 @@ describe('Collection resolver', () => {
 
 
             it('correctly filters when ProductVariant and Product both have matching FacetValue', async () => {
             it('correctly filters when ProductVariant and Product both have matching FacetValue', async () => {
                 const gamingPcFirstVariant = products.find(p => p.name === 'Gaming PC')!.variants[0];
                 const gamingPcFirstVariant = products.find(p => p.name === 'Gaming PC')!.variants[0];
-                await client.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
+                await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
                     UPDATE_PRODUCT_VARIANTS,
                     UPDATE_PRODUCT_VARIANTS,
                     {
                     {
                         input: [
                         input: [
@@ -1011,7 +1019,7 @@ describe('Collection resolver', () => {
                         ],
                         ],
                     },
                     },
                 );
                 );
-                const result = await client.query<
+                const result = await adminClient.query<
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Variables
                     GetCollectionProducts.Variables
                 >(GET_COLLECTION_PRODUCT_VARIANTS, { id: pearCollection.id });
                 >(GET_COLLECTION_PRODUCT_VARIANTS, { id: pearCollection.id });
@@ -1029,7 +1037,7 @@ describe('Collection resolver', () => {
         });
         });
 
 
         it('filter inheritance of nested collections (issue #158)', async () => {
         it('filter inheritance of nested collections (issue #158)', async () => {
-            const { createCollection: pearElectronics } = await client.query<
+            const { createCollection: pearElectronics } = await adminClient.query<
                 CreateCollectionSelectVariants.Mutation,
                 CreateCollectionSelectVariants.Mutation,
                 CreateCollectionSelectVariants.Variables
                 CreateCollectionSelectVariants.Variables
             >(CREATE_COLLECTION_SELECT_VARIANTS, {
             >(CREATE_COLLECTION_SELECT_VARIANTS, {
@@ -1058,12 +1066,12 @@ describe('Collection resolver', () => {
                 } as CreateCollectionInput,
                 } as CreateCollectionInput,
             });
             });
 
 
-            await awaitRunningJobs(client);
+            await awaitRunningJobs(adminClient);
 
 
-            const result = await client.query<GetCollectionProducts.Query, GetCollectionProducts.Variables>(
-                GET_COLLECTION_PRODUCT_VARIANTS,
-                { id: pearElectronics.id },
-            );
+            const result = await adminClient.query<
+                GetCollectionProducts.Query,
+                GetCollectionProducts.Variables
+            >(GET_COLLECTION_PRODUCT_VARIANTS, { id: pearElectronics.id });
             expect(result.collection!.productVariants.items.map(i => i.name)).toEqual([
             expect(result.collection!.productVariants.items.map(i => i.name)).toEqual([
                 'Laptop 13 inch 8GB',
                 'Laptop 13 inch 8GB',
                 'Laptop 15 inch 8GB',
                 'Laptop 15 inch 8GB',
@@ -1080,7 +1088,7 @@ describe('Collection resolver', () => {
 
 
     describe('Product collections property', () => {
     describe('Product collections property', () => {
         it('returns all collections to which the Product belongs', async () => {
         it('returns all collections to which the Product belongs', async () => {
-            const result = await client.query<
+            const result = await adminClient.query<
                 GetCollectionsForProducts.Query,
                 GetCollectionsForProducts.Query,
                 GetCollectionsForProducts.Variables
                 GetCollectionsForProducts.Variables
             >(GET_COLLECTIONS_FOR_PRODUCTS, { term: 'camera' });
             >(GET_COLLECTIONS_FOR_PRODUCTS, { term: 'camera' });
@@ -1097,10 +1105,10 @@ describe('Collection resolver', () => {
     });
     });
 
 
     it('collection does not list deleted products', async () => {
     it('collection does not list deleted products', async () => {
-        await client.query<DeleteProduct.Mutation, DeleteProduct.Variables>(DELETE_PRODUCT, {
+        await adminClient.query<DeleteProduct.Mutation, DeleteProduct.Variables>(DELETE_PRODUCT, {
             id: 'T_2', // curvy monitor
             id: 'T_2', // curvy monitor
         });
         });
-        const { collection } = await client.query<
+        const { collection } = await adminClient.query<
             GetCollectionProducts.Query,
             GetCollectionProducts.Query,
             GetCollectionProducts.Variables
             GetCollectionProducts.Variables
         >(GET_COLLECTION_PRODUCT_VARIANTS, {
         >(GET_COLLECTION_PRODUCT_VARIANTS, {

+ 6 - 51
packages/core/e2e/config/test-config.ts

@@ -1,15 +1,7 @@
-import { Transport } from '@nestjs/microservices';
-import { ADMIN_API_PATH, SHOP_API_PATH } from '@vendure/common/lib/shared-constants';
+import { mergeConfig } from '@vendure/core';
+import { testConfig as defaultTestConfig } from '@vendure/testing';
 import path from 'path';
 import path from 'path';
 
 
-import { DefaultAssetNamingStrategy } from '../../src/config/asset-naming-strategy/default-asset-naming-strategy';
-import { NoopLogger } from '../../src/config/logger/noop-logger';
-import { VendureConfig } from '../../src/config/vendure-config';
-
-import { TestingAssetPreviewStrategy } from './testing-asset-preview-strategy';
-import { TestingAssetStorageStrategy } from './testing-asset-storage-strategy';
-import { TestingEntityIdStrategy } from './testing-entity-id-strategy';
-
 /**
 /**
  * We use a relatively long timeout on the initial beforeAll() function of the
  * We use a relatively long timeout on the initial beforeAll() function of the
  * e2e tests because on the first run (and always in CI) the sqlite databases
  * e2e tests because on the first run (and always in CI) the sqlite databases
@@ -27,47 +19,10 @@ if (process.env.E2E_DEBUG) {
     jest.setTimeout(1800 * 1000);
     jest.setTimeout(1800 * 1000);
 }
 }
 
 
-/**
- * Config settings used for e2e tests
- */
-export const testConfig: VendureConfig = {
-    port: 3050,
-    adminApiPath: ADMIN_API_PATH,
-    shopApiPath: SHOP_API_PATH,
-    cors: true,
-    defaultChannelToken: 'e2e-default-channel',
-    authOptions: {
-        sessionSecret: 'some-secret',
-        tokenMethod: 'bearer',
-        requireVerification: true,
-    },
-    dbConnectionOptions: {
-        type: 'sqljs',
-        database: new Uint8Array([]),
-        location: '',
-        autoSave: false,
-        logging: false,
-    },
-    promotionOptions: {},
-    customFields: {},
-    entityIdStrategy: new TestingEntityIdStrategy(),
-    paymentOptions: {
-        paymentMethodHandlers: [],
-    },
-    logger: new NoopLogger(),
+export const dataDir = path.join(__dirname, '../__data__');
+
+export const testConfig = mergeConfig(defaultTestConfig, {
     importExportOptions: {
     importExportOptions: {
         importAssetsDir: path.join(__dirname, '..', 'fixtures/assets'),
         importAssetsDir: path.join(__dirname, '..', 'fixtures/assets'),
     },
     },
-    assetOptions: {
-        assetNamingStrategy: new DefaultAssetNamingStrategy(),
-        assetStorageStrategy: new TestingAssetStorageStrategy(),
-        assetPreviewStrategy: new TestingAssetPreviewStrategy(),
-    },
-    workerOptions: {
-        runInMainProcess: true,
-        transport: Transport.TCP,
-        options: {
-            port: 3051,
-        },
-    },
-};
+});

+ 36 - 23
packages/core/e2e/country.e2e-spec.ts

@@ -1,7 +1,9 @@
+import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
-import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
+import { initialData } from './fixtures/e2e-initial-data';
 import { COUNTRY_FRAGMENT } from './graphql/fragments';
 import { COUNTRY_FRAGMENT } from './graphql/fragments';
 import {
 import {
     CreateCountry,
     CreateCountry,
@@ -13,24 +15,23 @@ import {
     UpdateCountry,
     UpdateCountry,
 } from './graphql/generated-e2e-admin-types';
 } from './graphql/generated-e2e-admin-types';
 import { GET_COUNTRY_LIST, UPDATE_COUNTRY } from './graphql/shared-definitions';
 import { GET_COUNTRY_LIST, UPDATE_COUNTRY } from './graphql/shared-definitions';
-import { TestAdminClient } from './test-client';
-import { TestServer } from './test-server';
 
 
 // tslint:disable:no-non-null-assertion
 // tslint:disable:no-non-null-assertion
 
 
 describe('Facet resolver', () => {
 describe('Facet resolver', () => {
-    const client = new TestAdminClient();
-    const server = new TestServer();
+    const { server, adminClient } = createTestEnvironment(testConfig);
     let countries: GetCountryList.Items[];
     let countries: GetCountryList.Items[];
     let GB: GetCountryList.Items;
     let GB: GetCountryList.Items;
     let AT: GetCountryList.Items;
     let AT: GetCountryList.Items;
 
 
     beforeAll(async () => {
     beforeAll(async () => {
         await server.init({
         await server.init({
+            dataDir,
+            initialData,
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
             customerCount: 1,
             customerCount: 1,
         });
         });
-        await client.init();
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
     }, TEST_SETUP_TIMEOUT_MS);
 
 
     afterAll(async () => {
     afterAll(async () => {
@@ -38,7 +39,7 @@ describe('Facet resolver', () => {
     });
     });
 
 
     it('countries', async () => {
     it('countries', async () => {
-        const result = await client.query<GetCountryList.Query>(GET_COUNTRY_LIST, {});
+        const result = await adminClient.query<GetCountryList.Query>(GET_COUNTRY_LIST, {});
 
 
         expect(result.countries.totalItems).toBe(7);
         expect(result.countries.totalItems).toBe(7);
         countries = result.countries.items;
         countries = result.countries.items;
@@ -47,7 +48,7 @@ describe('Facet resolver', () => {
     });
     });
 
 
     it('country', async () => {
     it('country', async () => {
-        const result = await client.query<GetCountry.Query, GetCountry.Variables>(GET_COUNTRY, {
+        const result = await adminClient.query<GetCountry.Query, GetCountry.Variables>(GET_COUNTRY, {
             id: GB.id,
             id: GB.id,
         });
         });
 
 
@@ -55,50 +56,62 @@ describe('Facet resolver', () => {
     });
     });
 
 
     it('updateCountry', async () => {
     it('updateCountry', async () => {
-        const result = await client.query<UpdateCountry.Mutation, UpdateCountry.Variables>(UPDATE_COUNTRY, {
-            input: {
-                id: AT.id,
-                enabled: false,
+        const result = await adminClient.query<UpdateCountry.Mutation, UpdateCountry.Variables>(
+            UPDATE_COUNTRY,
+            {
+                input: {
+                    id: AT.id,
+                    enabled: false,
+                },
             },
             },
-        });
+        );
 
 
         expect(result.updateCountry.enabled).toBe(false);
         expect(result.updateCountry.enabled).toBe(false);
     });
     });
 
 
     it('createCountry', async () => {
     it('createCountry', async () => {
-        const result = await client.query<CreateCountry.Mutation, CreateCountry.Variables>(CREATE_COUNTRY, {
-            input: {
-                code: 'GL',
-                enabled: true,
-                translations: [{ languageCode: LanguageCode.en, name: 'Gondwanaland' }],
+        const result = await adminClient.query<CreateCountry.Mutation, CreateCountry.Variables>(
+            CREATE_COUNTRY,
+            {
+                input: {
+                    code: 'GL',
+                    enabled: true,
+                    translations: [{ languageCode: LanguageCode.en, name: 'Gondwanaland' }],
+                },
             },
             },
-        });
+        );
 
 
         expect(result.createCountry.name).toBe('Gondwanaland');
         expect(result.createCountry.name).toBe('Gondwanaland');
     });
     });
 
 
     describe('deletion', () => {
     describe('deletion', () => {
         it('deletes Country not used in any address', async () => {
         it('deletes Country not used in any address', async () => {
-            const result1 = await client.query<DeleteCountry.Mutation, DeleteCountry.Variables>(DELETE_COUNTRY, { id: AT.id });
+            const result1 = await adminClient.query<DeleteCountry.Mutation, DeleteCountry.Variables>(
+                DELETE_COUNTRY,
+                { id: AT.id },
+            );
 
 
             expect(result1.deleteCountry).toEqual({
             expect(result1.deleteCountry).toEqual({
                 result: DeletionResult.DELETED,
                 result: DeletionResult.DELETED,
                 message: '',
                 message: '',
             });
             });
 
 
-            const result2 = await client.query<GetCountryList.Query>(GET_COUNTRY_LIST, {});
+            const result2 = await adminClient.query<GetCountryList.Query>(GET_COUNTRY_LIST, {});
             expect(result2.countries.items.find(c => c.id === AT.id)).toBeUndefined();
             expect(result2.countries.items.find(c => c.id === AT.id)).toBeUndefined();
         });
         });
 
 
         it('does not delete Country that is used in one or more addresses', async () => {
         it('does not delete Country that is used in one or more addresses', async () => {
-            const result1 = await client.query<DeleteCountry.Mutation, DeleteCountry.Variables>(DELETE_COUNTRY, { id: GB.id });
+            const result1 = await adminClient.query<DeleteCountry.Mutation, DeleteCountry.Variables>(
+                DELETE_COUNTRY,
+                { id: GB.id },
+            );
 
 
             expect(result1.deleteCountry).toEqual({
             expect(result1.deleteCountry).toEqual({
                 result: DeletionResult.NOT_DELETED,
                 result: DeletionResult.NOT_DELETED,
                 message: 'The selected Country cannot be deleted as it is used in 1 Address',
                 message: 'The selected Country cannot be deleted as it is used in 1 Address',
             });
             });
 
 
-            const result2 = await client.query<GetCountryList.Query>(GET_COUNTRY_LIST, {});
+            const result2 = await adminClient.query<GetCountryList.Query>(GET_COUNTRY_LIST, {});
             expect(result2.countries.items.find(c => c.id === GB.id)).not.toBeUndefined();
             expect(result2.countries.items.find(c => c.id === GB.id)).not.toBeUndefined();
         });
         });
     });
     });

+ 95 - 94
packages/core/e2e/custom-fields.e2e-spec.ts

@@ -1,110 +1,111 @@
 // Force the timezone to avoid tests failing in other locales
 // Force the timezone to avoid tests failing in other locales
 process.env.TZ = 'UTC';
 process.env.TZ = 'UTC';
 
 
+import { LanguageCode } from '@vendure/common/lib/generated-types';
+import { CustomFields } from '@vendure/core/dist/config/custom-field/custom-field-types';
+import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
-import { LanguageCode } from '../../common/lib/generated-types';
-
-import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
-import { TestAdminClient, TestShopClient } from './test-client';
-import { TestServer } from './test-server';
+import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
+import { initialData } from './fixtures/e2e-initial-data';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 
 
 // tslint:disable:no-non-null-assertion
 // tslint:disable:no-non-null-assertion
 
 
+const customConfig = {
+    ...testConfig,
+    ...{
+        customFields: {
+            Product: [
+                { name: 'nullable', type: 'string' },
+                { name: 'notNullable', type: 'string', nullable: false, defaultValue: '' },
+                { name: 'stringWithDefault', type: 'string', defaultValue: 'hello' },
+                { name: 'localeStringWithDefault', type: 'localeString', defaultValue: 'hola' },
+                { name: 'intWithDefault', type: 'int', defaultValue: 5 },
+                { name: 'floatWithDefault', type: 'float', defaultValue: 5.5 },
+                { name: 'booleanWithDefault', type: 'boolean', defaultValue: true },
+                {
+                    name: 'dateTimeWithDefault',
+                    type: 'datetime',
+                    defaultValue: new Date('2019-04-30T12:59:16.4158386Z'),
+                },
+                { name: 'validateString', type: 'string', pattern: '^[0-9][a-z]+$' },
+                { name: 'validateLocaleString', type: 'localeString', pattern: '^[0-9][a-z]+$' },
+                { name: 'validateInt', type: 'int', min: 0, max: 10 },
+                { name: 'validateFloat', type: 'float', min: 0.5, max: 10.5 },
+                {
+                    name: 'validateDateTime',
+                    type: 'datetime',
+                    min: '2019-01-01T08:30',
+                    max: '2019-06-01T08:30',
+                },
+                {
+                    name: 'validateFn1',
+                    type: 'string',
+                    validate: value => {
+                        if (value !== 'valid') {
+                            return `The value ['${value}'] is not valid`;
+                        }
+                    },
+                },
+                {
+                    name: 'validateFn2',
+                    type: 'string',
+                    validate: value => {
+                        if (value !== 'valid') {
+                            return [
+                                {
+                                    languageCode: LanguageCode.en,
+                                    value: `The value ['${value}'] is not valid`,
+                                },
+                            ];
+                        }
+                    },
+                },
+                {
+                    name: 'stringWithOptions',
+                    type: 'string',
+                    options: [{ value: 'small' }, { value: 'medium' }, { value: 'large' }],
+                },
+                {
+                    name: 'nonPublic',
+                    type: 'string',
+                    defaultValue: 'hi!',
+                    public: false,
+                },
+                {
+                    name: 'public',
+                    type: 'string',
+                    defaultValue: 'ho!',
+                    public: true,
+                },
+                {
+                    name: 'longString',
+                    type: 'string',
+                },
+            ],
+            Facet: [
+                {
+                    name: 'translated',
+                    type: 'localeString',
+                },
+            ],
+        } as CustomFields,
+    },
+};
+
 describe('Custom fields', () => {
 describe('Custom fields', () => {
-    const adminClient = new TestAdminClient();
-    const shopClient = new TestShopClient();
-    const server = new TestServer();
+    const { server, adminClient, shopClient } = createTestEnvironment(customConfig);
 
 
     beforeAll(async () => {
     beforeAll(async () => {
-        await server.init(
-            {
-                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
-                customerCount: 1,
-            },
-            {
-                customFields: {
-                    Product: [
-                        { name: 'nullable', type: 'string' },
-                        { name: 'notNullable', type: 'string', nullable: false, defaultValue: '' },
-                        { name: 'stringWithDefault', type: 'string', defaultValue: 'hello' },
-                        { name: 'localeStringWithDefault', type: 'localeString', defaultValue: 'hola' },
-                        { name: 'intWithDefault', type: 'int', defaultValue: 5 },
-                        { name: 'floatWithDefault', type: 'float', defaultValue: 5.5 },
-                        { name: 'booleanWithDefault', type: 'boolean', defaultValue: true },
-                        {
-                            name: 'dateTimeWithDefault',
-                            type: 'datetime',
-                            defaultValue: new Date('2019-04-30T12:59:16.4158386Z'),
-                        },
-                        { name: 'validateString', type: 'string', pattern: '^[0-9][a-z]+$' },
-                        { name: 'validateLocaleString', type: 'localeString', pattern: '^[0-9][a-z]+$' },
-                        { name: 'validateInt', type: 'int', min: 0, max: 10 },
-                        { name: 'validateFloat', type: 'float', min: 0.5, max: 10.5 },
-                        {
-                            name: 'validateDateTime',
-                            type: 'datetime',
-                            min: '2019-01-01T08:30',
-                            max: '2019-06-01T08:30',
-                        },
-                        {
-                            name: 'validateFn1',
-                            type: 'string',
-                            validate: value => {
-                                if (value !== 'valid') {
-                                    return `The value ['${value}'] is not valid`;
-                                }
-                            },
-                        },
-                        {
-                            name: 'validateFn2',
-                            type: 'string',
-                            validate: value => {
-                                if (value !== 'valid') {
-                                    return [
-                                        {
-                                            languageCode: LanguageCode.en,
-                                            value: `The value ['${value}'] is not valid`,
-                                        },
-                                    ];
-                                }
-                            },
-                        },
-                        {
-                            name: 'stringWithOptions',
-                            type: 'string',
-                            options: [{ value: 'small' }, { value: 'medium' }, { value: 'large' }],
-                        },
-                        {
-                            name: 'nonPublic',
-                            type: 'string',
-                            defaultValue: 'hi!',
-                            public: false,
-                        },
-                        {
-                            name: 'public',
-                            type: 'string',
-                            defaultValue: 'ho!',
-                            public: true,
-                        },
-                        {
-                            name: 'longString',
-                            type: 'string',
-                        },
-                    ],
-                    Facet: [
-                        {
-                            name: 'translated',
-                            type: 'localeString',
-                        },
-                    ],
-                },
-            },
-        );
-        await adminClient.init();
-        await shopClient.init();
+        await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
+            customerCount: 1,
+        });
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
     }, TEST_SETUP_TIMEOUT_MS);
 
 
     afterAll(async () => {
     afterAll(async () => {

+ 37 - 37
packages/core/e2e/customer.e2e-spec.ts

@@ -1,14 +1,18 @@
 import { OnModuleInit } from '@nestjs/common';
 import { OnModuleInit } from '@nestjs/common';
 import { omit } from '@vendure/common/lib/omit';
 import { omit } from '@vendure/common/lib/omit';
+import {
+    AccountRegistrationEvent,
+    EventBus,
+    EventBusModule,
+    mergeConfig,
+    VendurePlugin,
+} from '@vendure/core';
+import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
-import { EventBus } from '../src/event-bus/event-bus';
-import { EventBusModule } from '../src/event-bus/event-bus.module';
-import { AccountRegistrationEvent } from '../src/event-bus/events/account-registration-event';
-import { VendurePlugin } from '../src/plugin/vendure-plugin';
-
-import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
+import { initialData } from './fixtures/e2e-initial-data';
 import { CUSTOMER_FRAGMENT } from './graphql/fragments';
 import { CUSTOMER_FRAGMENT } from './graphql/fragments';
 import {
 import {
     CreateAddress,
     CreateAddress,
@@ -25,32 +29,44 @@ import {
 import { AddItemToOrder } from './graphql/generated-e2e-shop-types';
 import { AddItemToOrder } from './graphql/generated-e2e-shop-types';
 import { GET_CUSTOMER, GET_CUSTOMER_LIST } from './graphql/shared-definitions';
 import { GET_CUSTOMER, GET_CUSTOMER_LIST } from './graphql/shared-definitions';
 import { ADD_ITEM_TO_ORDER } from './graphql/shop-definitions';
 import { ADD_ITEM_TO_ORDER } from './graphql/shop-definitions';
-import { TestAdminClient, TestShopClient } from './test-client';
-import { TestServer } from './test-server';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 
 
 // tslint:disable:no-non-null-assertion
 // tslint:disable:no-non-null-assertion
 let sendEmailFn: jest.Mock;
 let sendEmailFn: jest.Mock;
 
 
+/**
+ * This mock plugin simulates an EmailPlugin which would send emails
+ * on the registration & password reset events.
+ */
+@VendurePlugin({
+    imports: [EventBusModule],
+})
+class TestEmailPlugin implements OnModuleInit {
+    constructor(private eventBus: EventBus) {}
+    onModuleInit() {
+        this.eventBus.ofType(AccountRegistrationEvent).subscribe(event => {
+            sendEmailFn(event);
+        });
+    }
+}
+
 describe('Customer resolver', () => {
 describe('Customer resolver', () => {
-    const adminClient = new TestAdminClient();
-    const shopClient = new TestShopClient();
-    const server = new TestServer();
+    const { server, adminClient, shopClient } = createTestEnvironment(
+        mergeConfig(testConfig, { plugins: [TestEmailPlugin] }),
+    );
+
     let firstCustomer: GetCustomerList.Items;
     let firstCustomer: GetCustomerList.Items;
     let secondCustomer: GetCustomerList.Items;
     let secondCustomer: GetCustomerList.Items;
     let thirdCustomer: GetCustomerList.Items;
     let thirdCustomer: GetCustomerList.Items;
 
 
     beforeAll(async () => {
     beforeAll(async () => {
-        const token = await server.init(
-            {
-                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
-                customerCount: 5,
-            },
-            {
-                plugins: [TestEmailPlugin],
-            },
-        );
-        await adminClient.init();
+        await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
+            customerCount: 5,
+        });
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
     }, TEST_SETUP_TIMEOUT_MS);
 
 
     afterAll(async () => {
     afterAll(async () => {
@@ -486,19 +502,3 @@ const DELETE_CUSTOMER = gql`
         }
         }
     }
     }
 `;
 `;
-
-/**
- * This mock plugin simulates an EmailPlugin which would send emails
- * on the registration & password reset events.
- */
-@VendurePlugin({
-    imports: [EventBusModule],
-})
-class TestEmailPlugin implements OnModuleInit {
-    constructor(private eventBus: EventBus) {}
-    onModuleInit() {
-        this.eventBus.ofType(AccountRegistrationEvent).subscribe(event => {
-            sendEmailFn(event);
-        });
-    }
-}

+ 16 - 21
packages/core/e2e/default-search-plugin.e2e-spec.ts

@@ -1,12 +1,13 @@
 import { pick } from '@vendure/common/lib/pick';
 import { pick } from '@vendure/common/lib/pick';
+import { mergeConfig } from '@vendure/core';
+import { DefaultSearchPlugin } from '@vendure/core';
+import { facetValueCollectionFilter } from '@vendure/core/dist/config/collection/default-collection-filters';
+import { createTestEnvironment, SimpleGraphQLClient } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
-import { SimpleGraphQLClient } from '../mock-data/simple-graphql-client';
-import { facetValueCollectionFilter } from '../src/config/collection/default-collection-filters';
-import { DefaultSearchPlugin } from '../src/plugin/default-search-plugin/default-search-plugin';
-
-import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
+import { initialData } from './fixtures/e2e-initial-data';
 import {
 import {
     CreateCollection,
     CreateCollection,
     CreateFacet,
     CreateFacet,
@@ -29,27 +30,21 @@ import {
     UPDATE_TAX_RATE,
     UPDATE_TAX_RATE,
 } from './graphql/shared-definitions';
 } from './graphql/shared-definitions';
 import { SEARCH_PRODUCTS_SHOP } from './graphql/shop-definitions';
 import { SEARCH_PRODUCTS_SHOP } from './graphql/shop-definitions';
-import { TestAdminClient, TestShopClient } from './test-client';
-import { TestServer } from './test-server';
 import { awaitRunningJobs } from './utils/await-running-jobs';
 import { awaitRunningJobs } from './utils/await-running-jobs';
 
 
 describe('Default search plugin', () => {
 describe('Default search plugin', () => {
-    const adminClient = new TestAdminClient();
-    const shopClient = new TestShopClient();
-    const server = new TestServer();
+    const { server, adminClient, shopClient } = createTestEnvironment(
+        mergeConfig(testConfig, { plugins: [DefaultSearchPlugin] }),
+    );
 
 
     beforeAll(async () => {
     beforeAll(async () => {
-        const token = await server.init(
-            {
-                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
-                customerCount: 1,
-            },
-            {
-                plugins: [DefaultSearchPlugin],
-            },
-        );
-        await adminClient.init();
-        await shopClient.init();
+        await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
+            customerCount: 1,
+        });
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
     }, TEST_SETUP_TIMEOUT_MS);
 
 
     afterAll(async () => {
     afterAll(async () => {

+ 7 - 8
packages/core/e2e/entity-id-strategy.e2e-spec.ts

@@ -1,8 +1,10 @@
 /* tslint:disable:no-non-null-assertion */
 /* tslint:disable:no-non-null-assertion */
+import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
-import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
+import { initialData } from './fixtures/e2e-initial-data';
 import {
 import {
     IdTest1,
     IdTest1,
     IdTest2,
     IdTest2,
@@ -14,21 +16,18 @@ import {
     IdTest8,
     IdTest8,
     LanguageCode,
     LanguageCode,
 } from './graphql/generated-e2e-admin-types';
 } from './graphql/generated-e2e-admin-types';
-import { TestAdminClient, TestShopClient } from './test-client';
-import { TestServer } from './test-server';
 
 
 describe('EntityIdStrategy', () => {
 describe('EntityIdStrategy', () => {
-    const shopClient = new TestShopClient();
-    const adminClient = new TestAdminClient();
-    const server = new TestServer();
+    const { server, adminClient, shopClient } = createTestEnvironment(testConfig);
 
 
     beforeAll(async () => {
     beforeAll(async () => {
         await server.init({
         await server.init({
+            dataDir,
+            initialData,
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
             customerCount: 1,
             customerCount: 1,
         });
         });
-        await shopClient.init();
-        await adminClient.init();
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
     }, TEST_SETUP_TIMEOUT_MS);
 
 
     afterAll(async () => {
     afterAll(async () => {

+ 15 - 16
packages/core/e2e/entity-uuid-strategy.e2e-spec.ts

@@ -1,7 +1,8 @@
 /* tslint:disable:no-non-null-assertion */
 /* tslint:disable:no-non-null-assertion */
+import { UuidIdStrategy } from '@vendure/core';
+import { createTestEnvironment } from '@vendure/testing';
 import path from 'path';
 import path from 'path';
 
 
-import { UuidIdStrategy } from '../src/config/entity-id-strategy/uuid-id-strategy';
 // This import is here to simulate the behaviour of
 // This import is here to simulate the behaviour of
 // the package end-user importing symbols from the
 // the package end-user importing symbols from the
 // @vendure/core barrel file. Doing so will then cause the
 // @vendure/core barrel file. Doing so will then cause the
@@ -10,27 +11,25 @@ import { UuidIdStrategy } from '../src/config/entity-id-strategy/uuid-id-strateg
 // order of file evaluation.
 // order of file evaluation.
 import '../src/index';
 import '../src/index';
 
 
-import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
+import { initialData } from './fixtures/e2e-initial-data';
 import { GetProductList } from './graphql/generated-e2e-admin-types';
 import { GetProductList } from './graphql/generated-e2e-admin-types';
 import { GET_PRODUCT_LIST } from './graphql/shared-definitions';
 import { GET_PRODUCT_LIST } from './graphql/shared-definitions';
-import { TestAdminClient } from './test-client';
-import { TestServer } from './test-server';
 
 
 describe('UuidIdStrategy', () => {
 describe('UuidIdStrategy', () => {
-    const adminClient = new TestAdminClient();
-    const server = new TestServer();
+    const { server, adminClient } = createTestEnvironment({
+        ...testConfig,
+        entityIdStrategy: new UuidIdStrategy(),
+    });
 
 
     beforeAll(async () => {
     beforeAll(async () => {
-        await server.init(
-            {
-                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
-                customerCount: 1,
-            },
-            {
-                entityIdStrategy: new UuidIdStrategy(),
-            },
-        );
-        await adminClient.init();
+        await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
+            customerCount: 1,
+        });
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
     }, TEST_SETUP_TIMEOUT_MS);
 
 
     afterAll(async () => {
     afterAll(async () => {

+ 50 - 39
packages/core/e2e/facet.e2e-spec.ts

@@ -1,7 +1,9 @@
+import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
-import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
+import { initialData } from './fixtures/e2e-initial-data';
 import { FACET_VALUE_FRAGMENT, FACET_WITH_VALUES_FRAGMENT } from './graphql/fragments';
 import { FACET_VALUE_FRAGMENT, FACET_WITH_VALUES_FRAGMENT } from './graphql/fragments';
 import {
 import {
     CreateFacet,
     CreateFacet,
@@ -28,23 +30,23 @@ import {
     UPDATE_PRODUCT,
     UPDATE_PRODUCT,
     UPDATE_PRODUCT_VARIANTS,
     UPDATE_PRODUCT_VARIANTS,
 } from './graphql/shared-definitions';
 } from './graphql/shared-definitions';
-import { TestAdminClient } from './test-client';
-import { TestServer } from './test-server';
 
 
 // tslint:disable:no-non-null-assertion
 // tslint:disable:no-non-null-assertion
 
 
 describe('Facet resolver', () => {
 describe('Facet resolver', () => {
-    const client = new TestAdminClient();
-    const server = new TestServer();
+    const { server, adminClient } = createTestEnvironment(testConfig);
+
     let brandFacet: FacetWithValues.Fragment;
     let brandFacet: FacetWithValues.Fragment;
     let speakerTypeFacet: FacetWithValues.Fragment;
     let speakerTypeFacet: FacetWithValues.Fragment;
 
 
     beforeAll(async () => {
     beforeAll(async () => {
         await server.init({
         await server.init({
+            dataDir,
+            initialData,
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
             customerCount: 1,
             customerCount: 1,
         });
         });
-        await client.init();
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
     }, TEST_SETUP_TIMEOUT_MS);
 
 
     afterAll(async () => {
     afterAll(async () => {
@@ -52,7 +54,7 @@ describe('Facet resolver', () => {
     });
     });
 
 
     it('createFacet', async () => {
     it('createFacet', async () => {
-        const result = await client.query<CreateFacet.Mutation, CreateFacet.Variables>(CREATE_FACET, {
+        const result = await adminClient.query<CreateFacet.Mutation, CreateFacet.Variables>(CREATE_FACET, {
             input: {
             input: {
                 isPrivate: false,
                 isPrivate: false,
                 code: 'speaker-type',
                 code: 'speaker-type',
@@ -71,7 +73,7 @@ describe('Facet resolver', () => {
     });
     });
 
 
     it('updateFacet', async () => {
     it('updateFacet', async () => {
-        const result = await client.query<UpdateFacet.Mutation, UpdateFacet.Variables>(UPDATE_FACET, {
+        const result = await adminClient.query<UpdateFacet.Mutation, UpdateFacet.Variables>(UPDATE_FACET, {
             input: {
             input: {
                 id: speakerTypeFacet.id,
                 id: speakerTypeFacet.id,
                 translations: [{ languageCode: LanguageCode.en, name: 'Speaker Category' }],
                 translations: [{ languageCode: LanguageCode.en, name: 'Speaker Category' }],
@@ -82,7 +84,7 @@ describe('Facet resolver', () => {
     });
     });
 
 
     it('createFacetValues', async () => {
     it('createFacetValues', async () => {
-        const result = await client.query<CreateFacetValues.Mutation, CreateFacetValues.Variables>(
+        const result = await adminClient.query<CreateFacetValues.Mutation, CreateFacetValues.Variables>(
             CREATE_FACET_VALUES,
             CREATE_FACET_VALUES,
             {
             {
                 input: [
                 input: [
@@ -104,7 +106,7 @@ describe('Facet resolver', () => {
     });
     });
 
 
     it('updateFacetValues', async () => {
     it('updateFacetValues', async () => {
-        const result = await client.query<UpdateFacetValues.Mutation, UpdateFacetValues.Variables>(
+        const result = await adminClient.query<UpdateFacetValues.Mutation, UpdateFacetValues.Variables>(
             UPDATE_FACET_VALUES,
             UPDATE_FACET_VALUES,
             {
             {
                 input: [
                 input: [
@@ -120,7 +122,7 @@ describe('Facet resolver', () => {
     });
     });
 
 
     it('facets', async () => {
     it('facets', async () => {
-        const result = await client.query<GetFacetList.Query>(GET_FACET_LIST);
+        const result = await adminClient.query<GetFacetList.Query>(GET_FACET_LIST);
 
 
         const { items } = result.facets;
         const { items } = result.facets;
         expect(items.length).toBe(2);
         expect(items.length).toBe(2);
@@ -132,7 +134,7 @@ describe('Facet resolver', () => {
     });
     });
 
 
     it('facet', async () => {
     it('facet', async () => {
-        const result = await client.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
+        const result = await adminClient.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
             GET_FACET_WITH_VALUES,
             GET_FACET_WITH_VALUES,
             {
             {
                 id: speakerTypeFacet.id,
                 id: speakerTypeFacet.id,
@@ -147,19 +149,19 @@ describe('Facet resolver', () => {
 
 
         beforeAll(async () => {
         beforeAll(async () => {
             // add the FacetValues to products and variants
             // add the FacetValues to products and variants
-            const result1 = await client.query<GetProductListWithVariants.Query>(
+            const result1 = await adminClient.query<GetProductListWithVariants.Query>(
                 GET_PRODUCTS_LIST_WITH_VARIANTS,
                 GET_PRODUCTS_LIST_WITH_VARIANTS,
             );
             );
             products = result1.products.items;
             products = result1.products.items;
 
 
-            await client.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
+            await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
                 input: {
                 input: {
                     id: products[0].id,
                     id: products[0].id,
                     facetValueIds: [speakerTypeFacet.values[0].id],
                     facetValueIds: [speakerTypeFacet.values[0].id],
                 },
                 },
             });
             });
 
 
-            await client.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
+            await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
                 UPDATE_PRODUCT_VARIANTS,
                 UPDATE_PRODUCT_VARIANTS,
                 {
                 {
                     input: [
                     input: [
@@ -171,7 +173,7 @@ describe('Facet resolver', () => {
                 },
                 },
             );
             );
 
 
-            await client.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
+            await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
                 input: {
                 input: {
                     id: products[1].id,
                     id: products[1].id,
                     facetValueIds: [speakerTypeFacet.values[1].id],
                     facetValueIds: [speakerTypeFacet.values[1].id],
@@ -181,14 +183,14 @@ describe('Facet resolver', () => {
 
 
         it('deleteFacetValues deletes unused facetValue', async () => {
         it('deleteFacetValues deletes unused facetValue', async () => {
             const facetValueToDelete = speakerTypeFacet.values[2];
             const facetValueToDelete = speakerTypeFacet.values[2];
-            const result1 = await client.query<DeleteFacetValues.Mutation, DeleteFacetValues.Variables>(
+            const result1 = await adminClient.query<DeleteFacetValues.Mutation, DeleteFacetValues.Variables>(
                 DELETE_FACET_VALUES,
                 DELETE_FACET_VALUES,
                 {
                 {
                     ids: [facetValueToDelete.id],
                     ids: [facetValueToDelete.id],
                     force: false,
                     force: false,
                 },
                 },
             );
             );
-            const result2 = await client.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
+            const result2 = await adminClient.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
                 GET_FACET_WITH_VALUES,
                 GET_FACET_WITH_VALUES,
                 {
                 {
                     id: speakerTypeFacet.id,
                     id: speakerTypeFacet.id,
@@ -207,14 +209,14 @@ describe('Facet resolver', () => {
 
 
         it('deleteFacetValues for FacetValue in use returns NOT_DELETED', async () => {
         it('deleteFacetValues for FacetValue in use returns NOT_DELETED', async () => {
             const facetValueToDelete = speakerTypeFacet.values[0];
             const facetValueToDelete = speakerTypeFacet.values[0];
-            const result1 = await client.query<DeleteFacetValues.Mutation, DeleteFacetValues.Variables>(
+            const result1 = await adminClient.query<DeleteFacetValues.Mutation, DeleteFacetValues.Variables>(
                 DELETE_FACET_VALUES,
                 DELETE_FACET_VALUES,
                 {
                 {
                     ids: [facetValueToDelete.id],
                     ids: [facetValueToDelete.id],
                     force: false,
                     force: false,
                 },
                 },
             );
             );
-            const result2 = await client.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
+            const result2 = await adminClient.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
                 GET_FACET_WITH_VALUES,
                 GET_FACET_WITH_VALUES,
                 {
                 {
                     id: speakerTypeFacet.id,
                     id: speakerTypeFacet.id,
@@ -233,7 +235,7 @@ describe('Facet resolver', () => {
 
 
         it('deleteFacetValues for FacetValue in use can be force deleted', async () => {
         it('deleteFacetValues for FacetValue in use can be force deleted', async () => {
             const facetValueToDelete = speakerTypeFacet.values[0];
             const facetValueToDelete = speakerTypeFacet.values[0];
-            const result1 = await client.query<DeleteFacetValues.Mutation, DeleteFacetValues.Variables>(
+            const result1 = await adminClient.query<DeleteFacetValues.Mutation, DeleteFacetValues.Variables>(
                 DELETE_FACET_VALUES,
                 DELETE_FACET_VALUES,
                 {
                 {
                     ids: [facetValueToDelete.id],
                     ids: [facetValueToDelete.id],
@@ -249,7 +251,7 @@ describe('Facet resolver', () => {
             ]);
             ]);
 
 
             // FacetValue no longer in the Facet.values array
             // FacetValue no longer in the Facet.values array
-            const result2 = await client.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
+            const result2 = await adminClient.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
                 GET_FACET_WITH_VALUES,
                 GET_FACET_WITH_VALUES,
                 {
                 {
                     id: speakerTypeFacet.id,
                     id: speakerTypeFacet.id,
@@ -258,7 +260,7 @@ describe('Facet resolver', () => {
             expect(result2.facet!.values[0]).not.toEqual(facetValueToDelete);
             expect(result2.facet!.values[0]).not.toEqual(facetValueToDelete);
 
 
             // FacetValue no longer in the Product.facetValues array
             // FacetValue no longer in the Product.facetValues array
-            const result3 = await client.query<
+            const result3 = await adminClient.query<
                 GetProductWithVariants.Query,
                 GetProductWithVariants.Query,
                 GetProductWithVariants.Variables
                 GetProductWithVariants.Variables
             >(GET_PRODUCT_WITH_VARIANTS, {
             >(GET_PRODUCT_WITH_VARIANTS, {
@@ -268,11 +270,14 @@ describe('Facet resolver', () => {
         });
         });
 
 
         it('deleteFacet that is in use returns NOT_DELETED', async () => {
         it('deleteFacet that is in use returns NOT_DELETED', async () => {
-            const result1 = await client.query<DeleteFacet.Mutation, DeleteFacet.Variables>(DELETE_FACET, {
-                id: speakerTypeFacet.id,
-                force: false,
-            });
-            const result2 = await client.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
+            const result1 = await adminClient.query<DeleteFacet.Mutation, DeleteFacet.Variables>(
+                DELETE_FACET,
+                {
+                    id: speakerTypeFacet.id,
+                    force: false,
+                },
+            );
+            const result2 = await adminClient.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
                 GET_FACET_WITH_VALUES,
                 GET_FACET_WITH_VALUES,
                 {
                 {
                     id: speakerTypeFacet.id,
                     id: speakerTypeFacet.id,
@@ -288,10 +293,13 @@ describe('Facet resolver', () => {
         });
         });
 
 
         it('deleteFacet that is in use can be force deleted', async () => {
         it('deleteFacet that is in use can be force deleted', async () => {
-            const result1 = await client.query<DeleteFacet.Mutation, DeleteFacet.Variables>(DELETE_FACET, {
-                id: speakerTypeFacet.id,
-                force: true,
-            });
+            const result1 = await adminClient.query<DeleteFacet.Mutation, DeleteFacet.Variables>(
+                DELETE_FACET,
+                {
+                    id: speakerTypeFacet.id,
+                    force: true,
+                },
+            );
 
 
             expect(result1.deleteFacet).toEqual({
             expect(result1.deleteFacet).toEqual({
                 result: DeletionResult.DELETED,
                 result: DeletionResult.DELETED,
@@ -299,7 +307,7 @@ describe('Facet resolver', () => {
             });
             });
 
 
             // FacetValue no longer in the Facet.values array
             // FacetValue no longer in the Facet.values array
-            const result2 = await client.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
+            const result2 = await adminClient.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
                 GET_FACET_WITH_VALUES,
                 GET_FACET_WITH_VALUES,
                 {
                 {
                     id: speakerTypeFacet.id,
                     id: speakerTypeFacet.id,
@@ -308,7 +316,7 @@ describe('Facet resolver', () => {
             expect(result2.facet).toBe(null);
             expect(result2.facet).toBe(null);
 
 
             // FacetValue no longer in the Product.facetValues array
             // FacetValue no longer in the Product.facetValues array
-            const result3 = await client.query<
+            const result3 = await adminClient.query<
                 GetProductWithVariants.Query,
                 GetProductWithVariants.Query,
                 GetProductWithVariants.Variables
                 GetProductWithVariants.Variables
             >(GET_PRODUCT_WITH_VARIANTS, {
             >(GET_PRODUCT_WITH_VARIANTS, {
@@ -318,7 +326,7 @@ describe('Facet resolver', () => {
         });
         });
 
 
         it('deleteFacet with no FacetValues works', async () => {
         it('deleteFacet with no FacetValues works', async () => {
-            const { createFacet } = await client.query<CreateFacet.Mutation, CreateFacet.Variables>(
+            const { createFacet } = await adminClient.query<CreateFacet.Mutation, CreateFacet.Variables>(
                 CREATE_FACET,
                 CREATE_FACET,
                 {
                 {
                     input: {
                     input: {
@@ -328,10 +336,13 @@ describe('Facet resolver', () => {
                     },
                     },
                 },
                 },
             );
             );
-            const result = await client.query<DeleteFacet.Mutation, DeleteFacet.Variables>(DELETE_FACET, {
-                id: createFacet.id,
-                force: false,
-            });
+            const result = await adminClient.query<DeleteFacet.Mutation, DeleteFacet.Variables>(
+                DELETE_FACET,
+                {
+                    id: createFacet.id,
+                    force: false,
+                },
+            );
             expect(result.deleteFacet.result).toBe(DeletionResult.DELETED);
             expect(result.deleteFacet.result).toBe(DeletionResult.DELETED);
         });
         });
     });
     });

+ 126 - 0
packages/core/e2e/fixtures/test-payment-methods.ts

@@ -0,0 +1,126 @@
+import { PaymentMethodHandler } from '@vendure/core';
+
+import { LanguageCode } from '../graphql/generated-e2e-admin-types';
+
+export const testSuccessfulPaymentMethod = new PaymentMethodHandler({
+    code: 'test-payment-method',
+    description: [{ languageCode: LanguageCode.en, value: 'Test Payment Method' }],
+    args: {},
+    createPayment: (order, args, metadata) => {
+        return {
+            amount: order.total,
+            state: 'Settled',
+            transactionId: '12345',
+            metadata,
+        };
+    },
+    settlePayment: order => ({
+        success: true,
+    }),
+});
+
+/**
+ * A two-stage (authorize, capture) payment method, with no createRefund method.
+ */
+export const twoStagePaymentMethod = new PaymentMethodHandler({
+    code: 'authorize-only-payment-method',
+    description: [{ languageCode: LanguageCode.en, value: 'Test Payment Method' }],
+    args: {},
+    createPayment: (order, args, metadata) => {
+        return {
+            amount: order.total,
+            state: 'Authorized',
+            transactionId: '12345',
+            metadata,
+        };
+    },
+    settlePayment: () => {
+        return {
+            success: true,
+            metadata: {
+                moreData: 42,
+            },
+        };
+    },
+});
+
+/**
+ * A payment method which includes a createRefund method.
+ */
+export const singleStageRefundablePaymentMethod = new PaymentMethodHandler({
+    code: 'single-stage-refundable-payment-method',
+    description: [{ languageCode: LanguageCode.en, value: 'Test Payment Method' }],
+    args: {},
+    createPayment: (order, args, metadata) => {
+        return {
+            amount: order.total,
+            state: 'Settled',
+            transactionId: '12345',
+            metadata,
+        };
+    },
+    settlePayment: () => {
+        return { success: true };
+    },
+    createRefund: (input, total, order, payment, args) => {
+        return {
+            amount: total,
+            state: 'Settled',
+            transactionId: 'abc123',
+        };
+    },
+});
+
+/**
+ * A payment method where calling `settlePayment` always fails.
+ */
+export const failsToSettlePaymentMethod = new PaymentMethodHandler({
+    code: 'fails-to-settle-payment-method',
+    description: [{ languageCode: LanguageCode.en, value: 'Test Payment Method' }],
+    args: {},
+    createPayment: (order, args, metadata) => {
+        return {
+            amount: order.total,
+            state: 'Authorized',
+            transactionId: '12345',
+            metadata,
+        };
+    },
+    settlePayment: () => {
+        return {
+            success: false,
+            errorMessage: 'Something went horribly wrong',
+        };
+    },
+});
+export const testFailingPaymentMethod = new PaymentMethodHandler({
+    code: 'test-failing-payment-method',
+    description: [{ languageCode: LanguageCode.en, value: 'Test Failing Payment Method' }],
+    args: {},
+    createPayment: (order, args, metadata) => {
+        return {
+            amount: order.total,
+            state: 'Declined',
+            metadata,
+        };
+    },
+    settlePayment: order => ({
+        success: true,
+    }),
+});
+export const testErrorPaymentMethod = new PaymentMethodHandler({
+    code: 'test-error-payment-method',
+    description: [{ languageCode: LanguageCode.en, value: 'Test Error Payment Method' }],
+    args: {},
+    createPayment: (order, args, metadata) => {
+        return {
+            amount: order.total,
+            state: 'Error',
+            errorMessage: 'Something went horribly wrong',
+            metadata,
+        };
+    },
+    settlePayment: order => ({
+        success: true,
+    }),
+});

+ 31 - 20
packages/core/e2e/import.e2e-spec.ts

@@ -1,28 +1,27 @@
+import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
-import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
-import { TestAdminClient } from './test-client';
-import { TestServer } from './test-server';
+import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
+import { initialData } from './fixtures/e2e-initial-data';
 
 
 describe('Import resolver', () => {
 describe('Import resolver', () => {
-    const client = new TestAdminClient();
-    const server = new TestServer();
+    const { server, adminClient } = createTestEnvironment({
+        ...testConfig,
+        customFields: {
+            Product: [{ type: 'string', name: 'pageType' }],
+            ProductVariant: [{ type: 'int', name: 'weight' }],
+        },
+    });
 
 
     beforeAll(async () => {
     beforeAll(async () => {
-        const token = await server.init(
-            {
-                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-empty.csv'),
-                customerCount: 0,
-            },
-            {
-                customFields: {
-                    Product: [{ type: 'string', name: 'pageType' }],
-                    ProductVariant: [{ type: 'int', name: 'weight' }],
-                },
-            },
-        );
-        await client.init();
+        await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-empty.csv'),
+            customerCount: 0,
+        });
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
     }, TEST_SETUP_TIMEOUT_MS);
 
 
     afterAll(async () => {
     afterAll(async () => {
@@ -44,7 +43,19 @@ describe('Import resolver', () => {
         });
         });
 
 
         const csvFile = path.join(__dirname, 'fixtures', 'product-import.csv');
         const csvFile = path.join(__dirname, 'fixtures', 'product-import.csv');
-        const result = await client.importProducts(csvFile);
+        const result = await adminClient.fileUploadMutation({
+            mutation: gql`
+                mutation ImportProducts($csvFile: Upload!) {
+                    importProducts(csvFile: $csvFile) {
+                        imported
+                        processed
+                        errors
+                    }
+                }
+            `,
+            filePaths: [csvFile],
+            mapVariables: () => ({ csvFile: null }),
+        });
 
 
         expect(result.importProducts.errors).toEqual([
         expect(result.importProducts.errors).toEqual([
             'Invalid Record Length: header length is 16, got 1 on line 8',
             'Invalid Record Length: header length is 16, got 1 on line 8',
@@ -52,7 +63,7 @@ describe('Import resolver', () => {
         expect(result.importProducts.imported).toBe(4);
         expect(result.importProducts.imported).toBe(4);
         expect(result.importProducts.processed).toBe(4);
         expect(result.importProducts.processed).toBe(4);
 
 
-        const productResult = await client.query(
+        const productResult = await adminClient.query(
             gql`
             gql`
                 query GetProducts($options: ProductListOptions) {
                 query GetProducts($options: ProductListOptions) {
                     products(options: $options) {
                     products(options: $options) {

+ 27 - 24
packages/core/e2e/localization.e2e-spec.ts

@@ -1,9 +1,10 @@
+import { pick } from '@vendure/common/lib/pick';
+import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
-import { pick } from '../../common/lib/pick';
-
-import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
+import { initialData } from './fixtures/e2e-initial-data';
 import {
 import {
     GetProductWithVariants,
     GetProductWithVariants,
     LanguageCode,
     LanguageCode,
@@ -11,22 +12,21 @@ import {
     UpdateProduct,
     UpdateProduct,
 } from './graphql/generated-e2e-admin-types';
 } from './graphql/generated-e2e-admin-types';
 import { GET_PRODUCT_WITH_VARIANTS, UPDATE_PRODUCT } from './graphql/shared-definitions';
 import { GET_PRODUCT_WITH_VARIANTS, UPDATE_PRODUCT } from './graphql/shared-definitions';
-import { TestAdminClient } from './test-client';
-import { TestServer } from './test-server';
 
 
 /* tslint:disable:no-non-null-assertion */
 /* tslint:disable:no-non-null-assertion */
 describe('Role resolver', () => {
 describe('Role resolver', () => {
-    const client = new TestAdminClient();
-    const server = new TestServer();
+    const { server, adminClient } = createTestEnvironment(testConfig);
 
 
     beforeAll(async () => {
     beforeAll(async () => {
-        const token = await server.init({
+        await server.init({
+            dataDir,
+            initialData,
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
             customerCount: 1,
             customerCount: 1,
         });
         });
-        await client.init();
+        await adminClient.asSuperAdmin();
 
 
-        const { updateProduct } = await client.query<UpdateProduct.Mutation, UpdateProduct.Variables>(
+        const { updateProduct } = await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(
             UPDATE_PRODUCT,
             UPDATE_PRODUCT,
             {
             {
                 input: {
                 input: {
@@ -55,16 +55,19 @@ describe('Role resolver', () => {
             },
             },
         );
         );
 
 
-        await client.query<UpdateOptionGroup.Mutation, UpdateOptionGroup.Variables>(UPDATE_OPTION_GROUP, {
-            input: {
-                id: 'T_1',
-                translations: [
-                    { languageCode: LanguageCode.en, name: 'en name' },
-                    { languageCode: LanguageCode.de, name: 'de name' },
-                    { languageCode: LanguageCode.zh, name: 'zh name' },
-                ],
+        await adminClient.query<UpdateOptionGroup.Mutation, UpdateOptionGroup.Variables>(
+            UPDATE_OPTION_GROUP,
+            {
+                input: {
+                    id: 'T_1',
+                    translations: [
+                        { languageCode: LanguageCode.en, name: 'en name' },
+                        { languageCode: LanguageCode.de, name: 'de name' },
+                        { languageCode: LanguageCode.zh, name: 'zh name' },
+                    ],
+                },
             },
             },
-        });
+        );
     }, TEST_SETUP_TIMEOUT_MS);
     }, TEST_SETUP_TIMEOUT_MS);
 
 
     afterAll(async () => {
     afterAll(async () => {
@@ -72,7 +75,7 @@ describe('Role resolver', () => {
     });
     });
 
 
     it('returns default language when none specified', async () => {
     it('returns default language when none specified', async () => {
-        const { product } = await client.query<
+        const { product } = await adminClient.query<
             GetProductWithVariants.Query,
             GetProductWithVariants.Query,
             GetProductWithVariants.Variables
             GetProductWithVariants.Variables
         >(GET_PRODUCT_WITH_VARIANTS, {
         >(GET_PRODUCT_WITH_VARIANTS, {
@@ -86,7 +89,7 @@ describe('Role resolver', () => {
     });
     });
 
 
     it('returns specified language', async () => {
     it('returns specified language', async () => {
-        const { product } = await client.query<
+        const { product } = await adminClient.query<
             GetProductWithVariants.Query,
             GetProductWithVariants.Query,
             GetProductWithVariants.Variables
             GetProductWithVariants.Variables
         >(
         >(
@@ -104,7 +107,7 @@ describe('Role resolver', () => {
     });
     });
 
 
     it('falls back to default language code', async () => {
     it('falls back to default language code', async () => {
-        const { product } = await client.query<
+        const { product } = await adminClient.query<
             GetProductWithVariants.Query,
             GetProductWithVariants.Query,
             GetProductWithVariants.Variables
             GetProductWithVariants.Variables
         >(
         >(
@@ -122,7 +125,7 @@ describe('Role resolver', () => {
     });
     });
 
 
     it('nested entites are translated', async () => {
     it('nested entites are translated', async () => {
-        const { product } = await client.query<
+        const { product } = await adminClient.query<
             GetProductWithVariants.Query,
             GetProductWithVariants.Query,
             GetProductWithVariants.Variables
             GetProductWithVariants.Variables
         >(
         >(
@@ -138,7 +141,7 @@ describe('Role resolver', () => {
     });
     });
 
 
     it('translates results of mutation', async () => {
     it('translates results of mutation', async () => {
-        const { updateProduct } = await client.query<UpdateProduct.Mutation, UpdateProduct.Variables>(
+        const { updateProduct } = await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(
             UPDATE_PRODUCT,
             UPDATE_PRODUCT,
             {
             {
                 input: {
                 input: {

+ 24 - 30
packages/core/e2e/order-promotion.e2e-spec.ts

@@ -1,15 +1,18 @@
 /* tslint:disable:no-non-null-assertion */
 /* tslint:disable:no-non-null-assertion */
-import gql from 'graphql-tag';
-import path from 'path';
-
-import { pick } from '../../common/src/pick';
+import { pick } from '@vendure/common/lib/pick';
 import {
 import {
+    atLeastNWithFacets,
     discountOnItemWithFacets,
     discountOnItemWithFacets,
+    minimumOrderAmount,
     orderPercentageDiscount,
     orderPercentageDiscount,
-} from '../src/config/promotion/default-promotion-actions';
-import { atLeastNWithFacets, minimumOrderAmount } from '../src/config/promotion/default-promotion-conditions';
+} from '@vendure/core';
+import { createTestEnvironment } from '@vendure/testing';
+import gql from 'graphql-tag';
+import path from 'path';
 
 
-import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
+import { initialData } from './fixtures/e2e-initial-data';
+import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
 import {
 import {
     CreatePromotion,
     CreatePromotion,
     CreatePromotionInput,
     CreatePromotionInput,
@@ -36,19 +39,16 @@ import {
     REMOVE_COUPON_CODE,
     REMOVE_COUPON_CODE,
     SET_CUSTOMER,
     SET_CUSTOMER,
 } from './graphql/shop-definitions';
 } from './graphql/shop-definitions';
-import { TestAdminClient, TestShopClient } from './test-client';
-import { TestServer } from './test-server';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
-import {
-    addPaymentToOrder,
-    proceedToArrangingPayment,
-    testSuccessfulPaymentMethod,
-} from './utils/test-order-utils';
+import { addPaymentToOrder, proceedToArrangingPayment } from './utils/test-order-utils';
 
 
 describe('Promotions applied to Orders', () => {
 describe('Promotions applied to Orders', () => {
-    const adminClient = new TestAdminClient();
-    const shopClient = new TestShopClient();
-    const server = new TestServer();
+    const { server, adminClient, shopClient } = createTestEnvironment({
+        ...testConfig,
+        paymentOptions: {
+            paymentMethodHandlers: [testSuccessfulPaymentMethod],
+        },
+    });
 
 
     const freeOrderAction = {
     const freeOrderAction = {
         code: orderPercentageDiscount.code,
         code: orderPercentageDiscount.code,
@@ -65,19 +65,13 @@ describe('Promotions applied to Orders', () => {
     let products: GetPromoProducts.Items[];
     let products: GetPromoProducts.Items[];
 
 
     beforeAll(async () => {
     beforeAll(async () => {
-        const token = await server.init(
-            {
-                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-promotions.csv'),
-                customerCount: 2,
-            },
-            {
-                paymentOptions: {
-                    paymentMethodHandlers: [testSuccessfulPaymentMethod],
-                },
-            },
-        );
-        await shopClient.init();
-        await adminClient.init();
+        await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-promotions.csv'),
+            customerCount: 2,
+        });
+        await adminClient.asSuperAdmin();
 
 
         await getProducts();
         await getProducts();
         await createGlobalPromotions();
         await createGlobalPromotions();

+ 30 - 104
packages/core/e2e/order.e2e-spec.ts

@@ -1,13 +1,16 @@
 /* tslint:disable:no-non-null-assertion */
 /* tslint:disable:no-non-null-assertion */
-import { LanguageCode } from '@vendure/common/lib/generated-shop-types';
+import { pick } from '@vendure/common/lib/pick';
+import { createTestEnvironment, SimpleGraphQLClient } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
-import { HistoryEntryType, StockMovementType } from '../../common/lib/generated-types';
-import { pick } from '../../common/lib/pick';
-import { PaymentMethodHandler } from '../src/config/payment-method/payment-method-handler';
-
-import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
+import { initialData } from './fixtures/e2e-initial-data';
+import {
+    failsToSettlePaymentMethod,
+    singleStageRefundablePaymentMethod,
+    twoStagePaymentMethod,
+} from './fixtures/test-payment-methods';
 import { ORDER_FRAGMENT, ORDER_WITH_LINES_FRAGMENT } from './graphql/fragments';
 import { ORDER_FRAGMENT, ORDER_WITH_LINES_FRAGMENT } from './graphql/fragments';
 import {
 import {
     AddNoteToOrder,
     AddNoteToOrder,
@@ -22,10 +25,12 @@ import {
     GetOrderListFulfillments,
     GetOrderListFulfillments,
     GetProductWithVariants,
     GetProductWithVariants,
     GetStockMovement,
     GetStockMovement,
+    HistoryEntryType,
     OrderItemFragment,
     OrderItemFragment,
     RefundOrder,
     RefundOrder,
     SettlePayment,
     SettlePayment,
     SettleRefund,
     SettleRefund,
+    StockMovementType,
     UpdateProductVariants,
     UpdateProductVariants,
 } from './graphql/generated-e2e-admin-types';
 } from './graphql/generated-e2e-admin-types';
 import { AddItemToOrder, GetActiveOrder } from './graphql/generated-e2e-shop-types';
 import { AddItemToOrder, GetActiveOrder } from './graphql/generated-e2e-shop-types';
@@ -36,35 +41,31 @@ import {
     UPDATE_PRODUCT_VARIANTS,
     UPDATE_PRODUCT_VARIANTS,
 } from './graphql/shared-definitions';
 } from './graphql/shared-definitions';
 import { ADD_ITEM_TO_ORDER, GET_ACTIVE_ORDER } from './graphql/shop-definitions';
 import { ADD_ITEM_TO_ORDER, GET_ACTIVE_ORDER } from './graphql/shop-definitions';
-import { TestAdminClient, TestShopClient } from './test-client';
-import { TestServer } from './test-server';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 import { addPaymentToOrder, proceedToArrangingPayment } from './utils/test-order-utils';
 import { addPaymentToOrder, proceedToArrangingPayment } from './utils/test-order-utils';
 
 
 describe('Orders resolver', () => {
 describe('Orders resolver', () => {
-    const adminClient = new TestAdminClient();
-    const shopClient = new TestShopClient();
-    const server = new TestServer();
+    const { server, adminClient, shopClient } = createTestEnvironment({
+        ...testConfig,
+        paymentOptions: {
+            paymentMethodHandlers: [
+                twoStagePaymentMethod,
+                failsToSettlePaymentMethod,
+                singleStageRefundablePaymentMethod,
+            ],
+        },
+    });
     let customers: GetCustomerList.Items[];
     let customers: GetCustomerList.Items[];
     const password = 'test';
     const password = 'test';
 
 
     beforeAll(async () => {
     beforeAll(async () => {
-        const token = await server.init(
-            {
-                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
-                customerCount: 3,
-            },
-            {
-                paymentOptions: {
-                    paymentMethodHandlers: [
-                        twoStagePaymentMethod,
-                        failsToSettlePaymentMethod,
-                        singleStageRefundablePaymentMethod,
-                    ],
-                },
-            },
-        );
-        await adminClient.init();
+        await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
+            customerCount: 3,
+        });
+        await adminClient.asSuperAdmin();
 
 
         // Create a couple of orders to be queried
         // Create a couple of orders to be queried
         const result = await adminClient.query<GetCustomerList.Query, GetCustomerList.Variables>(
         const result = await adminClient.query<GetCustomerList.Query, GetCustomerList.Variables>(
@@ -1191,84 +1192,9 @@ describe('Orders resolver', () => {
     });
     });
 });
 });
 
 
-/**
- * A two-stage (authorize, capture) payment method, with no createRefund method.
- */
-const twoStagePaymentMethod = new PaymentMethodHandler({
-    code: 'authorize-only-payment-method',
-    description: [{ languageCode: LanguageCode.en, value: 'Test Payment Method' }],
-    args: {},
-    createPayment: (order, args, metadata) => {
-        return {
-            amount: order.total,
-            state: 'Authorized',
-            transactionId: '12345',
-            metadata,
-        };
-    },
-    settlePayment: () => {
-        return {
-            success: true,
-            metadata: {
-                moreData: 42,
-            },
-        };
-    },
-});
-
-/**
- * A payment method which includes a createRefund method.
- */
-const singleStageRefundablePaymentMethod = new PaymentMethodHandler({
-    code: 'single-stage-refundable-payment-method',
-    description: [{ languageCode: LanguageCode.en, value: 'Test Payment Method' }],
-    args: {},
-    createPayment: (order, args, metadata) => {
-        return {
-            amount: order.total,
-            state: 'Settled',
-            transactionId: '12345',
-            metadata,
-        };
-    },
-    settlePayment: () => {
-        return { success: true };
-    },
-    createRefund: (input, total, order, payment, args) => {
-        return {
-            amount: total,
-            state: 'Settled',
-            transactionId: 'abc123',
-        };
-    },
-});
-
-/**
- * A payment method where calling `settlePayment` always fails.
- */
-const failsToSettlePaymentMethod = new PaymentMethodHandler({
-    code: 'fails-to-settle-payment-method',
-    description: [{ languageCode: LanguageCode.en, value: 'Test Payment Method' }],
-    args: {},
-    createPayment: (order, args, metadata) => {
-        return {
-            amount: order.total,
-            state: 'Authorized',
-            transactionId: '12345',
-            metadata,
-        };
-    },
-    settlePayment: () => {
-        return {
-            success: false,
-            errorMessage: 'Something went horribly wrong',
-        };
-    },
-});
-
 async function createTestOrder(
 async function createTestOrder(
-    adminClient: TestAdminClient,
-    shopClient: TestShopClient,
+    adminClient: SimpleGraphQLClient,
+    shopClient: SimpleGraphQLClient,
     emailAddress: string,
     emailAddress: string,
     password: string,
     password: string,
 ): Promise<{
 ): Promise<{

+ 27 - 30
packages/core/e2e/plugin.e2e-spec.ts

@@ -1,10 +1,11 @@
 import { LanguageCode } from '@vendure/common/lib/generated-types';
 import { LanguageCode } from '@vendure/common/lib/generated-types';
+import { ConfigService } from '@vendure/core';
+import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
-import { ConfigService } from '../src/config/config.service';
-
-import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
+import { initialData } from './fixtures/e2e-initial-data';
 import {
 import {
     TestAPIExtensionPlugin,
     TestAPIExtensionPlugin,
     TestLazyExtensionPlugin,
     TestLazyExtensionPlugin,
@@ -12,42 +13,38 @@ import {
     TestPluginWithConfigAndBootstrap,
     TestPluginWithConfigAndBootstrap,
     TestPluginWithProvider,
     TestPluginWithProvider,
 } from './fixtures/test-plugins';
 } from './fixtures/test-plugins';
-import { TestAdminClient, TestShopClient } from './test-client';
-import { TestServer } from './test-server';
 
 
 describe('Plugins', () => {
 describe('Plugins', () => {
-    const adminClient = new TestAdminClient();
-    const shopClient = new TestShopClient();
-    const server = new TestServer();
     const bootstrapMockFn = jest.fn();
     const bootstrapMockFn = jest.fn();
     const onBootstrapFn = jest.fn();
     const onBootstrapFn = jest.fn();
     const onWorkerBootstrapFn = jest.fn();
     const onWorkerBootstrapFn = jest.fn();
     const onCloseFn = jest.fn();
     const onCloseFn = jest.fn();
     const onWorkerCloseFn = jest.fn();
     const onWorkerCloseFn = jest.fn();
 
 
+    const { server, adminClient, shopClient } = createTestEnvironment({
+        ...testConfig,
+        plugins: [
+            TestPluginWithAllLifecycleHooks.init(
+                onBootstrapFn,
+                onWorkerBootstrapFn,
+                onCloseFn,
+                onWorkerCloseFn,
+            ),
+            TestPluginWithConfigAndBootstrap.setup(bootstrapMockFn),
+            TestAPIExtensionPlugin,
+            TestPluginWithProvider,
+            TestLazyExtensionPlugin,
+        ],
+    });
+
     beforeAll(async () => {
     beforeAll(async () => {
-        const token = await server.init(
-            {
-                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
-                customerCount: 1,
-            },
-            {
-                plugins: [
-                    TestPluginWithAllLifecycleHooks.init(
-                        onBootstrapFn,
-                        onWorkerBootstrapFn,
-                        onCloseFn,
-                        onWorkerCloseFn,
-                    ),
-                    TestPluginWithConfigAndBootstrap.setup(bootstrapMockFn),
-                    TestAPIExtensionPlugin,
-                    TestPluginWithProvider,
-                    TestLazyExtensionPlugin,
-                ],
-            },
-        );
-        await adminClient.init();
-        await shopClient.init();
+        await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
+            customerCount: 1,
+        });
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
     }, TEST_SETUP_TIMEOUT_MS);
 
 
     afterAll(async () => {
     afterAll(async () => {

+ 17 - 13
packages/core/e2e/product-option.e2e-spec.ts

@@ -1,9 +1,11 @@
+import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
 import { omit } from '../../common/lib/omit';
 import { omit } from '../../common/lib/omit';
 
 
-import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
+import { initialData } from './fixtures/e2e-initial-data';
 import {
 import {
     CreateProductOption,
     CreateProductOption,
     CreateProductOptionGroup,
     CreateProductOptionGroup,
@@ -12,24 +14,23 @@ import {
     UpdateProductOption,
     UpdateProductOption,
     UpdateProductOptionGroup,
     UpdateProductOptionGroup,
 } from './graphql/generated-e2e-admin-types';
 } from './graphql/generated-e2e-admin-types';
-import { TestAdminClient } from './test-client';
-import { TestServer } from './test-server';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 
 
 // tslint:disable:no-non-null-assertion
 // tslint:disable:no-non-null-assertion
 
 
 describe('ProductOption resolver', () => {
 describe('ProductOption resolver', () => {
-    const client = new TestAdminClient();
-    const server = new TestServer();
+    const { server, adminClient } = createTestEnvironment(testConfig);
     let sizeGroup: ProductOptionGroupFragment;
     let sizeGroup: ProductOptionGroupFragment;
     let mediumOption: CreateProductOption.CreateProductOption;
     let mediumOption: CreateProductOption.CreateProductOption;
 
 
     beforeAll(async () => {
     beforeAll(async () => {
-        const token = await server.init({
+        await server.init({
+            dataDir,
+            initialData,
             customerCount: 1,
             customerCount: 1,
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
         });
         });
-        await client.init();
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
     }, TEST_SETUP_TIMEOUT_MS);
 
 
     afterAll(async () => {
     afterAll(async () => {
@@ -37,7 +38,7 @@ describe('ProductOption resolver', () => {
     });
     });
 
 
     it('createProductOptionGroup', async () => {
     it('createProductOptionGroup', async () => {
-        const { createProductOptionGroup } = await client.query<
+        const { createProductOptionGroup } = await adminClient.query<
             CreateProductOptionGroup.Mutation,
             CreateProductOptionGroup.Mutation,
             CreateProductOptionGroup.Variables
             CreateProductOptionGroup.Variables
         >(CREATE_PRODUCT_OPTION_GROUP, {
         >(CREATE_PRODUCT_OPTION_GROUP, {
@@ -75,7 +76,7 @@ describe('ProductOption resolver', () => {
     });
     });
 
 
     it('updateProductOptionGroup', async () => {
     it('updateProductOptionGroup', async () => {
-        const { updateProductOptionGroup } = await client.query<
+        const { updateProductOptionGroup } = await adminClient.query<
             UpdateProductOptionGroup.Mutation,
             UpdateProductOptionGroup.Mutation,
             UpdateProductOptionGroup.Variables
             UpdateProductOptionGroup.Variables
         >(UPDATE_PRODUCT_OPTION_GROUP, {
         >(UPDATE_PRODUCT_OPTION_GROUP, {
@@ -93,7 +94,7 @@ describe('ProductOption resolver', () => {
     it(
     it(
         'createProductOption throws with invalid productOptionGroupId',
         'createProductOption throws with invalid productOptionGroupId',
         assertThrowsWithMessage(async () => {
         assertThrowsWithMessage(async () => {
-            const { createProductOption } = await client.query<
+            const { createProductOption } = await adminClient.query<
                 CreateProductOption.Mutation,
                 CreateProductOption.Mutation,
                 CreateProductOption.Variables
                 CreateProductOption.Variables
             >(CREATE_PRODUCT_OPTION, {
             >(CREATE_PRODUCT_OPTION, {
@@ -106,11 +107,11 @@ describe('ProductOption resolver', () => {
                     ],
                     ],
                 },
                 },
             });
             });
-        }, 'No ProductOptionGroup with the id \'999\' could be found'),
+        }, "No ProductOptionGroup with the id '999' could be found"),
     );
     );
 
 
     it('createProductOption', async () => {
     it('createProductOption', async () => {
-        const { createProductOption } = await client.query<
+        const { createProductOption } = await adminClient.query<
             CreateProductOption.Mutation,
             CreateProductOption.Mutation,
             CreateProductOption.Variables
             CreateProductOption.Variables
         >(CREATE_PRODUCT_OPTION, {
         >(CREATE_PRODUCT_OPTION, {
@@ -134,7 +135,10 @@ describe('ProductOption resolver', () => {
     });
     });
 
 
     it('updateProductOption', async () => {
     it('updateProductOption', async () => {
-        const { updateProductOption } = await client.query<UpdateProductOption.Mutation, UpdateProductOption.Variables>(UPDATE_PRODUCT_OPTION, {
+        const { updateProductOption } = await adminClient.query<
+            UpdateProductOption.Mutation,
+            UpdateProductOption.Variables
+        >(UPDATE_PRODUCT_OPTION, {
             input: {
             input: {
                 id: 'T_7',
                 id: 'T_7',
                 translations: [
                 translations: [

+ 8 - 9
packages/core/e2e/product.e2e-spec.ts

@@ -1,10 +1,12 @@
 import { omit } from '@vendure/common/lib/omit';
 import { omit } from '@vendure/common/lib/omit';
 import { pick } from '@vendure/common/lib/pick';
 import { pick } from '@vendure/common/lib/pick';
 import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
+import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
-import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
+import { initialData } from './fixtures/e2e-initial-data';
 import {
 import {
     AddOptionGroupToProduct,
     AddOptionGroupToProduct,
     CreateProduct,
     CreateProduct,
@@ -35,24 +37,21 @@ import {
     UPDATE_PRODUCT,
     UPDATE_PRODUCT,
     UPDATE_PRODUCT_VARIANTS,
     UPDATE_PRODUCT_VARIANTS,
 } from './graphql/shared-definitions';
 } from './graphql/shared-definitions';
-import { TestAdminClient, TestShopClient } from './test-client';
-import { TestServer } from './test-server';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 
 
 // tslint:disable:no-non-null-assertion
 // tslint:disable:no-non-null-assertion
 
 
 describe('Product resolver', () => {
 describe('Product resolver', () => {
-    const adminClient = new TestAdminClient();
-    const shopClient = new TestShopClient();
-    const server = new TestServer();
+    const { server, adminClient, shopClient } = createTestEnvironment(testConfig);
 
 
     beforeAll(async () => {
     beforeAll(async () => {
-        const token = await server.init({
+        await server.init({
+            dataDir,
+            initialData,
             customerCount: 1,
             customerCount: 1,
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
         });
         });
-        await adminClient.init();
-        await shopClient.init();
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
     }, TEST_SETUP_TIMEOUT_MS);
 
 
     afterAll(async () => {
     afterAll(async () => {

+ 40 - 40
packages/core/e2e/promotion.e2e-spec.ts

@@ -1,12 +1,11 @@
 import { pick } from '@vendure/common/lib/pick';
 import { pick } from '@vendure/common/lib/pick';
+import { PromotionAction, PromotionCondition, PromotionOrderAction } from '@vendure/core';
+import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
-import { LanguageCode } from '../../common/lib/generated-types';
-import { PromotionAction, PromotionOrderAction } from '../src/config/promotion/promotion-action';
-import { PromotionCondition } from '../src/config/promotion/promotion-condition';
-
-import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
+import { initialData } from './fixtures/e2e-initial-data';
 import { PROMOTION_FRAGMENT } from './graphql/fragments';
 import { PROMOTION_FRAGMENT } from './graphql/fragments';
 import {
 import {
     CreatePromotion,
     CreatePromotion,
@@ -15,25 +14,28 @@ import {
     GetAdjustmentOperations,
     GetAdjustmentOperations,
     GetPromotion,
     GetPromotion,
     GetPromotionList,
     GetPromotionList,
+    LanguageCode,
     Promotion,
     Promotion,
     UpdatePromotion,
     UpdatePromotion,
 } from './graphql/generated-e2e-admin-types';
 } from './graphql/generated-e2e-admin-types';
 import { CREATE_PROMOTION } from './graphql/shared-definitions';
 import { CREATE_PROMOTION } from './graphql/shared-definitions';
-import { TestAdminClient } from './test-client';
-import { TestServer } from './test-server';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 
 
 // tslint:disable:no-non-null-assertion
 // tslint:disable:no-non-null-assertion
 
 
 describe('Promotion resolver', () => {
 describe('Promotion resolver', () => {
-    const client = new TestAdminClient();
-    const server = new TestServer();
-
     const promoCondition = generateTestCondition('promo_condition');
     const promoCondition = generateTestCondition('promo_condition');
     const promoCondition2 = generateTestCondition('promo_condition2');
     const promoCondition2 = generateTestCondition('promo_condition2');
-
     const promoAction = generateTestAction('promo_action');
     const promoAction = generateTestAction('promo_action');
 
 
+    const { server, adminClient, shopClient } = createTestEnvironment({
+        ...testConfig,
+        promotionOptions: {
+            promotionConditions: [promoCondition, promoCondition2],
+            promotionActions: [promoAction],
+        },
+    });
+
     const snapshotProps: Array<keyof Promotion.Fragment> = [
     const snapshotProps: Array<keyof Promotion.Fragment> = [
         'name',
         'name',
         'actions',
         'actions',
@@ -46,19 +48,13 @@ describe('Promotion resolver', () => {
     let promotion: Promotion.Fragment;
     let promotion: Promotion.Fragment;
 
 
     beforeAll(async () => {
     beforeAll(async () => {
-        await server.init(
-            {
-                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
-                customerCount: 1,
-            },
-            {
-                promotionOptions: {
-                    promotionConditions: [promoCondition, promoCondition2],
-                    promotionActions: [promoAction],
-                },
-            },
-        );
-        await client.init();
+        await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
+            customerCount: 1,
+        });
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
     }, TEST_SETUP_TIMEOUT_MS);
 
 
     afterAll(async () => {
     afterAll(async () => {
@@ -66,7 +62,7 @@ describe('Promotion resolver', () => {
     });
     });
 
 
     it('createPromotion', async () => {
     it('createPromotion', async () => {
-        const result = await client.query<CreatePromotion.Mutation, CreatePromotion.Variables>(
+        const result = await adminClient.query<CreatePromotion.Mutation, CreatePromotion.Variables>(
             CREATE_PROMOTION,
             CREATE_PROMOTION,
             {
             {
                 input: {
                 input: {
@@ -103,7 +99,7 @@ describe('Promotion resolver', () => {
     it(
     it(
         'createPromotion throws with empty conditions and no couponCode',
         'createPromotion throws with empty conditions and no couponCode',
         assertThrowsWithMessage(async () => {
         assertThrowsWithMessage(async () => {
-            await client.query<CreatePromotion.Mutation, CreatePromotion.Variables>(CREATE_PROMOTION, {
+            await adminClient.query<CreatePromotion.Mutation, CreatePromotion.Variables>(CREATE_PROMOTION, {
                 input: {
                 input: {
                     name: 'bad promotion',
                     name: 'bad promotion',
                     enabled: true,
                     enabled: true,
@@ -126,7 +122,7 @@ describe('Promotion resolver', () => {
     );
     );
 
 
     it('updatePromotion', async () => {
     it('updatePromotion', async () => {
-        const result = await client.query<UpdatePromotion.Mutation, UpdatePromotion.Variables>(
+        const result = await adminClient.query<UpdatePromotion.Mutation, UpdatePromotion.Variables>(
             UPDATE_PROMOTION,
             UPDATE_PROMOTION,
             {
             {
                 input: {
                 input: {
@@ -153,7 +149,7 @@ describe('Promotion resolver', () => {
     it(
     it(
         'updatePromotion throws with empty conditions and no couponCode',
         'updatePromotion throws with empty conditions and no couponCode',
         assertThrowsWithMessage(async () => {
         assertThrowsWithMessage(async () => {
-            await client.query<UpdatePromotion.Mutation, UpdatePromotion.Variables>(UPDATE_PROMOTION, {
+            await adminClient.query<UpdatePromotion.Mutation, UpdatePromotion.Variables>(UPDATE_PROMOTION, {
                 input: {
                 input: {
                     id: promotion.id,
                     id: promotion.id,
                     couponCode: '',
                     couponCode: '',
@@ -164,7 +160,7 @@ describe('Promotion resolver', () => {
     );
     );
 
 
     it('promotion', async () => {
     it('promotion', async () => {
-        const result = await client.query<GetPromotion.Query, GetPromotion.Variables>(GET_PROMOTION, {
+        const result = await adminClient.query<GetPromotion.Query, GetPromotion.Variables>(GET_PROMOTION, {
             id: promotion.id,
             id: promotion.id,
         });
         });
 
 
@@ -172,7 +168,7 @@ describe('Promotion resolver', () => {
     });
     });
 
 
     it('promotions', async () => {
     it('promotions', async () => {
-        const result = await client.query<GetPromotionList.Query, GetPromotionList.Variables>(
+        const result = await adminClient.query<GetPromotionList.Query, GetPromotionList.Variables>(
             GET_PROMOTION_LIST,
             GET_PROMOTION_LIST,
             {},
             {},
         );
         );
@@ -182,9 +178,10 @@ describe('Promotion resolver', () => {
     });
     });
 
 
     it('adjustmentOperations', async () => {
     it('adjustmentOperations', async () => {
-        const result = await client.query<GetAdjustmentOperations.Query, GetAdjustmentOperations.Variables>(
-            GET_ADJUSTMENT_OPERATIONS,
-        );
+        const result = await adminClient.query<
+            GetAdjustmentOperations.Query,
+            GetAdjustmentOperations.Variables
+        >(GET_ADJUSTMENT_OPERATIONS);
 
 
         expect(result.promotionActions).toMatchSnapshot();
         expect(result.promotionActions).toMatchSnapshot();
         expect(result.promotionConditions).toMatchSnapshot();
         expect(result.promotionConditions).toMatchSnapshot();
@@ -195,13 +192,13 @@ describe('Promotion resolver', () => {
         let promotionToDelete: GetPromotionList.Items;
         let promotionToDelete: GetPromotionList.Items;
 
 
         beforeAll(async () => {
         beforeAll(async () => {
-            const result = await client.query<GetPromotionList.Query>(GET_PROMOTION_LIST);
+            const result = await adminClient.query<GetPromotionList.Query>(GET_PROMOTION_LIST);
             allPromotions = result.promotions.items;
             allPromotions = result.promotions.items;
         });
         });
 
 
         it('deletes a promotion', async () => {
         it('deletes a promotion', async () => {
             promotionToDelete = allPromotions[0];
             promotionToDelete = allPromotions[0];
-            const result = await client.query<DeletePromotion.Mutation, DeletePromotion.Variables>(
+            const result = await adminClient.query<DeletePromotion.Mutation, DeletePromotion.Variables>(
                 DELETE_PROMOTION,
                 DELETE_PROMOTION,
                 { id: promotionToDelete.id },
                 { id: promotionToDelete.id },
             );
             );
@@ -210,15 +207,18 @@ describe('Promotion resolver', () => {
         });
         });
 
 
         it('cannot get a deleted promotion', async () => {
         it('cannot get a deleted promotion', async () => {
-            const result = await client.query<GetPromotion.Query, GetPromotion.Variables>(GET_PROMOTION, {
-                id: promotionToDelete.id,
-            });
+            const result = await adminClient.query<GetPromotion.Query, GetPromotion.Variables>(
+                GET_PROMOTION,
+                {
+                    id: promotionToDelete.id,
+                },
+            );
 
 
             expect(result.promotion).toBe(null);
             expect(result.promotion).toBe(null);
         });
         });
 
 
         it('deleted promotion omitted from list', async () => {
         it('deleted promotion omitted from list', async () => {
-            const result = await client.query<GetPromotionList.Query>(GET_PROMOTION_LIST);
+            const result = await adminClient.query<GetPromotionList.Query>(GET_PROMOTION_LIST);
 
 
             expect(result.promotions.items.length).toBe(allPromotions.length - 1);
             expect(result.promotions.items.length).toBe(allPromotions.length - 1);
             expect(result.promotions.items.map(c => c.id).includes(promotionToDelete.id)).toBe(false);
             expect(result.promotions.items.map(c => c.id).includes(promotionToDelete.id)).toBe(false);
@@ -228,7 +228,7 @@ describe('Promotion resolver', () => {
             'updatePromotion throws for deleted promotion',
             'updatePromotion throws for deleted promotion',
             assertThrowsWithMessage(
             assertThrowsWithMessage(
                 () =>
                 () =>
-                    client.query<UpdatePromotion.Mutation, UpdatePromotion.Variables>(UPDATE_PROMOTION, {
+                    adminClient.query<UpdatePromotion.Mutation, UpdatePromotion.Variables>(UPDATE_PROMOTION, {
                         input: {
                         input: {
                             id: promotionToDelete.id,
                             id: promotionToDelete.id,
                             enabled: false,
                             enabled: false,

+ 37 - 28
packages/core/e2e/role.e2e-spec.ts

@@ -1,9 +1,11 @@
 import { omit } from '@vendure/common/lib/omit';
 import { omit } from '@vendure/common/lib/omit';
 import { CUSTOMER_ROLE_CODE, SUPER_ADMIN_ROLE_CODE } from '@vendure/common/lib/shared-constants';
 import { CUSTOMER_ROLE_CODE, SUPER_ADMIN_ROLE_CODE } from '@vendure/common/lib/shared-constants';
+import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
-import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
+import { initialData } from './fixtures/e2e-initial-data';
 import { ROLE_FRAGMENT } from './graphql/fragments';
 import { ROLE_FRAGMENT } from './graphql/fragments';
 import {
 import {
     CreateRole,
     CreateRole,
@@ -14,22 +16,21 @@ import {
     UpdateRole,
     UpdateRole,
 } from './graphql/generated-e2e-admin-types';
 } from './graphql/generated-e2e-admin-types';
 import { CREATE_ROLE } from './graphql/shared-definitions';
 import { CREATE_ROLE } from './graphql/shared-definitions';
-import { TestAdminClient } from './test-client';
-import { TestServer } from './test-server';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 
 
 describe('Role resolver', () => {
 describe('Role resolver', () => {
-    const client = new TestAdminClient();
-    const server = new TestServer();
+    const { server, adminClient } = createTestEnvironment(testConfig);
     let createdRole: Role.Fragment;
     let createdRole: Role.Fragment;
     let defaultRoles: Role.Fragment[];
     let defaultRoles: Role.Fragment[];
 
 
     beforeAll(async () => {
     beforeAll(async () => {
-        const token = await server.init({
+        await server.init({
+            dataDir,
+            initialData,
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
             customerCount: 1,
             customerCount: 1,
         });
         });
-        await client.init();
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
     }, TEST_SETUP_TIMEOUT_MS);
 
 
     afterAll(async () => {
     afterAll(async () => {
@@ -37,7 +38,7 @@ describe('Role resolver', () => {
     });
     });
 
 
     it('roles', async () => {
     it('roles', async () => {
-        const result = await client.query<GetRoles.Query, GetRoles.Variables>(GET_ROLES);
+        const result = await adminClient.query<GetRoles.Query, GetRoles.Variables>(GET_ROLES);
 
 
         defaultRoles = result.roles.items;
         defaultRoles = result.roles.items;
         expect(result.roles.items.length).toBe(2);
         expect(result.roles.items.length).toBe(2);
@@ -47,7 +48,7 @@ describe('Role resolver', () => {
     it(
     it(
         'createRole with invalid permission',
         'createRole with invalid permission',
         assertThrowsWithMessage(async () => {
         assertThrowsWithMessage(async () => {
-            await client.query<CreateRole.Mutation, CreateRole.Variables>(CREATE_ROLE, {
+            await adminClient.query<CreateRole.Mutation, CreateRole.Variables>(CREATE_ROLE, {
                 input: {
                 input: {
                     code: 'test',
                     code: 'test',
                     description: 'test role',
                     description: 'test role',
@@ -58,13 +59,16 @@ describe('Role resolver', () => {
     );
     );
 
 
     it('createRole with no permissions includes Authenticated', async () => {
     it('createRole with no permissions includes Authenticated', async () => {
-        const { createRole } = await client.query<CreateRole.Mutation, CreateRole.Variables>(CREATE_ROLE, {
-            input: {
-                code: 'test',
-                description: 'test role',
-                permissions: [],
+        const { createRole } = await adminClient.query<CreateRole.Mutation, CreateRole.Variables>(
+            CREATE_ROLE,
+            {
+                input: {
+                    code: 'test',
+                    description: 'test role',
+                    permissions: [],
+                },
             },
             },
-        });
+        );
 
 
         expect(omit(createRole, ['channels'])).toEqual({
         expect(omit(createRole, ['channels'])).toEqual({
             code: 'test',
             code: 'test',
@@ -75,13 +79,16 @@ describe('Role resolver', () => {
     });
     });
 
 
     it('createRole deduplicates permissions', async () => {
     it('createRole deduplicates permissions', async () => {
-        const { createRole } = await client.query<CreateRole.Mutation, CreateRole.Variables>(CREATE_ROLE, {
-            input: {
-                code: 'test2',
-                description: 'test role2',
-                permissions: [Permission.ReadSettings, Permission.ReadSettings],
+        const { createRole } = await adminClient.query<CreateRole.Mutation, CreateRole.Variables>(
+            CREATE_ROLE,
+            {
+                input: {
+                    code: 'test2',
+                    description: 'test role2',
+                    permissions: [Permission.ReadSettings, Permission.ReadSettings],
+                },
             },
             },
-        });
+        );
 
 
         expect(omit(createRole, ['channels'])).toEqual({
         expect(omit(createRole, ['channels'])).toEqual({
             code: 'test2',
             code: 'test2',
@@ -92,7 +99,7 @@ describe('Role resolver', () => {
     });
     });
 
 
     it('createRole with permissions', async () => {
     it('createRole with permissions', async () => {
-        const result = await client.query<CreateRole.Mutation, CreateRole.Variables>(CREATE_ROLE, {
+        const result = await adminClient.query<CreateRole.Mutation, CreateRole.Variables>(CREATE_ROLE, {
             input: {
             input: {
                 code: 'test',
                 code: 'test',
                 description: 'test role',
                 description: 'test role',
@@ -110,12 +117,14 @@ describe('Role resolver', () => {
     });
     });
 
 
     it('role', async () => {
     it('role', async () => {
-        const result = await client.query<GetRole.Query, GetRole.Variables>(GET_ROLE, { id: createdRole.id });
+        const result = await adminClient.query<GetRole.Query, GetRole.Variables>(GET_ROLE, {
+            id: createdRole.id,
+        });
         expect(result.role).toEqual(createdRole);
         expect(result.role).toEqual(createdRole);
     });
     });
 
 
     it('updateRole', async () => {
     it('updateRole', async () => {
-        const result = await client.query<UpdateRole.Mutation, UpdateRole.Variables>(UPDATE_ROLE, {
+        const result = await adminClient.query<UpdateRole.Mutation, UpdateRole.Variables>(UPDATE_ROLE, {
             input: {
             input: {
                 id: createdRole.id,
                 id: createdRole.id,
                 code: 'test-modified',
                 code: 'test-modified',
@@ -138,7 +147,7 @@ describe('Role resolver', () => {
     });
     });
 
 
     it('updateRole works with partial input', async () => {
     it('updateRole works with partial input', async () => {
-        const result = await client.query<UpdateRole.Mutation, UpdateRole.Variables>(UPDATE_ROLE, {
+        const result = await adminClient.query<UpdateRole.Mutation, UpdateRole.Variables>(UPDATE_ROLE, {
             input: {
             input: {
                 id: createdRole.id,
                 id: createdRole.id,
                 code: 'test-modified-again',
                 code: 'test-modified-again',
@@ -156,7 +165,7 @@ describe('Role resolver', () => {
     });
     });
 
 
     it('updateRole deduplicates permissions', async () => {
     it('updateRole deduplicates permissions', async () => {
-        const result = await client.query<UpdateRole.Mutation, UpdateRole.Variables>(UPDATE_ROLE, {
+        const result = await adminClient.query<UpdateRole.Mutation, UpdateRole.Variables>(UPDATE_ROLE, {
             input: {
             input: {
                 id: createdRole.id,
                 id: createdRole.id,
                 permissions: [
                 permissions: [
@@ -179,7 +188,7 @@ describe('Role resolver', () => {
                 fail(`Could not find SuperAdmin role`);
                 fail(`Could not find SuperAdmin role`);
                 return;
                 return;
             }
             }
-            return client.query<UpdateRole.Mutation, UpdateRole.Variables>(UPDATE_ROLE, {
+            return adminClient.query<UpdateRole.Mutation, UpdateRole.Variables>(UPDATE_ROLE, {
                 input: {
                 input: {
                     id: superAdminRole.id,
                     id: superAdminRole.id,
                     code: 'superadmin-modified',
                     code: 'superadmin-modified',
@@ -198,7 +207,7 @@ describe('Role resolver', () => {
                 fail(`Could not find Customer role`);
                 fail(`Could not find Customer role`);
                 return;
                 return;
             }
             }
-            return client.query<UpdateRole.Mutation, UpdateRole.Variables>(UPDATE_ROLE, {
+            return adminClient.query<UpdateRole.Mutation, UpdateRole.Variables>(UPDATE_ROLE, {
                 input: {
                 input: {
                     id: customerRole.id,
                     id: customerRole.id,
                     code: 'customer-modified',
                     code: 'customer-modified',

+ 41 - 43
packages/core/e2e/shipping-method.e2e-spec.ts

@@ -1,13 +1,15 @@
 /* tslint:disable:no-non-null-assertion */
 /* tslint:disable:no-non-null-assertion */
+import {
+    defaultShippingCalculator,
+    defaultShippingEligibilityChecker,
+    ShippingCalculator,
+} from '@vendure/core';
+import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
-import { LanguageCode } from '../../common/lib/generated-types';
-import { defaultShippingCalculator } from '../src/config/shipping-method/default-shipping-calculator';
-import { defaultShippingEligibilityChecker } from '../src/config/shipping-method/default-shipping-eligibility-checker';
-import { ShippingCalculator } from '../src/config/shipping-method/shipping-calculator';
-
-import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
+import { initialData } from './fixtures/e2e-initial-data';
 import {
 import {
     CreateShippingMethod,
     CreateShippingMethod,
     DeleteShippingMethod,
     DeleteShippingMethod,
@@ -16,33 +18,47 @@ import {
     GetEligibilityCheckers,
     GetEligibilityCheckers,
     GetShippingMethod,
     GetShippingMethod,
     GetShippingMethodList,
     GetShippingMethodList,
+    LanguageCode,
     TestEligibleMethods,
     TestEligibleMethods,
     TestShippingMethod,
     TestShippingMethod,
     UpdateShippingMethod,
     UpdateShippingMethod,
 } from './graphql/generated-e2e-admin-types';
 } from './graphql/generated-e2e-admin-types';
-import { TestAdminClient, TestShopClient } from './test-client';
-import { TestServer } from './test-server';
+
+const TEST_METADATA = {
+    foo: 'bar',
+    baz: [1, 2, 3],
+};
+
+const calculatorWithMetadata = new ShippingCalculator({
+    code: 'calculator-with-metadata',
+    description: [{ languageCode: LanguageCode.en, value: 'Has metadata' }],
+    args: {},
+    calculate: order => {
+        return {
+            price: 100,
+            priceWithTax: 100,
+            metadata: TEST_METADATA,
+        };
+    },
+});
 
 
 describe('ShippingMethod resolver', () => {
 describe('ShippingMethod resolver', () => {
-    const adminClient = new TestAdminClient();
-    const shopClient = new TestShopClient();
-    const server = new TestServer();
+    const { server, adminClient, shopClient } = createTestEnvironment({
+        ...testConfig,
+        shippingOptions: {
+            shippingEligibilityCheckers: [defaultShippingEligibilityChecker],
+            shippingCalculators: [defaultShippingCalculator, calculatorWithMetadata],
+        },
+    });
 
 
     beforeAll(async () => {
     beforeAll(async () => {
-        const token = await server.init(
-            {
-                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
-                customerCount: 1,
-            },
-            {
-                shippingOptions: {
-                    shippingEligibilityCheckers: [defaultShippingEligibilityChecker],
-                    shippingCalculators: [defaultShippingCalculator, calculatorWithMetadata],
-                },
-            },
-        );
-        await adminClient.init();
-        await shopClient.init();
+        await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
+            customerCount: 1,
+        });
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
     }, TEST_SETUP_TIMEOUT_MS);
 
 
     afterAll(async () => {
     afterAll(async () => {
@@ -279,24 +295,6 @@ describe('ShippingMethod resolver', () => {
     });
     });
 });
 });
 
 
-const TEST_METADATA = {
-    foo: 'bar',
-    baz: [1, 2, 3],
-};
-
-const calculatorWithMetadata = new ShippingCalculator({
-    code: 'calculator-with-metadata',
-    description: [{ languageCode: LanguageCode.en, value: 'Has metadata' }],
-    args: {},
-    calculate: order => {
-        return {
-            price: 100,
-            priceWithTax: 100,
-            metadata: TEST_METADATA,
-        };
-    },
-});
-
 const SHIPPING_METHOD_FRAGMENT = gql`
 const SHIPPING_METHOD_FRAGMENT = gql`
     fragment ShippingMethod on ShippingMethod {
     fragment ShippingMethod on ShippingMethod {
         id
         id

+ 95 - 103
packages/core/e2e/shop-auth.e2e-spec.ts

@@ -2,19 +2,23 @@
 import { OnModuleInit } from '@nestjs/common';
 import { OnModuleInit } from '@nestjs/common';
 import { RegisterCustomerInput } from '@vendure/common/lib/generated-shop-types';
 import { RegisterCustomerInput } from '@vendure/common/lib/generated-shop-types';
 import { pick } from '@vendure/common/lib/pick';
 import { pick } from '@vendure/common/lib/pick';
+import {
+    AccountRegistrationEvent,
+    EventBus,
+    EventBusModule,
+    IdentifierChangeEvent,
+    IdentifierChangeRequestEvent,
+    mergeConfig,
+    PasswordResetEvent,
+    VendurePlugin,
+} from '@vendure/core';
+import { createTestEnvironment } from '@vendure/testing';
 import { DocumentNode } from 'graphql';
 import { DocumentNode } from 'graphql';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
-import { EventBus } from '../src/event-bus/event-bus';
-import { EventBusModule } from '../src/event-bus/event-bus.module';
-import { AccountRegistrationEvent } from '../src/event-bus/events/account-registration-event';
-import { IdentifierChangeEvent } from '../src/event-bus/events/identifier-change-event';
-import { IdentifierChangeRequestEvent } from '../src/event-bus/events/identifier-change-request-event';
-import { PasswordResetEvent } from '../src/event-bus/events/password-reset-event';
-import { VendurePlugin } from '../src/plugin/vendure-plugin';
-
-import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
+import { initialData } from './fixtures/e2e-initial-data';
 import {
 import {
     CreateAdministrator,
     CreateAdministrator,
     CreateRole,
     CreateRole,
@@ -42,29 +46,50 @@ import {
     UPDATE_EMAIL_ADDRESS,
     UPDATE_EMAIL_ADDRESS,
     VERIFY_EMAIL,
     VERIFY_EMAIL,
 } from './graphql/shop-definitions';
 } from './graphql/shop-definitions';
-import { TestAdminClient, TestShopClient } from './test-client';
-import { TestServer } from './test-server';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 
 
 let sendEmailFn: jest.Mock;
 let sendEmailFn: jest.Mock;
 
 
+/**
+ * This mock plugin simulates an EmailPlugin which would send emails
+ * on the registration & password reset events.
+ */
+@VendurePlugin({
+    imports: [EventBusModule],
+})
+class TestEmailPlugin implements OnModuleInit {
+    constructor(private eventBus: EventBus) {}
+    onModuleInit() {
+        this.eventBus.ofType(AccountRegistrationEvent).subscribe(event => {
+            sendEmailFn(event);
+        });
+        this.eventBus.ofType(PasswordResetEvent).subscribe(event => {
+            sendEmailFn(event);
+        });
+        this.eventBus.ofType(IdentifierChangeRequestEvent).subscribe(event => {
+            sendEmailFn(event);
+        });
+        this.eventBus.ofType(IdentifierChangeEvent).subscribe(event => {
+            sendEmailFn(event);
+        });
+    }
+}
+
 describe('Shop auth & accounts', () => {
 describe('Shop auth & accounts', () => {
-    const shopClient = new TestShopClient();
-    const adminClient = new TestAdminClient();
-    const server = new TestServer();
+    const { server, adminClient, shopClient } = createTestEnvironment(
+        mergeConfig(testConfig, {
+            plugins: [TestEmailPlugin as any],
+        }),
+    );
 
 
     beforeAll(async () => {
     beforeAll(async () => {
-        const token = await server.init(
-            {
-                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
-                customerCount: 2,
-            },
-            {
-                plugins: [TestEmailPlugin],
-            },
-        );
-        await shopClient.init();
-        await adminClient.init();
+        await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
+            customerCount: 2,
+        });
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
     }, TEST_SETUP_TIMEOUT_MS);
 
 
     afterAll(async () => {
     afterAll(async () => {
@@ -508,25 +533,23 @@ describe('Shop auth & accounts', () => {
 });
 });
 
 
 describe('Expiring tokens', () => {
 describe('Expiring tokens', () => {
-    const shopClient = new TestShopClient();
-    const adminClient = new TestAdminClient();
-    const server = new TestServer();
+    const { server, adminClient, shopClient } = createTestEnvironment(
+        mergeConfig(testConfig, {
+            plugins: [TestEmailPlugin as any],
+            authOptions: {
+                verificationTokenDuration: '1ms',
+            },
+        }),
+    );
 
 
     beforeAll(async () => {
     beforeAll(async () => {
-        const token = await server.init(
-            {
-                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
-                customerCount: 1,
-            },
-            {
-                plugins: [TestEmailPlugin],
-                authOptions: {
-                    verificationTokenDuration: '1ms',
-                },
-            },
-        );
-        await shopClient.init();
-        await adminClient.init();
+        await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
+            customerCount: 1,
+        });
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
     }, TEST_SETUP_TIMEOUT_MS);
 
 
     beforeEach(() => {
     beforeEach(() => {
@@ -598,24 +621,23 @@ describe('Expiring tokens', () => {
 });
 });
 
 
 describe('Registration without email verification', () => {
 describe('Registration without email verification', () => {
-    const shopClient = new TestShopClient();
-    const server = new TestServer();
+    const { server, shopClient } = createTestEnvironment(
+        mergeConfig(testConfig, {
+            plugins: [TestEmailPlugin as any],
+            authOptions: {
+                requireVerification: false,
+            },
+        }),
+    );
     const userEmailAddress = 'glen.beardsley@test.com';
     const userEmailAddress = 'glen.beardsley@test.com';
 
 
     beforeAll(async () => {
     beforeAll(async () => {
-        const token = await server.init(
-            {
-                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
-                customerCount: 1,
-            },
-            {
-                plugins: [TestEmailPlugin],
-                authOptions: {
-                    requireVerification: false,
-                },
-            },
-        );
-        await shopClient.init();
+        await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
+            customerCount: 1,
+        });
     }, TEST_SETUP_TIMEOUT_MS);
     }, TEST_SETUP_TIMEOUT_MS);
 
 
     beforeEach(() => {
     beforeEach(() => {
@@ -672,35 +694,30 @@ describe('Registration without email verification', () => {
 });
 });
 
 
 describe('Updating email address without email verification', () => {
 describe('Updating email address without email verification', () => {
-    const shopClient = new TestShopClient();
-    const adminClient = new TestAdminClient();
-    const server = new TestServer();
+    const { server, adminClient, shopClient } = createTestEnvironment(
+        mergeConfig(testConfig, {
+            plugins: [TestEmailPlugin as any],
+            authOptions: {
+                requireVerification: false,
+            },
+        }),
+    );
     let customer: GetCustomer.Customer;
     let customer: GetCustomer.Customer;
     const NEW_EMAIL_ADDRESS = 'new@address.com';
     const NEW_EMAIL_ADDRESS = 'new@address.com';
 
 
     beforeAll(async () => {
     beforeAll(async () => {
-        const token = await server.init(
-            {
-                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
-                customerCount: 1,
-            },
-            {
-                plugins: [TestEmailPlugin],
-                authOptions: {
-                    requireVerification: false,
-                },
-            },
-        );
-        await shopClient.init();
-        await adminClient.init();
-    }, TEST_SETUP_TIMEOUT_MS);
-
-    beforeAll(async () => {
+        await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
+            customerCount: 1,
+        });
+        await adminClient.asSuperAdmin();
         const result = await adminClient.query<GetCustomer.Query, GetCustomer.Variables>(GET_CUSTOMER, {
         const result = await adminClient.query<GetCustomer.Query, GetCustomer.Variables>(GET_CUSTOMER, {
             id: 'T_1',
             id: 'T_1',
         });
         });
         customer = result.customer!;
         customer = result.customer!;
-    });
+    }, TEST_SETUP_TIMEOUT_MS);
 
 
     beforeEach(() => {
     beforeEach(() => {
         sendEmailFn = jest.fn();
         sendEmailFn = jest.fn();
@@ -729,31 +746,6 @@ describe('Updating email address without email verification', () => {
     });
     });
 });
 });
 
 
-/**
- * This mock plugin simulates an EmailPlugin which would send emails
- * on the registration & password reset events.
- */
-@VendurePlugin({
-    imports: [EventBusModule],
-})
-class TestEmailPlugin implements OnModuleInit {
-    constructor(private eventBus: EventBus) {}
-    onModuleInit() {
-        this.eventBus.ofType(AccountRegistrationEvent).subscribe(event => {
-            sendEmailFn(event);
-        });
-        this.eventBus.ofType(PasswordResetEvent).subscribe(event => {
-            sendEmailFn(event);
-        });
-        this.eventBus.ofType(IdentifierChangeRequestEvent).subscribe(event => {
-            sendEmailFn(event);
-        });
-        this.eventBus.ofType(IdentifierChangeEvent).subscribe(event => {
-            sendEmailFn(event);
-        });
-    }
-}
-
 function getVerificationTokenPromise(): Promise<string> {
 function getVerificationTokenPromise(): Promise<string> {
     return new Promise<any>(resolve => {
     return new Promise<any>(resolve => {
         sendEmailFn.mockImplementation((event: AccountRegistrationEvent) => {
         sendEmailFn.mockImplementation((event: AccountRegistrationEvent) => {

+ 9 - 11
packages/core/e2e/shop-catalog.e2e-spec.ts

@@ -1,10 +1,11 @@
 /* tslint:disable:no-non-null-assertion */
 /* tslint:disable:no-non-null-assertion */
+import { facetValueCollectionFilter } from '@vendure/core/dist/config/collection/default-collection-filters';
+import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
-import { facetValueCollectionFilter } from '../src/config/collection/default-collection-filters';
-
-import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
+import { initialData } from './fixtures/e2e-initial-data';
 import {
 import {
     CreateCollection,
     CreateCollection,
     CreateFacet,
     CreateFacet,
@@ -36,22 +37,19 @@ import {
     UPDATE_PRODUCT,
     UPDATE_PRODUCT,
     UPDATE_PRODUCT_VARIANTS,
     UPDATE_PRODUCT_VARIANTS,
 } from './graphql/shared-definitions';
 } from './graphql/shared-definitions';
-import { TestAdminClient, TestShopClient } from './test-client';
-import { TestServer } from './test-server';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 
 
 describe('Shop catalog', () => {
 describe('Shop catalog', () => {
-    const shopClient = new TestShopClient();
-    const adminClient = new TestAdminClient();
-    const server = new TestServer();
+    const { server, adminClient, shopClient } = createTestEnvironment(testConfig);
 
 
     beforeAll(async () => {
     beforeAll(async () => {
-        const token = await server.init({
+        await server.init({
+            dataDir,
+            initialData,
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
             customerCount: 1,
             customerCount: 1,
         });
         });
-        await shopClient.init();
-        await adminClient.init();
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
     }, TEST_SETUP_TIMEOUT_MS);
 
 
     afterAll(async () => {
     afterAll(async () => {

+ 8 - 9
packages/core/e2e/shop-customer.e2e-spec.ts

@@ -1,8 +1,10 @@
 /* tslint:disable:no-non-null-assertion */
 /* tslint:disable:no-non-null-assertion */
+import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
-import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
+import { initialData } from './fixtures/e2e-initial-data';
 import { AttemptLogin, GetCustomer, GetCustomerIds } from './graphql/generated-e2e-admin-types';
 import { AttemptLogin, GetCustomer, GetCustomerIds } from './graphql/generated-e2e-admin-types';
 import {
 import {
     CreateAddressInput,
     CreateAddressInput,
@@ -22,23 +24,20 @@ import {
     UPDATE_CUSTOMER,
     UPDATE_CUSTOMER,
     UPDATE_PASSWORD,
     UPDATE_PASSWORD,
 } from './graphql/shop-definitions';
 } from './graphql/shop-definitions';
-import { TestAdminClient, TestShopClient } from './test-client';
-import { TestServer } from './test-server';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 
 
 describe('Shop customers', () => {
 describe('Shop customers', () => {
-    const shopClient = new TestShopClient();
-    const adminClient = new TestAdminClient();
-    const server = new TestServer();
+    const { server, adminClient, shopClient } = createTestEnvironment(testConfig);
     let customer: GetCustomer.Customer;
     let customer: GetCustomer.Customer;
 
 
     beforeAll(async () => {
     beforeAll(async () => {
-        const token = await server.init({
+        await server.init({
+            dataDir,
+            initialData,
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
             customerCount: 2,
             customerCount: 2,
         });
         });
-        await shopClient.init();
-        await adminClient.init();
+        await adminClient.asSuperAdmin();
 
 
         // Fetch the first Customer and store it as the `customer` variable.
         // Fetch the first Customer and store it as the `customer` variable.
         const { customers } = await adminClient.query<GetCustomerIds.Query>(gql`
         const { customers } = await adminClient.query<GetCustomerIds.Query>(gql`

+ 30 - 63
packages/core/e2e/shop-order.e2e-spec.ts

@@ -1,15 +1,20 @@
 /* tslint:disable:no-non-null-assertion */
 /* tslint:disable:no-non-null-assertion */
+import { mergeConfig } from '@vendure/core';
+import { createTestEnvironment } from '@vendure/testing';
 import path from 'path';
 import path from 'path';
 
 
-import { PaymentMethodHandler } from '../src/config/payment-method/payment-method-handler';
-
-import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
+import { initialData } from './fixtures/e2e-initial-data';
+import {
+    testErrorPaymentMethod,
+    testFailingPaymentMethod,
+    testSuccessfulPaymentMethod,
+} from './fixtures/test-payment-methods';
 import {
 import {
     CreateAddressInput,
     CreateAddressInput,
     GetCountryList,
     GetCountryList,
     GetCustomer,
     GetCustomer,
     GetCustomerList,
     GetCustomerList,
-    LanguageCode,
     UpdateCountry,
     UpdateCountry,
 } from './graphql/generated-e2e-admin-types';
 } from './graphql/generated-e2e-admin-types';
 import {
 import {
@@ -52,37 +57,32 @@ import {
     SET_SHIPPING_METHOD,
     SET_SHIPPING_METHOD,
     TRANSITION_TO_STATE,
     TRANSITION_TO_STATE,
 } from './graphql/shop-definitions';
 } from './graphql/shop-definitions';
-import { TestAdminClient, TestShopClient } from './test-client';
-import { TestServer } from './test-server';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
-import { testSuccessfulPaymentMethod } from './utils/test-order-utils';
 
 
 describe('Shop orders', () => {
 describe('Shop orders', () => {
-    const adminClient = new TestAdminClient();
-    const shopClient = new TestShopClient();
-    const server = new TestServer();
-
-    beforeAll(async () => {
-        const token = await server.init(
-            {
-                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
-                customerCount: 2,
+    const { server, adminClient, shopClient } = createTestEnvironment(
+        mergeConfig(testConfig, {
+            paymentOptions: {
+                paymentMethodHandlers: [
+                    testSuccessfulPaymentMethod,
+                    testFailingPaymentMethod,
+                    testErrorPaymentMethod,
+                ],
             },
             },
-            {
-                paymentOptions: {
-                    paymentMethodHandlers: [
-                        testSuccessfulPaymentMethod,
-                        testFailingPaymentMethod,
-                        testErrorPaymentMethod,
-                    ],
-                },
-                orderOptions: {
-                    orderItemsLimit: 99,
-                },
+            orderOptions: {
+                orderItemsLimit: 99,
             },
             },
-        );
-        await shopClient.init();
-        await adminClient.init();
+        }),
+    );
+
+    beforeAll(async () => {
+        await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
+            customerCount: 2,
+        });
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
     }, TEST_SETUP_TIMEOUT_MS);
 
 
     afterAll(async () => {
     afterAll(async () => {
@@ -867,36 +867,3 @@ describe('Shop orders', () => {
         });
         });
     });
     });
 });
 });
-
-const testFailingPaymentMethod = new PaymentMethodHandler({
-    code: 'test-failing-payment-method',
-    description: [{ languageCode: LanguageCode.en, value: 'Test Failing Payment Method' }],
-    args: {},
-    createPayment: (order, args, metadata) => {
-        return {
-            amount: order.total,
-            state: 'Declined',
-            metadata,
-        };
-    },
-    settlePayment: order => ({
-        success: true,
-    }),
-});
-
-const testErrorPaymentMethod = new PaymentMethodHandler({
-    code: 'test-error-payment-method',
-    description: [{ languageCode: LanguageCode.en, value: 'Test Error Payment Method' }],
-    args: {},
-    createPayment: (order, args, metadata) => {
-        return {
-            amount: order.total,
-            state: 'Error',
-            errorMessage: 'Something went horribly wrong',
-            metadata,
-        };
-    },
-    settlePayment: order => ({
-        success: true,
-    }),
-});

+ 20 - 41
packages/core/e2e/stock-control.e2e-spec.ts

@@ -1,16 +1,16 @@
 /* tslint:disable:no-non-null-assertion */
 /* tslint:disable:no-non-null-assertion */
+import { mergeConfig, OrderState } from '@vendure/core';
+import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
-import { PaymentMethodHandler } from '../src/config/payment-method/payment-method-handler';
-import { OrderState } from '../src/service/helpers/order-state-machine/order-state';
-
-import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
+import { initialData } from './fixtures/e2e-initial-data';
+import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
 import { VARIANT_WITH_STOCK_FRAGMENT } from './graphql/fragments';
 import { VARIANT_WITH_STOCK_FRAGMENT } from './graphql/fragments';
 import {
 import {
     CreateAddressInput,
     CreateAddressInput,
     GetStockMovement,
     GetStockMovement,
-    LanguageCode,
     StockMovementType,
     StockMovementType,
     UpdateProductVariantInput,
     UpdateProductVariantInput,
     UpdateStock,
     UpdateStock,
@@ -30,29 +30,25 @@ import {
     SET_SHIPPING_ADDRESS,
     SET_SHIPPING_ADDRESS,
     TRANSITION_TO_STATE,
     TRANSITION_TO_STATE,
 } from './graphql/shop-definitions';
 } from './graphql/shop-definitions';
-import { TestAdminClient, TestShopClient } from './test-client';
-import { TestServer } from './test-server';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 
 
 describe('Stock control', () => {
 describe('Stock control', () => {
-    const adminClient = new TestAdminClient();
-    const shopClient = new TestShopClient();
-    const server = new TestServer();
+    const { server, adminClient, shopClient } = createTestEnvironment(
+        mergeConfig(testConfig, {
+            paymentOptions: {
+                paymentMethodHandlers: [testSuccessfulPaymentMethod],
+            },
+        }),
+    );
 
 
     beforeAll(async () => {
     beforeAll(async () => {
-        const token = await server.init(
-            {
-                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-stock-control.csv'),
-                customerCount: 2,
-            },
-            {
-                paymentOptions: {
-                    paymentMethodHandlers: [testPaymentMethod],
-                },
-            },
-        );
-        await shopClient.init();
-        await adminClient.init();
+        await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-stock-control.csv'),
+            customerCount: 2,
+        });
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
     }, TEST_SETUP_TIMEOUT_MS);
 
 
     afterAll(async () => {
     afterAll(async () => {
@@ -202,7 +198,7 @@ describe('Stock control', () => {
                 AddPaymentToOrder.Variables
                 AddPaymentToOrder.Variables
             >(ADD_PAYMENT, {
             >(ADD_PAYMENT, {
                 input: {
                 input: {
-                    method: testPaymentMethod.code,
+                    method: testSuccessfulPaymentMethod.code,
                     metadata: {},
                     metadata: {},
                 } as PaymentInput,
                 } as PaymentInput,
             });
             });
@@ -236,23 +232,6 @@ describe('Stock control', () => {
     });
     });
 });
 });
 
 
-const testPaymentMethod = new PaymentMethodHandler({
-    code: 'test-payment-method',
-    description: [{ languageCode: LanguageCode.en, value: 'Test Payment Method' }],
-    args: {},
-    createPayment: (order, args, metadata) => {
-        return {
-            amount: order.total,
-            state: 'Settled',
-            transactionId: '12345',
-            metadata,
-        };
-    },
-    settlePayment: order => ({
-        success: true,
-    }),
-});
-
 const UPDATE_STOCK_ON_HAND = gql`
 const UPDATE_STOCK_ON_HAND = gql`
     mutation UpdateStock($input: [UpdateProductVariantInput!]!) {
     mutation UpdateStock($input: [UpdateProductVariantInput!]!) {
         updateProductVariants(input: $input) {
         updateProductVariants(input: $input) {

+ 0 - 34
packages/core/e2e/test-client.ts

@@ -1,34 +0,0 @@
-import { getDefaultChannelToken } from '../mock-data/get-default-channel-token';
-import { SimpleGraphQLClient } from '../mock-data/simple-graphql-client';
-
-import { testConfig } from './config/test-config';
-
-// tslint:disable:no-console
-/**
- * A GraphQL client for use in e2e tests configured to use the test admin server endpoint.
- */
-export class TestAdminClient extends SimpleGraphQLClient {
-    constructor() {
-        super(`http://localhost:${testConfig.port}/${testConfig.adminApiPath}`);
-    }
-
-    async init() {
-        const token = await getDefaultChannelToken(false);
-        this.setChannelToken(token);
-        await this.asSuperAdmin();
-    }
-}
-
-/**
- * A GraphQL client for use in e2e tests configured to use the test shop server endpoint.
- */
-export class TestShopClient extends SimpleGraphQLClient {
-    constructor() {
-        super(`http://localhost:${testConfig.port}/${testConfig.shopApiPath}`);
-    }
-
-    async init() {
-        const token = await getDefaultChannelToken(false);
-        this.setChannelToken(token);
-    }
-}

+ 3 - 2
packages/core/e2e/utils/await-running-jobs.ts

@@ -1,12 +1,13 @@
+import { SimpleGraphQLClient } from '@vendure/testing';
+
 import { GetRunningJobs, JobState } from '../graphql/generated-e2e-admin-types';
 import { GetRunningJobs, JobState } from '../graphql/generated-e2e-admin-types';
 import { GET_RUNNING_JOBS } from '../graphql/shared-definitions';
 import { GET_RUNNING_JOBS } from '../graphql/shared-definitions';
-import { TestAdminClient } from '../test-client';
 
 
 /**
 /**
  * For mutation which trigger background jobs, this can be used to "pause" the execution of
  * For mutation which trigger background jobs, this can be used to "pause" the execution of
  * the test until those jobs have completed;
  * the test until those jobs have completed;
  */
  */
-export async function awaitRunningJobs(adminClient: TestAdminClient, timeout: number = 5000) {
+export async function awaitRunningJobs(adminClient: SimpleGraphQLClient, timeout: number = 5000) {
     let runningJobs = 0;
     let runningJobs = 0;
     const startTime = +new Date();
     const startTime = +new Date();
     let timedOut = false;
     let timedOut = false;

+ 0 - 9
packages/core/e2e/utils/test-environment.ts

@@ -1,9 +0,0 @@
-export const E2E_TESTING_ENV_VARIABLE = 'isE2ETest';
-
-export function setTestEnvironment() {
-    process.env[E2E_TESTING_ENV_VARIABLE] = '1';
-}
-
-export function isTestEnvironment() {
-    return !!process.env[E2E_TESTING_ENV_VARIABLE];
-}

+ 6 - 23
packages/core/e2e/utils/test-order-utils.ts

@@ -1,7 +1,8 @@
 /* tslint:disable:no-non-null-assertion */
 /* tslint:disable:no-non-null-assertion */
-import { ID } from '../../../common/lib/shared-types';
-import { PaymentMethodHandler } from '../../src/config/payment-method/payment-method-handler';
-import { LanguageCode } from '../graphql/generated-e2e-admin-types';
+import { ID } from '@vendure/common/lib/shared-types';
+import { PaymentMethodHandler } from '@vendure/core';
+import { SimpleGraphQLClient } from '@vendure/testing';
+
 import {
 import {
     AddPaymentToOrder,
     AddPaymentToOrder,
     GetShippingMethods,
     GetShippingMethods,
@@ -16,9 +17,8 @@ import {
     SET_SHIPPING_METHOD,
     SET_SHIPPING_METHOD,
     TRANSITION_TO_STATE,
     TRANSITION_TO_STATE,
 } from '../graphql/shop-definitions';
 } from '../graphql/shop-definitions';
-import { TestShopClient } from '../test-client';
 
 
-export async function proceedToArrangingPayment(shopClient: TestShopClient): Promise<ID> {
+export async function proceedToArrangingPayment(shopClient: SimpleGraphQLClient): Promise<ID> {
     await shopClient.query<SetShippingAddress.Mutation, SetShippingAddress.Variables>(SET_SHIPPING_ADDRESS, {
     await shopClient.query<SetShippingAddress.Mutation, SetShippingAddress.Variables>(SET_SHIPPING_ADDRESS, {
         input: {
         input: {
             fullName: 'name',
             fullName: 'name',
@@ -46,7 +46,7 @@ export async function proceedToArrangingPayment(shopClient: TestShopClient): Pro
 }
 }
 
 
 export async function addPaymentToOrder(
 export async function addPaymentToOrder(
-    shopClient: TestShopClient,
+    shopClient: SimpleGraphQLClient,
     handler: PaymentMethodHandler,
     handler: PaymentMethodHandler,
 ): Promise<NonNullable<AddPaymentToOrder.Mutation['addPaymentToOrder']>> {
 ): Promise<NonNullable<AddPaymentToOrder.Mutation['addPaymentToOrder']>> {
     const result = await shopClient.query<AddPaymentToOrder.Mutation, AddPaymentToOrder.Variables>(
     const result = await shopClient.query<AddPaymentToOrder.Mutation, AddPaymentToOrder.Variables>(
@@ -63,20 +63,3 @@ export async function addPaymentToOrder(
     const order = result.addPaymentToOrder!;
     const order = result.addPaymentToOrder!;
     return order as any;
     return order as any;
 }
 }
-
-export const testSuccessfulPaymentMethod = new PaymentMethodHandler({
-    code: 'test-payment-method',
-    description: [{ languageCode: LanguageCode.en, value: 'Test Payment Method' }],
-    args: {},
-    createPayment: (order, args, metadata) => {
-        return {
-            amount: order.total,
-            state: 'Settled',
-            transactionId: '12345',
-            metadata,
-        };
-    },
-    settlePayment: order => ({
-        success: true,
-    }),
-});

+ 32 - 25
packages/core/e2e/zone.e2e-spec.ts

@@ -1,26 +1,27 @@
+import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
-import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
+import { initialData } from './fixtures/e2e-initial-data';
 import { ZONE_FRAGMENT } from './graphql/fragments';
 import { ZONE_FRAGMENT } from './graphql/fragments';
 import {
 import {
     AddMembersToZone,
     AddMembersToZone,
-    CreateZone, DeleteZone,
+    CreateZone,
+    DeleteZone,
     DeletionResult,
     DeletionResult,
     GetCountryList,
     GetCountryList,
-    GetZone, GetZones,
+    GetZone,
+    GetZones,
     RemoveMembersFromZone,
     RemoveMembersFromZone,
     UpdateZone,
     UpdateZone,
 } from './graphql/generated-e2e-admin-types';
 } from './graphql/generated-e2e-admin-types';
 import { GET_COUNTRY_LIST } from './graphql/shared-definitions';
 import { GET_COUNTRY_LIST } from './graphql/shared-definitions';
-import { TestAdminClient } from './test-client';
-import { TestServer } from './test-server';
 
 
 // tslint:disable:no-non-null-assertion
 // tslint:disable:no-non-null-assertion
 
 
 describe('Facet resolver', () => {
 describe('Facet resolver', () => {
-    const client = new TestAdminClient();
-    const server = new TestServer();
+    const { server, adminClient } = createTestEnvironment(testConfig);
     let countries: GetCountryList.Items[];
     let countries: GetCountryList.Items[];
     let zones: Array<{ id: string; name: string }>;
     let zones: Array<{ id: string; name: string }>;
     let oceania: { id: string; name: string };
     let oceania: { id: string; name: string };
@@ -28,12 +29,14 @@ describe('Facet resolver', () => {
 
 
     beforeAll(async () => {
     beforeAll(async () => {
         await server.init({
         await server.init({
+            dataDir,
+            initialData,
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
             customerCount: 1,
             customerCount: 1,
         });
         });
-        await client.init();
+        await adminClient.asSuperAdmin();
 
 
-        const result = await client.query<GetCountryList.Query>(GET_COUNTRY_LIST, {});
+        const result = await adminClient.query<GetCountryList.Query>(GET_COUNTRY_LIST, {});
         countries = result.countries.items;
         countries = result.countries.items;
     }, TEST_SETUP_TIMEOUT_MS);
     }, TEST_SETUP_TIMEOUT_MS);
 
 
@@ -42,14 +45,14 @@ describe('Facet resolver', () => {
     });
     });
 
 
     it('zones', async () => {
     it('zones', async () => {
-        const result = await client.query<GetZones.Query>(GET_ZONE_LIST);
+        const result = await adminClient.query<GetZones.Query>(GET_ZONE_LIST);
         expect(result.zones.length).toBe(5);
         expect(result.zones.length).toBe(5);
         zones = result.zones;
         zones = result.zones;
         oceania = zones[0];
         oceania = zones[0];
     });
     });
 
 
     it('zone', async () => {
     it('zone', async () => {
-        const result = await client.query<GetZone.Query, GetZone.Variables>(GET_ZONE, {
+        const result = await adminClient.query<GetZone.Query, GetZone.Variables>(GET_ZONE, {
             id: oceania.id,
             id: oceania.id,
         });
         });
 
 
@@ -57,7 +60,7 @@ describe('Facet resolver', () => {
     });
     });
 
 
     it('updateZone', async () => {
     it('updateZone', async () => {
-        const result = await client.query<UpdateZone.Mutation, UpdateZone.Variables>(UPDATE_ZONE, {
+        const result = await adminClient.query<UpdateZone.Mutation, UpdateZone.Variables>(UPDATE_ZONE, {
             input: {
             input: {
                 id: oceania.id,
                 id: oceania.id,
                 name: 'oceania2',
                 name: 'oceania2',
@@ -68,7 +71,7 @@ describe('Facet resolver', () => {
     });
     });
 
 
     it('createZone', async () => {
     it('createZone', async () => {
-        const result = await client.query<CreateZone.Mutation, CreateZone.Variables>(CREATE_ZONE, {
+        const result = await adminClient.query<CreateZone.Mutation, CreateZone.Variables>(CREATE_ZONE, {
             input: {
             input: {
                 name: 'Pangaea',
                 name: 'Pangaea',
                 memberIds: [countries[0].id, countries[1].id],
                 memberIds: [countries[0].id, countries[1].id],
@@ -81,7 +84,7 @@ describe('Facet resolver', () => {
     });
     });
 
 
     it('addMembersToZone', async () => {
     it('addMembersToZone', async () => {
-        const result = await client.query<AddMembersToZone.Mutation, AddMembersToZone.Variables>(
+        const result = await adminClient.query<AddMembersToZone.Mutation, AddMembersToZone.Variables>(
             ADD_MEMBERS_TO_ZONE,
             ADD_MEMBERS_TO_ZONE,
             {
             {
                 zoneId: oceania.id,
                 zoneId: oceania.id,
@@ -94,13 +97,13 @@ describe('Facet resolver', () => {
     });
     });
 
 
     it('removeMembersFromZone', async () => {
     it('removeMembersFromZone', async () => {
-        const result = await client.query<RemoveMembersFromZone.Mutation, RemoveMembersFromZone.Variables>(
-            REMOVE_MEMBERS_FROM_ZONE,
-            {
-                zoneId: oceania.id,
-                memberIds: [countries[0].id, countries[2].id],
-            },
-        );
+        const result = await adminClient.query<
+            RemoveMembersFromZone.Mutation,
+            RemoveMembersFromZone.Variables
+        >(REMOVE_MEMBERS_FROM_ZONE, {
+            zoneId: oceania.id,
+            memberIds: [countries[0].id, countries[2].id],
+        });
 
 
         expect(!!result.removeMembersFromZone.members.find(m => m.name === countries[0].name)).toBe(false);
         expect(!!result.removeMembersFromZone.members.find(m => m.name === countries[0].name)).toBe(false);
         expect(!!result.removeMembersFromZone.members.find(m => m.name === countries[2].name)).toBe(false);
         expect(!!result.removeMembersFromZone.members.find(m => m.name === countries[2].name)).toBe(false);
@@ -109,19 +112,23 @@ describe('Facet resolver', () => {
 
 
     describe('deletion', () => {
     describe('deletion', () => {
         it('deletes Zone not used in any TaxRate', async () => {
         it('deletes Zone not used in any TaxRate', async () => {
-            const result1 = await client.query<DeleteZone.Mutation, DeleteZone.Variables>(DELETE_ZONE, { id: pangaea.id });
+            const result1 = await adminClient.query<DeleteZone.Mutation, DeleteZone.Variables>(DELETE_ZONE, {
+                id: pangaea.id,
+            });
 
 
             expect(result1.deleteZone).toEqual({
             expect(result1.deleteZone).toEqual({
                 result: DeletionResult.DELETED,
                 result: DeletionResult.DELETED,
                 message: '',
                 message: '',
             });
             });
 
 
-            const result2 = await client.query<GetZones.Query>(GET_ZONE_LIST);
+            const result2 = await adminClient.query<GetZones.Query>(GET_ZONE_LIST);
             expect(result2.zones.find(c => c.id === pangaea.id)).toBeUndefined();
             expect(result2.zones.find(c => c.id === pangaea.id)).toBeUndefined();
         });
         });
 
 
         it('does not delete Zone that is used in one or more TaxRates', async () => {
         it('does not delete Zone that is used in one or more TaxRates', async () => {
-            const result1 = await client.query<DeleteZone.Mutation, DeleteZone.Variables>(DELETE_ZONE, { id: oceania.id });
+            const result1 = await adminClient.query<DeleteZone.Mutation, DeleteZone.Variables>(DELETE_ZONE, {
+                id: oceania.id,
+            });
 
 
             expect(result1.deleteZone).toEqual({
             expect(result1.deleteZone).toEqual({
                 result: DeletionResult.NOT_DELETED,
                 result: DeletionResult.NOT_DELETED,
@@ -130,7 +137,7 @@ describe('Facet resolver', () => {
                     'TaxRates: Standard Tax Oceania, Reduced Tax Oceania, Zero Tax Oceania',
                     'TaxRates: Standard Tax Oceania, Reduced Tax Oceania, Zero Tax Oceania',
             });
             });
 
 
-            const result2 = await client.query<GetZones.Query>(GET_ZONE_LIST);
+            const result2 = await adminClient.query<GetZones.Query>(GET_ZONE_LIST);
             expect(result2.zones.find(c => c.id === oceania.id)).not.toBeUndefined();
             expect(result2.zones.find(c => c.id === oceania.id)).not.toBeUndefined();
         });
         });
     });
     });

+ 0 - 14
packages/core/mock-data/populate-customers.ts

@@ -1,14 +0,0 @@
-import { VendureConfig } from '../src/config/vendure-config';
-
-import { MockDataService } from './mock-data.service';
-import { SimpleGraphQLClient } from './simple-graphql-client';
-
-/**
- * Creates customers with addresses by making API calls to the Admin API.
- */
-export async function populateCustomers(count: number, config: VendureConfig, logging: boolean = false) {
-    const client = new SimpleGraphQLClient(`http://localhost:${config.port}/${config.adminApiPath}`);
-    await client.asSuperAdmin();
-    const mockDataService = new MockDataService(client, logging);
-    await mockDataService.populateCustomers(count);
-}

+ 5 - 2
packages/core/package.json

@@ -18,7 +18,11 @@
   "scripts": {
   "scripts": {
     "tsc:watch": "tsc -p ./build/tsconfig.build.json --watch",
     "tsc:watch": "tsc -p ./build/tsconfig.build.json --watch",
     "gulp:watch": "gulp -f ./build/gulpfile.ts watch",
     "gulp:watch": "gulp -f ./build/gulpfile.ts watch",
-    "build": "rimraf dist && tsc -p ./build/tsconfig.build.json && tsc -p ./build/tsconfig.cli.json && gulp -f ./build/gulpfile.ts build"
+    "build": "rimraf dist && tsc -p ./build/tsconfig.build.json && tsc -p ./build/tsconfig.cli.json && gulp -f ./build/gulpfile.ts build",
+    "watch": "concurrently yarn:tsc:watch yarn:gulp:watch",
+    "lint": "tslint --fix --project ./",
+    "test": "jest --config ./jest.config.js",
+    "e2e": "jest --config ./e2e/config/jest-e2e.json --runInBand"
   },
   },
   "publishConfig": {
   "publishConfig": {
     "access": "public"
     "access": "public"
@@ -87,7 +91,6 @@
     "@types/node-fetch": "^2.5.2",
     "@types/node-fetch": "^2.5.2",
     "@types/progress": "^2.0.3",
     "@types/progress": "^2.0.3",
     "@types/prompts": "^2.0.2",
     "@types/prompts": "^2.0.2",
-    "faker": "^4.1.0",
     "gulp": "^4.0.0",
     "gulp": "^4.0.0",
     "mysql": "^2.16.0",
     "mysql": "^2.16.0",
     "node-fetch": "^2.6.0",
     "node-fetch": "^2.6.0",

+ 0 - 5
packages/core/src/common/types/common-types.ts

@@ -30,11 +30,6 @@ export interface Orderable {
  */
  */
 export type ReadOnlyRequired<T> = { +readonly [K in keyof T]-?: T[K] };
 export type ReadOnlyRequired<T> = { +readonly [K in keyof T]-?: T[K] };
 
 
-/**
- * Creates a mutable version of a type with readonly properties.
- */
-export type Mutable<T> = { -readonly [K in keyof T]: T[K] };
-
 /**
 /**
  * Given an array type e.g. Array<string>, return the inner type e.g. string.
  * Given an array type e.g. Array<string>, return the inner type e.g. string.
  */
  */

+ 2 - 4
packages/core/src/config/config-helpers.ts

@@ -1,8 +1,6 @@
-import { DeepPartial } from '@vendure/common/lib/shared-types';
-
 import { defaultConfig } from './default-config';
 import { defaultConfig } from './default-config';
 import { mergeConfig } from './merge-config';
 import { mergeConfig } from './merge-config';
-import { RuntimeVendureConfig, VendureConfig } from './vendure-config';
+import { PartialVendureConfig, RuntimeVendureConfig } from './vendure-config';
 
 
 let activeConfig = defaultConfig;
 let activeConfig = defaultConfig;
 
 
@@ -10,7 +8,7 @@ let activeConfig = defaultConfig;
  * Override the default config by merging in the supplied values. Should only be used prior to
  * Override the default config by merging in the supplied values. Should only be used prior to
  * bootstrapping the app.
  * bootstrapping the app.
  */
  */
-export function setConfig(userConfig: DeepPartial<VendureConfig>): void {
+export function setConfig(userConfig: PartialVendureConfig): void {
     activeConfig = mergeConfig(activeConfig, userConfig);
     activeConfig = mergeConfig(activeConfig, userConfig);
 }
 }
 
 

+ 0 - 1
packages/core/src/config/default-config.ts

@@ -23,7 +23,6 @@ import { RuntimeVendureConfig } from './vendure-config';
  * The default configuration settings which are used if not explicitly overridden in the bootstrap() call.
  * The default configuration settings which are used if not explicitly overridden in the bootstrap() call.
  *
  *
  * @docsCategory configuration
  * @docsCategory configuration
- * @docsPage Configuration
  */
  */
 export const defaultConfig: RuntimeVendureConfig = {
 export const defaultConfig: RuntimeVendureConfig = {
     channelTokenKey: 'vendure-token',
     channelTokenKey: 'vendure-token',

+ 2 - 0
packages/core/src/config/index.ts

@@ -10,6 +10,7 @@ export * from './entity-id-strategy/uuid-id-strategy';
 export * from './order-merge-strategy/order-merge-strategy';
 export * from './order-merge-strategy/order-merge-strategy';
 export * from './logger/vendure-logger';
 export * from './logger/vendure-logger';
 export * from './logger/default-logger';
 export * from './logger/default-logger';
+export * from './logger/noop-logger';
 export * from './payment-method/example-payment-method-config';
 export * from './payment-method/example-payment-method-config';
 export * from './payment-method/payment-method-handler';
 export * from './payment-method/payment-method-handler';
 export * from './promotion/default-promotion-actions';
 export * from './promotion/default-promotion-actions';
@@ -21,3 +22,4 @@ export * from './shipping-method/default-shipping-eligibility-checker';
 export * from './shipping-method/shipping-calculator';
 export * from './shipping-method/shipping-calculator';
 export * from './shipping-method/shipping-eligibility-checker';
 export * from './shipping-method/shipping-eligibility-checker';
 export * from './vendure-config';
 export * from './vendure-config';
+export * from './merge-config';

+ 21 - 7
packages/core/src/config/merge-config.spec.ts

@@ -1,14 +1,28 @@
 import { mergeConfig } from './merge-config';
 import { mergeConfig } from './merge-config';
 
 
 describe('mergeConfig()', () => {
 describe('mergeConfig()', () => {
+    it('creates a new object reference', () => {
+        const nestedObject = { b: 2 };
+        const input: any = {
+            a: nestedObject,
+        };
+
+        const result = mergeConfig(input, { c: 5 } as any);
+        expect(result).toEqual({
+            a: nestedObject,
+            c: 5,
+        });
+        expect(input).not.toBe(result);
+        expect(input.a).not.toBe(result.a);
+    });
     it('merges top-level properties', () => {
     it('merges top-level properties', () => {
         const input: any = {
         const input: any = {
             a: 1,
             a: 1,
             b: 2,
             b: 2,
         };
         };
 
 
-        mergeConfig(input, { b: 3, c: 5 } as any);
-        expect(input).toEqual({
+        const result = mergeConfig(input, { b: 3, c: 5 } as any);
+        expect(result).toEqual({
             a: 1,
             a: 1,
             b: 3,
             b: 3,
             c: 5,
             c: 5,
@@ -21,8 +35,8 @@ describe('mergeConfig()', () => {
             b: { c: 2 },
             b: { c: 2 },
         };
         };
 
 
-        mergeConfig(input, { b: { c: 5 } } as any);
-        expect(input).toEqual({
+        const result = mergeConfig(input, { b: { c: 5 } } as any);
+        expect(result).toEqual({
             a: 1,
             a: 1,
             b: { c: 5 },
             b: { c: 5 },
         });
         });
@@ -40,9 +54,9 @@ describe('mergeConfig()', () => {
             class: new Foo(),
             class: new Foo(),
         };
         };
 
 
-        mergeConfig(input, { class: new Bar() } as any);
+        const result = mergeConfig(input, { class: new Bar() } as any);
 
 
-        expect(input.class instanceof Bar).toBe(true);
-        expect(input.class.name).toBe('bar');
+        expect(result.class instanceof Bar).toBe(true);
+        expect(result.class.name).toBe('bar');
     });
     });
 });
 });

+ 25 - 20
packages/core/src/config/merge-config.ts

@@ -1,37 +1,42 @@
-import { DeepPartial } from '@vendure/common/lib/shared-types';
+import { isClassInstance, isObject } from '@vendure/common/lib/shared-utils';
+import { simpleDeepClone } from '@vendure/common/lib/simple-deep-clone';
 
 
-import { VendureConfig } from './vendure-config';
+import { PartialVendureConfig, VendureConfig } from './vendure-config';
 
 
 /**
 /**
- * Simple object check.
- * From https://stackoverflow.com/a/34749873/772859
+ * @description
+ * Performs a deep merge of two VendureConfig objects. Unlike `Object.assign()` the `target` object is
+ * not mutated, instead the function returns a new object which is the result of deeply merging the
+ * values of `source` into `target`.
+ *
+ * @example
+ * ```TypeScript
+ * const result = mergeConfig(defaultConfig, {
+ *   assetOptions: {
+ *     uploadMaxFileSize: 5000,
+ *   },
+ * };
+ * ```
+ *
+ * @docsCategory configuration
  */
  */
-function isObject(item: any): item is object {
-    return item && typeof item === 'object' && !Array.isArray(item);
-}
-
-function isClassInstance(item: any): boolean {
-    return isObject(item) && item.constructor.name !== 'Object';
-}
-
-/**
- * Deep merge config objects. Based on the solution from https://stackoverflow.com/a/34749873/772859
- * but modified so that it does not overwrite fields of class instances, rather it overwrites
- * the entire instance.
- */
-export function mergeConfig<T extends VendureConfig>(target: T, source: DeepPartial<VendureConfig>): T {
+export function mergeConfig<T extends VendureConfig>(target: T, source: PartialVendureConfig, depth = 0): T {
     if (!source) {
     if (!source) {
         return target;
         return target;
     }
     }
 
 
+    if (depth === 0) {
+        target = simpleDeepClone(target);
+    }
+
     if (isObject(target) && isObject(source)) {
     if (isObject(target) && isObject(source)) {
         for (const key in source) {
         for (const key in source) {
             if (isObject((source as any)[key])) {
             if (isObject((source as any)[key])) {
                 if (!(target as any)[key]) {
                 if (!(target as any)[key]) {
-                    Object.assign((target as any), { [key]: {} });
+                    Object.assign(target as any, { [key]: {} });
                 }
                 }
                 if (!isClassInstance((source as any)[key])) {
                 if (!isClassInstance((source as any)[key])) {
-                    mergeConfig((target as any)[key], (source as any)[key]);
+                    mergeConfig((target as any)[key], (source as any)[key], depth + 1);
                 } else {
                 } else {
                     (target as any)[key] = (source as any)[key];
                     (target as any)[key] = (source as any)[key];
                 }
                 }

+ 14 - 2
packages/core/src/config/vendure-config.ts

@@ -373,7 +373,6 @@ export interface WorkerOptions {
  * [`VendureConfig`](https://github.com/vendure-ecommerce/vendure/blob/master/server/src/config/vendure-config.ts) interface.
  * [`VendureConfig`](https://github.com/vendure-ecommerce/vendure/blob/master/server/src/config/vendure-config.ts) interface.
  *
  *
  * @docsCategory configuration
  * @docsCategory configuration
- * @docsPage Configuration
  * */
  * */
 export interface VendureConfig {
 export interface VendureConfig {
     /**
     /**
@@ -531,7 +530,6 @@ export interface VendureConfig {
  * config values have been merged with the {@link defaultConfig} values.
  * config values have been merged with the {@link defaultConfig} values.
  *
  *
  * @docsCategory configuration
  * @docsCategory configuration
- * @docsPage Configuration
  */
  */
 export interface RuntimeVendureConfig extends Required<VendureConfig> {
 export interface RuntimeVendureConfig extends Required<VendureConfig> {
     assetOptions: Required<AssetOptions>;
     assetOptions: Required<AssetOptions>;
@@ -541,3 +539,17 @@ export interface RuntimeVendureConfig extends Required<VendureConfig> {
     orderOptions: Required<OrderOptions>;
     orderOptions: Required<OrderOptions>;
     workerOptions: Required<WorkerOptions>;
     workerOptions: Required<WorkerOptions>;
 }
 }
+
+type DeepPartialSimple<T> = {
+    [P in keyof T]?:
+        | null
+        | (T[P] extends Array<infer U>
+              ? Array<DeepPartialSimple<U>>
+              : T[P] extends ReadonlyArray<infer X>
+              ? ReadonlyArray<DeepPartialSimple<X>>
+              : T[P] extends Type<any>
+              ? T[P]
+              : DeepPartialSimple<T[P]>);
+};
+
+export type PartialVendureConfig = DeepPartialSimple<VendureConfig>;

+ 0 - 50
packages/core/src/data-import/import-cli.ts

@@ -1,50 +0,0 @@
-import path from 'path';
-
-import { devConfig } from '../../../dev-server/dev-config';
-import { SimpleGraphQLClient } from '../../mock-data/simple-graphql-client';
-import { bootstrap } from '../bootstrap';
-import { setConfig } from '../config/config-helpers';
-import { VendureConfig } from '../config/vendure-config';
-
-// tslint:disable:no-console
-/**
- * A CLI script which imports products from a CSV file.
- */
-if (require.main === module) {
-    // Running from command line
-    const csvFile = process.argv[2];
-    const csvPath = path.join(__dirname, csvFile);
-    getClient()
-        .then(client => importProducts(client, csvPath))
-        .then(
-            () => process.exit(0),
-            err => {
-                console.log(err);
-                process.exit(1);
-            },
-        );
-}
-
-async function getClient() {
-    const config: VendureConfig = {
-        ...devConfig as any,
-        port: 3020,
-        authOptions: {
-            tokenMethod: 'bearer',
-        },
-        plugins: [],
-    };
-    (config.dbConnectionOptions as any).logging = false;
-    setConfig(config);
-    const app = await bootstrap(config);
-    const client = new SimpleGraphQLClient(`http://localhost:${config.port}/${config.adminApiPath}`);
-    client.setChannelToken(devConfig.defaultChannelToken || 'no-default-channel-token');
-    await client.asSuperAdmin();
-    return client;
-}
-
-async function importProducts(client: SimpleGraphQLClient, csvFile: string) {
-    console.log(`loading data from "${csvFile}"`);
-    const result = await client.importProducts(csvFile);
-    console.log(result);
-}

+ 0 - 4
packages/core/typings.d.ts

@@ -1,9 +1,5 @@
 // This file is for any 3rd party JS libs which don't have a corresponding @types/ package.
 // This file is for any 3rd party JS libs which don't have a corresponding @types/ package.
 
 
-declare module 'node-libcurl' {
-    export const Curl: any;
-}
-
 declare module 'opn' {
 declare module 'opn' {
     declare const opn: (path: string) => Promise<any>;
     declare const opn: (path: string) => Promise<any>;
     export default opn;
     export default opn;

+ 3 - 1
packages/create/package.json

@@ -12,7 +12,9 @@
   ],
   ],
   "scripts": {
   "scripts": {
     "copy-assets": "rimraf assets && ts-node ./build.ts",
     "copy-assets": "rimraf assets && ts-node ./build.ts",
-    "build": "yarn copy-assets && rimraf lib && tsc -p ./tsconfig.build.json"
+    "build": "yarn copy-assets && rimraf lib && tsc -p ./tsconfig.build.json",
+    "watch": "yarn copy-assets && rimraf lib && tsc -p ./tsconfig.build.json -w",
+    "lint": "tslint --fix --project ./"
   },
   },
   "publishConfig": {
   "publishConfig": {
     "access": "public"
     "access": "public"

+ 1 - 0
packages/dev-server/.gitignore

@@ -6,4 +6,5 @@ vendure-import-error.log
 load-testing/data-sources/products*.csv
 load-testing/data-sources/products*.csv
 load-testing/results/*.json
 load-testing/results/*.json
 load-testing/results/*.csv
 load-testing/results/*.csv
+load-testing/static/assets
 dev-config-override.ts
 dev-config-override.ts

+ 0 - 1
packages/dev-server/load-testing/graphql/shop/deep-query.graphql

@@ -44,7 +44,6 @@
                     value
                     value
                 }
                 }
                 code
                 code
-                description
             }
             }
             featuredAsset {
             featuredAsset {
                 id
                 id

+ 14 - 5
packages/dev-server/load-testing/init-load-test.ts

@@ -3,15 +3,19 @@
 import { bootstrap } from '@vendure/core';
 import { bootstrap } from '@vendure/core';
 import { populate } from '@vendure/core/cli/populate';
 import { populate } from '@vendure/core/cli/populate';
 import { BaseProductRecord } from '@vendure/core/dist/data-import/providers/import-parser/import-parser';
 import { BaseProductRecord } from '@vendure/core/dist/data-import/providers/import-parser/import-parser';
+import { clearAllTables, populateCustomers } from '@vendure/testing';
 import stringify from 'csv-stringify';
 import stringify from 'csv-stringify';
 import fs from 'fs';
 import fs from 'fs';
 import path from 'path';
 import path from 'path';
 
 
-import { clearAllTables } from '../../core/mock-data/clear-all-tables';
 import { initialData } from '../../core/mock-data/data-sources/initial-data';
 import { initialData } from '../../core/mock-data/data-sources/initial-data';
-import { populateCustomers } from '../../core/mock-data/populate-customers';
 
 
-import { getLoadTestConfig, getMysqlConnectionOptions, getProductCount, getProductCsvFilePath } from './load-test-config';
+import {
+    getLoadTestConfig,
+    getMysqlConnectionOptions,
+    getProductCount,
+    getProductCsvFilePath,
+} from './load-test-config';
 
 
 // tslint:disable:no-console
 // tslint:disable:no-console
 
 
@@ -41,7 +45,7 @@ if (require.main === module) {
                     )
                     )
                     .then(async app => {
                     .then(async app => {
                         console.log('populating customers...');
                         console.log('populating customers...');
-                        await populateCustomers(10, config as any, true);
+                        await populateCustomers(10, config, true);
                         return app.close();
                         return app.close();
                     });
                     });
             } else {
             } else {
@@ -170,7 +174,12 @@ function generateMockData(productCount: number, writeFn: (row: string[]) => void
 }
 }
 
 
 function getCategoryNames() {
 function getCategoryNames() {
-    const allNames = initialData.collections.reduce((all, c) => [...all, ...c.facetNames], [] as string[]);
+    const allNames = new Set<string>();
+    for (const collection of initialData.collections) {
+        for (const filter of collection.filters || []) {
+            filter.args.facetValueNames.forEach(name => allNames.add(name));
+        }
+    }
     return Array.from(new Set(allNames));
     return Array.from(new Set(allNames));
 }
 }
 
 

+ 29 - 8
packages/dev-server/load-testing/load-test-config.ts

@@ -1,12 +1,19 @@
 /* tslint:disable:no-console */
 /* tslint:disable:no-console */
-import { VendureConfig } from '@vendure/core';
+import { AssetServerPlugin } from '@vendure/asset-server-plugin';
+import {
+    DefaultLogger,
+    DefaultSearchPlugin,
+    examplePaymentHandler,
+    LogLevel,
+    mergeConfig,
+    VendureConfig,
+} from '@vendure/core';
+import { defaultConfig } from '@vendure/core/dist/config/default-config';
 import path from 'path';
 import path from 'path';
 
 
-import { devConfig } from '../dev-config';
-
 export function getMysqlConnectionOptions(count: number) {
 export function getMysqlConnectionOptions(count: number) {
     return {
     return {
-        type: 'mysql',
+        type: 'mysql' as const,
         host: '192.168.99.100',
         host: '192.168.99.100',
         port: 3306,
         port: 3306,
         username: 'root',
         username: 'root',
@@ -15,10 +22,13 @@ export function getMysqlConnectionOptions(count: number) {
     };
     };
 }
 }
 
 
-export function getLoadTestConfig(tokenMethod: 'cookie' | 'bearer'): VendureConfig {
+export function getLoadTestConfig(tokenMethod: 'cookie' | 'bearer'): Required<VendureConfig> {
     const count = getProductCount();
     const count = getProductCount();
-    return {
-        ...devConfig as any,
+    return mergeConfig(defaultConfig, {
+        paymentOptions: {
+            paymentMethodHandlers: [examplePaymentHandler],
+        },
+        logger: new DefaultLogger({ level: LogLevel.Info }),
         dbConnectionOptions: getMysqlConnectionOptions(count),
         dbConnectionOptions: getMysqlConnectionOptions(count),
         authOptions: {
         authOptions: {
             tokenMethod,
             tokenMethod,
@@ -27,8 +37,19 @@ export function getLoadTestConfig(tokenMethod: 'cookie' | 'bearer'): VendureConf
         importExportOptions: {
         importExportOptions: {
             importAssetsDir: path.join(__dirname, './data-sources'),
             importAssetsDir: path.join(__dirname, './data-sources'),
         },
         },
+        workerOptions: {
+            runInMainProcess: true,
+        },
         customFields: {},
         customFields: {},
-    };
+        plugins: [
+            AssetServerPlugin.init({
+                assetUploadDir: path.join(__dirname, 'static/assets'),
+                route: 'assets',
+                port: 5002,
+            }),
+            DefaultSearchPlugin,
+        ],
+    });
 }
 }
 
 
 export function getProductCsvFilePath() {
 export function getProductCsvFilePath() {

+ 0 - 0
packages/dev-server/load-testing/static/.gitkeep


+ 1 - 0
packages/dev-server/package.json

@@ -24,6 +24,7 @@
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@types/csv-stringify": "^3.1.0",
     "@types/csv-stringify": "^3.1.0",
+    "@vendure/testing": "0.5.0",
     "concurrently": "^5.0.0",
     "concurrently": "^5.0.0",
     "csv-stringify": "^5.3.3"
     "csv-stringify": "^5.3.3"
   }
   }

+ 21 - 19
packages/dev-server/populate-dev-server.ts

@@ -1,12 +1,12 @@
 // tslint:disable-next-line:no-reference
 // tslint:disable-next-line:no-reference
 /// <reference path="../core/typings.d.ts" />
 /// <reference path="../core/typings.d.ts" />
-import { bootstrap, VendureConfig } from '@vendure/core';
+import { bootstrap, mergeConfig, VendureConfig } from '@vendure/core';
 import { populate } from '@vendure/core/cli/populate';
 import { populate } from '@vendure/core/cli/populate';
+import { defaultConfig } from '@vendure/core/dist/config/default-config';
+import { clearAllTables, populateCustomers } from '@vendure/testing';
 import path from 'path';
 import path from 'path';
 
 
-import { clearAllTables } from '../core/mock-data/clear-all-tables';
 import { initialData } from '../core/mock-data/data-sources/initial-data';
 import { initialData } from '../core/mock-data/data-sources/initial-data';
-import { populateCustomers } from '../core/mock-data/populate-customers';
 
 
 import { devConfig } from './dev-config';
 import { devConfig } from './dev-config';
 
 
@@ -17,21 +17,23 @@ import { devConfig } from './dev-config';
  */
  */
 if (require.main === module) {
 if (require.main === module) {
     // Running from command line
     // Running from command line
-    const populateConfig: VendureConfig = {
-        ...(devConfig as any),
-        authOptions: {
-            tokenMethod: 'bearer',
-            requireVerification: false,
-        },
-        importExportOptions: {
-            importAssetsDir: path.join(__dirname, '../core/mock-data/assets'),
-        },
-        workerOptions: {
-            runInMainProcess: true,
-        },
-        customFields: {},
-    };
-    clearAllTables(populateConfig as any, true)
+    const populateConfig = mergeConfig(
+        defaultConfig,
+        mergeConfig(devConfig, {
+            authOptions: {
+                tokenMethod: 'bearer',
+                requireVerification: false,
+            },
+            importExportOptions: {
+                importAssetsDir: path.join(__dirname, '../core/mock-data/assets'),
+            },
+            workerOptions: {
+                runInMainProcess: true,
+            },
+            customFields: {},
+        }),
+    );
+    clearAllTables(populateConfig, true)
         .then(() =>
         .then(() =>
             populate(
             populate(
                 () => bootstrap(populateConfig),
                 () => bootstrap(populateConfig),
@@ -41,7 +43,7 @@ if (require.main === module) {
         )
         )
         .then(async app => {
         .then(async app => {
             console.log('populating customers...');
             console.log('populating customers...');
-            await populateCustomers(10, populateConfig as any, true);
+            await populateCustomers(10, populateConfig, true);
             return app.close();
             return app.close();
         })
         })
         .then(
         .then(

+ 3 - 1
packages/elasticsearch-plugin/package.json

@@ -9,7 +9,9 @@
   ],
   ],
   "scripts": {
   "scripts": {
     "watch": "tsc -p ./tsconfig.build.json --watch",
     "watch": "tsc -p ./tsconfig.build.json --watch",
-    "build": "rimraf lib && tsc -p ./tsconfig.build.json"
+    "build": "rimraf lib && tsc -p ./tsconfig.build.json",
+    "lint": "tslint --fix --project ./",
+    "test": "jest --config ./jest.config.js"
   },
   },
   "publishConfig": {
   "publishConfig": {
     "access": "public"
     "access": "public"

+ 3 - 1
packages/email-plugin/package.json

@@ -11,7 +11,9 @@
   ],
   ],
   "scripts": {
   "scripts": {
     "watch": "tsc -p ./tsconfig.build.json --watch",
     "watch": "tsc -p ./tsconfig.build.json --watch",
-    "build": "rimraf lib && tsc -p ./tsconfig.build.json"
+    "build": "rimraf lib && tsc -p ./tsconfig.build.json",
+    "lint": "tslint --fix --project ./",
+    "test": "jest --config ./jest.config.js"
   },
   },
   "publishConfig": {
   "publishConfig": {
     "access": "public"
     "access": "public"

+ 1 - 0
packages/testing/.gitignore

@@ -0,0 +1 @@
+lib

+ 5 - 0
packages/testing/README.md

@@ -0,0 +1,5 @@
+# @vendure/testing
+
+This package contains utilities for writing end-to-end tests for Vendure.
+
+

+ 47 - 0
packages/testing/package.json

@@ -0,0 +1,47 @@
+{
+  "name": "@vendure/testing",
+  "version": "0.5.0",
+  "description": "End-to-end testing tools for Vendure projects",
+  "keywords": [
+    "vendure",
+    "testing",
+    "e2e"
+  ],
+  "author": "Michael Bromley <michael@michaelbromley.co.uk>",
+  "homepage": "https://github.com/vendure-ecommerce/vendure#readme",
+  "license": "MIT",
+  "directories": {
+    "lib": "lib"
+  },
+  "files": [
+    "lib"
+  ],
+  "main": "lib/index.js",
+  "types": "lib/index.d.ts",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/vendure-ecommerce/vendure.git"
+  },
+  "scripts": {
+    "build": "tsc -p ./tsconfig.build.json",
+    "watch": "tsc -p ./tsconfig.build.json -w",
+    "lint": "tslint --fix --project ./"
+  },
+  "bugs": {
+    "url": "https://github.com/vendure-ecommerce/vendure/issues"
+  },
+  "dependencies": {
+    "@vendure/common": "^0.5.0",
+    "faker": "^4.1.0",
+    "graphql": "^14.5.8",
+    "graphql-tag": "^2.10.1",
+    "node-fetch": "^2.6.0",
+    "node-libcurl": "^2.0.2",
+    "sql.js": "^1.0.0"
+  },
+  "devDependencies": {
+    "@vendure/core": "^0.5.0",
+    "rimraf": "^3.0.0",
+    "typescript": "^3.6.4"
+  }
+}

+ 63 - 0
packages/testing/src/config/test-config.ts

@@ -0,0 +1,63 @@
+import { Transport } from '@nestjs/microservices';
+import { ADMIN_API_PATH, SHOP_API_PATH } from '@vendure/common/lib/shared-constants';
+import { DefaultAssetNamingStrategy, NoopLogger, VendureConfig } from '@vendure/core';
+import { defaultConfig } from '@vendure/core/dist/config/default-config';
+import { mergeConfig } from '@vendure/core/dist/config/merge-config';
+import path from 'path';
+
+import { TestingAssetPreviewStrategy } from './testing-asset-preview-strategy';
+import { TestingAssetStorageStrategy } from './testing-asset-storage-strategy';
+import { TestingEntityIdStrategy } from './testing-entity-id-strategy';
+
+/**
+ * @description
+ * A {@link VendureConfig} object used for e2e tests. This configuration uses sqljs as the database
+ * and configures some special settings which are optimized for e2e tests:
+ *
+ * * `entityIdStrategy: new TestingEntityIdStrategy()` This ID strategy uses auto-increment IDs but encodes all IDs
+ * to be prepended with the string `'T_'`, so ID `1` becomes `'T_1'`.
+ * * `logger: new NoopLogger()` Do no output logs by default
+ * * `assetStorageStrategy: new TestingAssetStorageStrategy()` This strategy does not actually persist any binary data to disk.
+ * * `assetPreviewStrategy: new TestingAssetPreviewStrategy()` This strategy is a no-op.
+ *
+ * @docsCategory testing
+ */
+export const testConfig: Required<VendureConfig> = mergeConfig(defaultConfig, {
+    port: 3050,
+    adminApiPath: ADMIN_API_PATH,
+    shopApiPath: SHOP_API_PATH,
+    cors: true,
+    defaultChannelToken: 'e2e-default-channel',
+    authOptions: {
+        sessionSecret: 'some-secret',
+        tokenMethod: 'bearer',
+        requireVerification: true,
+    },
+    dbConnectionOptions: {
+        type: 'sqljs',
+        database: new Uint8Array([]),
+        location: '',
+        autoSave: false,
+        logging: false,
+    },
+    promotionOptions: {},
+    customFields: {},
+    entityIdStrategy: new TestingEntityIdStrategy(),
+    paymentOptions: {
+        paymentMethodHandlers: [],
+    },
+    logger: new NoopLogger(),
+    importExportOptions: {},
+    assetOptions: {
+        assetNamingStrategy: new DefaultAssetNamingStrategy(),
+        assetStorageStrategy: new TestingAssetStorageStrategy(),
+        assetPreviewStrategy: new TestingAssetPreviewStrategy(),
+    },
+    workerOptions: {
+        runInMainProcess: true,
+        transport: Transport.TCP,
+        options: {
+            port: 3051,
+        },
+    },
+});

+ 1 - 1
packages/core/e2e/config/testing-asset-preview-strategy.ts → packages/testing/src/config/testing-asset-preview-strategy.ts

@@ -1,4 +1,4 @@
-import { AssetPreviewStrategy } from '../../src/config/asset-preview-strategy/asset-preview-strategy';
+import { AssetPreviewStrategy } from '@vendure/core';
 
 
 const TEST_IMAGE_BASE_64 =
 const TEST_IMAGE_BASE_64 =
     // tslint:disable-next-line:max-line-length
     // tslint:disable-next-line:max-line-length

+ 1 - 2
packages/core/e2e/config/testing-asset-storage-strategy.ts → packages/testing/src/config/testing-asset-storage-strategy.ts

@@ -1,8 +1,7 @@
+import { AssetStorageStrategy } from '@vendure/core';
 import { Request } from 'express';
 import { Request } from 'express';
 import { Readable, Stream, Writable } from 'stream';
 import { Readable, Stream, Writable } from 'stream';
 
 
-import { AssetStorageStrategy } from '../../src/config/asset-storage-strategy/asset-storage-strategy';
-
 import { getTestImageBuffer } from './testing-asset-preview-strategy';
 import { getTestImageBuffer } from './testing-asset-preview-strategy';
 
 
 /**
 /**

+ 1 - 1
packages/core/e2e/config/testing-entity-id-strategy.ts → packages/testing/src/config/testing-entity-id-strategy.ts

@@ -1,4 +1,4 @@
-import { IntegerIdStrategy } from '../../src/config/entity-id-strategy/entity-id-strategy';
+import { IntegerIdStrategy } from '@vendure/core';
 
 
 /**
 /**
  * A testing entity id strategy which prefixes all IDs with a constant string. This is used in the
  * A testing entity id strategy which prefixes all IDs with a constant string. This is used in the

+ 75 - 0
packages/testing/src/create-test-environment.ts

@@ -0,0 +1,75 @@
+import { VendureConfig } from '@vendure/core';
+
+import { SimpleGraphQLClient } from './simple-graphql-client';
+import { TestServer } from './test-server';
+
+/**
+ * @description
+ * The return value of {@link createTestEnvironment}, containing the test server
+ * and clients for the Shop API and Admin API.
+ *
+ * @docsCategory testing
+ */
+export interface TestEnvironment {
+    /**
+     * @description
+     * A Vendure server instance against which GraphQL requests can be made.
+     */
+    server: TestServer;
+    /**
+     * @description
+     * A GraphQL client configured for the Admin API.
+     */
+    adminClient: SimpleGraphQLClient;
+    /**
+     * @description
+     * A GraphQL client configured for the Shop API.
+     */
+    shopClient: SimpleGraphQLClient;
+}
+
+/**
+ * @description
+ * Configures a {@link TestServer} and a {@link SimpleGraphQLClient} for each of the GraphQL APIs
+ * for use in end-to-end tests. Returns a {@link TestEnvironment} object.
+ *
+ * @example
+ * ```TypeScript
+ * import { createTestEnvironment, testConfig } from '\@vendure/testing';
+ *
+ * describe('some feature to test', () => {
+ *
+ *   const { server, adminClient, shopClient } = createTestEnvironment(testConfig);
+ *
+ *   beforeAll(async () => {
+ *     await server.init({
+ *         // ... server options
+ *     });
+ *     await adminClient.asSuperAdmin();
+ *   });
+ *
+ *   afterAll(async () => {
+ *       await server.destroy();
+ *   });
+ *
+ *   // ... end-to-end tests here
+ * });
+ * ```
+ * @docsCategory testing
+ */
+export function createTestEnvironment(config: Required<VendureConfig>): TestEnvironment {
+    const server = new TestServer(config);
+    const adminClient = new SimpleGraphQLClient(
+        config,
+        `http://localhost:${config.port}/${config.adminApiPath}`,
+    );
+    const shopClient = new SimpleGraphQLClient(
+        config,
+        `http://localhost:${config.port}/${config.shopApiPath}`,
+    );
+    return {
+        server,
+        adminClient,
+        shopClient,
+    };
+}

+ 4 - 8
packages/core/mock-data/clear-all-tables.ts → packages/testing/src/data-population/clear-all-tables.ts

@@ -1,10 +1,7 @@
+import { VendureConfig } from '@vendure/core';
+import { preBootstrapConfig } from '@vendure/core/dist/bootstrap';
 import { createConnection } from 'typeorm';
 import { createConnection } from 'typeorm';
 
 
-import { isTestEnvironment } from '../e2e/utils/test-environment';
-import { preBootstrapConfig } from '../src/bootstrap';
-import { defaultConfig } from '../src/config/default-config';
-import { VendureConfig } from '../src/config/vendure-config';
-
 // tslint:disable:no-console
 // tslint:disable:no-console
 // tslint:disable:no-floating-promises
 // tslint:disable:no-floating-promises
 /**
 /**
@@ -12,9 +9,8 @@ import { VendureConfig } from '../src/config/vendure-config';
  */
  */
 export async function clearAllTables(config: VendureConfig, logging = true) {
 export async function clearAllTables(config: VendureConfig, logging = true) {
     config = await preBootstrapConfig(config);
     config = await preBootstrapConfig(config);
-    const entityIdStrategy = config.entityIdStrategy || defaultConfig.entityIdStrategy;
-    const name = isTestEnvironment() ? undefined : 'clearAllTables';
-    const connection = await createConnection({ ...config.dbConnectionOptions, name });
+    const entityIdStrategy = config.entityIdStrategy;
+    const connection = await createConnection({ ...config.dbConnectionOptions });
     if (logging) {
     if (logging) {
         console.log('Clearing all tables...');
         console.log('Clearing all tables...');
     }
     }

+ 3 - 5
packages/core/mock-data/mock-data.service.ts → packages/testing/src/data-population/mock-data.service.ts

@@ -1,9 +1,7 @@
 import faker from 'faker/locale/en_GB';
 import faker from 'faker/locale/en_GB';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 
 
-import { CreateAddressInput, CreateCustomerInput } from '../e2e/graphql/generated-e2e-admin-types';
-
-import { SimpleGraphQLClient } from './simple-graphql-client';
+import { SimpleGraphQLClient } from '../simple-graphql-client';
 
 
 // tslint:disable:no-console
 // tslint:disable:no-console
 /**
 /**
@@ -37,7 +35,7 @@ export class MockDataService {
                     lastName,
                     lastName,
                     emailAddress: faker.internet.email(firstName, lastName),
                     emailAddress: faker.internet.email(firstName, lastName),
                     phoneNumber: faker.phone.phoneNumber(),
                     phoneNumber: faker.phone.phoneNumber(),
-                } as CreateCustomerInput,
+                },
                 password: 'test',
                 password: 'test',
             };
             };
 
 
@@ -63,7 +61,7 @@ export class MockDataService {
                         province: faker.address.county(),
                         province: faker.address.county(),
                         postalCode: faker.address.zipCode(),
                         postalCode: faker.address.zipCode(),
                         countryCode: 'GB',
                         countryCode: 'GB',
-                    } as CreateAddressInput,
+                    },
                     customerId: customer.id,
                     customerId: customer.id,
                 };
                 };
 
 

+ 23 - 0
packages/testing/src/data-population/populate-customers.ts

@@ -0,0 +1,23 @@
+import { VendureConfig } from '@vendure/core';
+
+import { SimpleGraphQLClient } from '../simple-graphql-client';
+
+import { MockDataService } from './mock-data.service';
+
+/**
+ * Creates customers with addresses by making API calls to the Admin API.
+ */
+export async function populateCustomers(
+    count: number,
+    config: Required<VendureConfig>,
+    logging: boolean = false,
+    simpleGraphQLClient = new SimpleGraphQLClient(
+        config,
+        `http://localhost:${config.port}/${config.adminApiPath}`,
+    ),
+) {
+    const client = simpleGraphQLClient;
+    await client.asSuperAdmin();
+    const mockDataService = new MockDataService(client, logging);
+    await mockDataService.populateCustomers(count);
+}

+ 12 - 22
packages/core/mock-data/populate-for-testing.ts → packages/testing/src/data-population/populate-for-testing.ts

@@ -1,51 +1,42 @@
 /* tslint:disable:no-console */
 /* tslint:disable:no-console */
 import { INestApplication, INestMicroservice } from '@nestjs/common';
 import { INestApplication, INestMicroservice } from '@nestjs/common';
 import { LanguageCode } from '@vendure/common/lib/generated-types';
 import { LanguageCode } from '@vendure/common/lib/generated-types';
+import { InitialData, VendureConfig } from '@vendure/core';
 import fs from 'fs-extra';
 import fs from 'fs-extra';
 
 
-import { setConfig } from '../src/config/config-helpers';
-import { VendureConfig } from '../src/config/vendure-config';
+import { TestServerOptions } from '../types';
 
 
 import { clearAllTables } from './clear-all-tables';
 import { clearAllTables } from './clear-all-tables';
 import { populateCustomers } from './populate-customers';
 import { populateCustomers } from './populate-customers';
 
 
-export interface PopulateOptions {
-    logging?: boolean;
-    customerCount: number;
-    productsCsvPath: string;
-    initialDataPath: string;
-}
-
 // tslint:disable:no-floating-promises
 // tslint:disable:no-floating-promises
 /**
 /**
  * Clears all tables from the database and populates with (deterministic) random data.
  * Clears all tables from the database and populates with (deterministic) random data.
  */
  */
 export async function populateForTesting(
 export async function populateForTesting(
-    config: VendureConfig,
+    config: Required<VendureConfig>,
     bootstrapFn: (config: VendureConfig) => Promise<[INestApplication, INestMicroservice | undefined]>,
     bootstrapFn: (config: VendureConfig) => Promise<[INestApplication, INestMicroservice | undefined]>,
-    options: PopulateOptions,
+    options: TestServerOptions,
 ): Promise<[INestApplication, INestMicroservice | undefined]> {
 ): Promise<[INestApplication, INestMicroservice | undefined]> {
     (config.dbConnectionOptions as any).logging = false;
     (config.dbConnectionOptions as any).logging = false;
     const logging = options.logging === undefined ? true : options.logging;
     const logging = options.logging === undefined ? true : options.logging;
     const originalRequireVerification = config.authOptions.requireVerification;
     const originalRequireVerification = config.authOptions.requireVerification;
     config.authOptions.requireVerification = false;
     config.authOptions.requireVerification = false;
 
 
-    setConfig(config);
     await clearAllTables(config, logging);
     await clearAllTables(config, logging);
     const [app, worker] = await bootstrapFn(config);
     const [app, worker] = await bootstrapFn(config);
 
 
-    await populateInitialData(app, options.initialDataPath, logging);
+    await populateInitialData(app, options.initialData, logging);
     await populateProducts(app, options.productsCsvPath, logging);
     await populateProducts(app, options.productsCsvPath, logging);
-    await populateCollections(app, options.initialDataPath, logging);
-    await populateCustomers(options.customerCount, config, logging);
+    await populateCollections(app, options.initialData, logging);
+    await populateCustomers(options.customerCount || 10, config, logging);
 
 
     config.authOptions.requireVerification = originalRequireVerification;
     config.authOptions.requireVerification = originalRequireVerification;
     return [app, worker];
     return [app, worker];
 }
 }
 
 
-async function populateInitialData(app: INestApplication, initialDataPath: string, logging: boolean) {
-    const { Populator } = await import('../src/data-import/providers/populator/populator');
-    const { initialData } = await import(initialDataPath);
+async function populateInitialData(app: INestApplication, initialData: InitialData, logging: boolean) {
+    const { Populator } = await import('@vendure/core');
     const populator = app.get(Populator);
     const populator = app.get(Populator);
     try {
     try {
         await populator.populateInitialData(initialData);
         await populator.populateInitialData(initialData);
@@ -57,9 +48,8 @@ async function populateInitialData(app: INestApplication, initialDataPath: strin
     }
     }
 }
 }
 
 
-async function populateCollections(app: INestApplication, initialDataPath: string, logging: boolean) {
-    const { Populator } = await import('../src/data-import/providers/populator/populator');
-    const { initialData } = await import(initialDataPath);
+async function populateCollections(app: INestApplication, initialData: InitialData, logging: boolean) {
+    const { Populator } = await import('@vendure/core');
     if (!initialData.collections.length) {
     if (!initialData.collections.length) {
         return;
         return;
     }
     }
@@ -75,7 +65,7 @@ async function populateCollections(app: INestApplication, initialDataPath: strin
 }
 }
 
 
 async function populateProducts(app: INestApplication, productsCsvPath: string, logging: boolean) {
 async function populateProducts(app: INestApplication, productsCsvPath: string, logging: boolean) {
-    const { Importer } = await import('../src/data-import/providers/importer/importer');
+    const { Importer } = await import('@vendure/core');
     const importer = app.get(Importer);
     const importer = app.get(Importer);
     const productData = await fs.readFile(productsCsvPath, 'utf-8');
     const productData = await fs.readFile(productsCsvPath, 'utf-8');
 
 

+ 6 - 0
packages/testing/src/index.ts

@@ -0,0 +1,6 @@
+export * from './simple-graphql-client';
+export * from './test-server';
+export * from './config/test-config';
+export * from './create-test-environment';
+export * from './data-population/clear-all-tables';
+export * from './data-population/populate-customers';

+ 37 - 26
packages/core/mock-data/simple-graphql-client.ts → packages/testing/src/simple-graphql-client.ts

@@ -1,5 +1,5 @@
-/// <reference types="../typings" />
 import { SUPER_ADMIN_USER_IDENTIFIER, SUPER_ADMIN_USER_PASSWORD } from '@vendure/common/lib/shared-constants';
 import { SUPER_ADMIN_USER_IDENTIFIER, SUPER_ADMIN_USER_PASSWORD } from '@vendure/common/lib/shared-constants';
+import { VendureConfig } from '@vendure/core';
 import { DocumentNode } from 'graphql';
 import { DocumentNode } from 'graphql';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import { print } from 'graphql/language/printer';
 import { print } from 'graphql/language/printer';
@@ -7,10 +7,7 @@ import fetch, { Response } from 'node-fetch';
 import { Curl } from 'node-libcurl';
 import { Curl } from 'node-libcurl';
 import { stringify } from 'querystring';
 import { stringify } from 'querystring';
 
 
-import { ImportInfo } from '../e2e/graphql/generated-e2e-admin-types';
-import { getConfig } from '../src/config/config-helpers';
-
-import { createUploadPostData } from './create-upload-post-data';
+import { createUploadPostData } from './utils/create-upload-post-data';
 
 
 const LOGIN = gql`
 const LOGIN = gql`
     mutation($username: String!, $password: String!) {
     mutation($username: String!, $password: String!) {
@@ -30,29 +27,42 @@ export type QueryParams = { [key: string]: string | number };
 
 
 // tslint:disable:no-console
 // tslint:disable:no-console
 /**
 /**
+ * @description
  * A minimalistic GraphQL client for populating and querying test data.
  * A minimalistic GraphQL client for populating and querying test data.
+ *
+ * @docsCategory testing
  */
  */
 export class SimpleGraphQLClient {
 export class SimpleGraphQLClient {
     private authToken: string;
     private authToken: string;
     private channelToken: string;
     private channelToken: string;
     private headers: { [key: string]: any } = {};
     private headers: { [key: string]: any } = {};
 
 
-    constructor(private apiUrl: string = '') {}
+    constructor(private vendureConfig: Required<VendureConfig>, private apiUrl: string = '') {}
 
 
+    /**
+     * @description
+     * Sets the authToken to be used in each GraphQL request.
+     */
     setAuthToken(token: string) {
     setAuthToken(token: string) {
         this.authToken = token;
         this.authToken = token;
         this.headers.Authorization = `Bearer ${this.authToken}`;
         this.headers.Authorization = `Bearer ${this.authToken}`;
     }
     }
 
 
+    /**
+     * @description
+     * Returns the authToken currently being used.
+     */
     getAuthToken(): string {
     getAuthToken(): string {
         return this.authToken;
         return this.authToken;
     }
     }
 
 
+    /** @internal */
     setChannelToken(token: string) {
     setChannelToken(token: string) {
-        this.headers[getConfig().channelTokenKey] = token;
+        this.headers[this.vendureConfig.channelTokenKey] = token;
     }
     }
 
 
     /**
     /**
+     * @description
      * Performs both query and mutation operations.
      * Performs both query and mutation operations.
      */
      */
     async query<T = any, V = Record<string, any>>(
     async query<T = any, V = Record<string, any>>(
@@ -74,27 +84,19 @@ export class SimpleGraphQLClient {
         }
         }
     }
     }
 
 
+    /**
+     * @description
+     * Performs a query or mutation and returns the resulting status code.
+     */
     async queryStatus<T = any, V = Record<string, any>>(query: DocumentNode, variables?: V): Promise<number> {
     async queryStatus<T = any, V = Record<string, any>>(query: DocumentNode, variables?: V): Promise<number> {
         const response = await this.request(query, variables);
         const response = await this.request(query, variables);
         return response.status;
         return response.status;
     }
     }
 
 
-    importProducts(csvFilePath: string): Promise<{ importProducts: ImportInfo }> {
-        return this.fileUploadMutation({
-            mutation: gql`
-                mutation ImportProducts($csvFile: Upload!) {
-                    importProducts(csvFile: $csvFile) {
-                        imported
-                        processed
-                        errors
-                    }
-                }
-            `,
-            filePaths: [csvFilePath],
-            mapVariables: () => ({ csvFile: null }),
-        });
-    }
-
+    /**
+     * @description
+     * Attemps to log in with the specified credentials.
+     */
     async asUserWithCredentials(username: string, password: string) {
     async asUserWithCredentials(username: string, password: string) {
         // first log out as the current user
         // first log out as the current user
         if (this.authToken) {
         if (this.authToken) {
@@ -110,10 +112,18 @@ export class SimpleGraphQLClient {
         return result.login;
         return result.login;
     }
     }
 
 
+    /**
+     * @description
+     * Logs in as the SuperAdmin user.
+     */
     async asSuperAdmin() {
     async asSuperAdmin() {
         await this.asUserWithCredentials(SUPER_ADMIN_USER_IDENTIFIER, SUPER_ADMIN_USER_PASSWORD);
         await this.asUserWithCredentials(SUPER_ADMIN_USER_IDENTIFIER, SUPER_ADMIN_USER_PASSWORD);
     }
     }
 
 
+    /**
+     * @description
+     * Logs out so that the client is then treated as an anonymous user.
+     */
     async asAnonymousUser() {
     async asAnonymousUser() {
         await this.query(
         await this.query(
             gql`
             gql`
@@ -142,7 +152,7 @@ export class SimpleGraphQLClient {
             headers: { 'Content-Type': 'application/json', ...this.headers },
             headers: { 'Content-Type': 'application/json', ...this.headers },
             body,
             body,
         });
         });
-        const authToken = response.headers.get(getConfig().authOptions.authTokenHeaderKey || '');
+        const authToken = response.headers.get(this.vendureConfig.authOptions.authTokenHeaderKey || '');
         if (authToken != null) {
         if (authToken != null) {
             this.setAuthToken(authToken);
             this.setAuthToken(authToken);
         }
         }
@@ -159,13 +169,14 @@ export class SimpleGraphQLClient {
     }
     }
 
 
     /**
     /**
+     * @description
      * Uses curl to post a multipart/form-data request to the server. Due to differences between the Node and browser
      * Uses curl to post a multipart/form-data request to the server. Due to differences between the Node and browser
      * environments, we cannot just use an existing library like apollo-upload-client.
      * environments, we cannot just use an existing library like apollo-upload-client.
      *
      *
      * Upload spec: https://github.com/jaydenseric/graphql-multipart-request-spec
      * Upload spec: https://github.com/jaydenseric/graphql-multipart-request-spec
      * Discussion of issue: https://github.com/jaydenseric/apollo-upload-client/issues/32
      * Discussion of issue: https://github.com/jaydenseric/apollo-upload-client/issues/32
      */
      */
-    private fileUploadMutation(options: {
+    fileUploadMutation(options: {
         mutation: DocumentNode;
         mutation: DocumentNode;
         filePaths: string[];
         filePaths: string[];
         mapVariables: (filePaths: string[]) => any;
         mapVariables: (filePaths: string[]) => any;
@@ -197,7 +208,7 @@ export class SimpleGraphQLClient {
             curl.setOpt(Curl.option.HTTPPOST, processedPostData);
             curl.setOpt(Curl.option.HTTPPOST, processedPostData);
             curl.setOpt(Curl.option.HTTPHEADER, [
             curl.setOpt(Curl.option.HTTPHEADER, [
                 `Authorization: Bearer ${this.authToken}`,
                 `Authorization: Bearer ${this.authToken}`,
-                `${getConfig().channelTokenKey}: ${this.channelToken}`,
+                `${this.vendureConfig.channelTokenKey}: ${this.channelToken}`,
             ]);
             ]);
             curl.perform();
             curl.perform();
             curl.on('end', (statusCode: any, body: any) => {
             curl.on('end', (statusCode: any, body: any) => {

+ 53 - 38
packages/core/e2e/test-server.ts → packages/testing/src/test-server.ts

@@ -1,54 +1,48 @@
 import { INestApplication, INestMicroservice } from '@nestjs/common';
 import { INestApplication, INestMicroservice } from '@nestjs/common';
 import { NestFactory } from '@nestjs/core';
 import { NestFactory } from '@nestjs/core';
-import { Omit } from '@vendure/common/lib/omit';
+import { DefaultLogger, Logger, VendureConfig } from '@vendure/core';
+import { preBootstrapConfig } from '@vendure/core/dist/bootstrap';
 import fs from 'fs';
 import fs from 'fs';
 import path from 'path';
 import path from 'path';
-import { ConnectionOptions } from 'typeorm';
 import { SqljsConnectionOptions } from 'typeorm/driver/sqljs/SqljsConnectionOptions';
 import { SqljsConnectionOptions } from 'typeorm/driver/sqljs/SqljsConnectionOptions';
 
 
-import { populateForTesting, PopulateOptions } from '../mock-data/populate-for-testing';
-import { preBootstrapConfig } from '../src/bootstrap';
-import { Mutable } from '../src/common/types/common-types';
-import { DefaultLogger } from '../src/config/logger/default-logger';
-import { Logger } from '../src/config/logger/vendure-logger';
-import { VendureConfig } from '../src/config/vendure-config';
-
-import { testConfig } from './config/test-config';
-import { setTestEnvironment } from './utils/test-environment';
+import { populateForTesting } from './data-population/populate-for-testing';
+import { Mutable, TestServerOptions } from './types';
 
 
 // tslint:disable:no-console
 // tslint:disable:no-console
 /**
 /**
- * A server against which the e2e tests should be run.
+ * @description
+ * A real Vendure server against which the e2e tests should be run.
+ *
+ * @docsCategory testing
  */
  */
 export class TestServer {
 export class TestServer {
-    app: INestApplication;
-    worker?: INestMicroservice;
+    private app: INestApplication;
+    private worker?: INestMicroservice;
+
+    constructor(private vendureConfig: Required<VendureConfig>) {}
 
 
     /**
     /**
+     * @description
      * Bootstraps an instance of Vendure server and populates the database according to the options
      * Bootstraps an instance of Vendure server and populates the database according to the options
-     * passed in. Should be called immediately after creating the client in the `beforeAll` function.
+     * passed in. Should be called in the `beforeAll` function.
      *
      *
      * The populated data is saved into an .sqlite file for each test file. On subsequent runs, this file
      * The populated data is saved into an .sqlite file for each test file. On subsequent runs, this file
      * is loaded so that the populate step can be skipped, which speeds up the tests significantly.
      * is loaded so that the populate step can be skipped, which speeds up the tests significantly.
      */
      */
-    async init(
-        options: Omit<PopulateOptions, 'initialDataPath'>,
-        customConfig: Partial<VendureConfig> = {},
-    ): Promise<void> {
-        setTestEnvironment();
-        const testingConfig = { ...testConfig, ...customConfig };
-        const dbFilePath = this.getDbFilePath();
-        (testingConfig.dbConnectionOptions as Mutable<SqljsConnectionOptions>).location = dbFilePath;
+    async init(options: TestServerOptions): Promise<void> {
+        const dbFilePath = this.getDbFilePath(options.dataDir);
+        (this.vendureConfig.dbConnectionOptions as Mutable<SqljsConnectionOptions>).location = dbFilePath;
         if (!fs.existsSync(dbFilePath)) {
         if (!fs.existsSync(dbFilePath)) {
             if (options.logging) {
             if (options.logging) {
                 console.log(`Test data not found. Populating database and caching...`);
                 console.log(`Test data not found. Populating database and caching...`);
             }
             }
-            await this.populateInitialData(testingConfig, options);
+            await this.populateInitialData(this.vendureConfig, options);
         }
         }
         if (options.logging) {
         if (options.logging) {
             console.log(`Loading test data from "${dbFilePath}"`);
             console.log(`Loading test data from "${dbFilePath}"`);
         }
         }
-        const [app, worker] = await this.bootstrapForTesting(testingConfig);
+        const [app, worker] = await this.bootstrapForTesting(this.vendureConfig);
         if (app) {
         if (app) {
             this.app = app;
             this.app = app;
         } else {
         } else {
@@ -61,7 +55,9 @@ export class TestServer {
     }
     }
 
 
     /**
     /**
-     * Destroy the Vendure instance. Should be called in the `afterAll` function.
+     * @description
+     * Destroy the Vendure server instance and clean up all resources.
+     * Should be called after all tests have run, e.g. in an `afterAll` function.
      */
      */
     async destroy() {
     async destroy() {
         // allow a grace period of any outstanding async tasks to complete
         // allow a grace period of any outstanding async tasks to complete
@@ -72,30 +68,49 @@ export class TestServer {
         }
         }
     }
     }
 
 
-    private getDbFilePath() {
-        const dbDataDir = '__data__';
+    private getDbFilePath(dataDir: string) {
         // tslint:disable-next-line:no-non-null-assertion
         // tslint:disable-next-line:no-non-null-assertion
-        const testFilePath = module!.parent!.filename;
+        const testFilePath = this.getCallerFilename(2);
         const dbFileName = path.basename(testFilePath) + '.sqlite';
         const dbFileName = path.basename(testFilePath) + '.sqlite';
-        const dbFilePath = path.join(path.dirname(testFilePath), dbDataDir, dbFileName);
+        const dbFilePath = path.join(dataDir, dbFileName);
         return dbFilePath;
         return dbFilePath;
     }
     }
 
 
+    private getCallerFilename(depth: number) {
+        let pst: ErrorConstructor['prepareStackTrace'];
+        let stack: any;
+        let file: any;
+        let frame: any;
+
+        pst = Error.prepareStackTrace;
+        Error.prepareStackTrace = (_, _stack) => {
+            Error.prepareStackTrace = pst;
+            return _stack;
+        };
+
+        stack = new Error().stack;
+        stack = stack.slice(depth + 1);
+
+        do {
+            frame = stack.shift();
+            file = frame && frame.getFileName();
+        } while (stack.length && file === 'module.js');
+
+        return file;
+    }
+
     /**
     /**
      * Populates an .sqlite database file based on the PopulateOptions.
      * Populates an .sqlite database file based on the PopulateOptions.
      */
      */
     private async populateInitialData(
     private async populateInitialData(
-        testingConfig: VendureConfig,
-        options: Omit<PopulateOptions, 'initialDataPath'>,
+        testingConfig: Required<VendureConfig>,
+        options: TestServerOptions,
     ): Promise<void> {
     ): Promise<void> {
         (testingConfig.dbConnectionOptions as Mutable<SqljsConnectionOptions>).autoSave = true;
         (testingConfig.dbConnectionOptions as Mutable<SqljsConnectionOptions>).autoSave = true;
 
 
         const [app, worker] = await populateForTesting(testingConfig, this.bootstrapForTesting, {
         const [app, worker] = await populateForTesting(testingConfig, this.bootstrapForTesting, {
             logging: false,
             logging: false,
-            ...{
-                ...options,
-                initialDataPath: path.join(__dirname, 'fixtures/e2e-initial-data.ts'),
-            },
+            ...options,
         });
         });
         await app.close();
         await app.close();
         if (worker) {
         if (worker) {
@@ -113,14 +128,14 @@ export class TestServer {
     ): Promise<[INestApplication, INestMicroservice | undefined]> {
     ): Promise<[INestApplication, INestMicroservice | undefined]> {
         const config = await preBootstrapConfig(userConfig);
         const config = await preBootstrapConfig(userConfig);
         Logger.useLogger(config.logger);
         Logger.useLogger(config.logger);
-        const appModule = await import('../src/app.module');
+        const appModule = await import('@vendure/core/dist/app.module');
         try {
         try {
             DefaultLogger.hideNestBoostrapLogs();
             DefaultLogger.hideNestBoostrapLogs();
             const app = await NestFactory.create(appModule.AppModule, { cors: config.cors, logger: false });
             const app = await NestFactory.create(appModule.AppModule, { cors: config.cors, logger: false });
             let worker: INestMicroservice | undefined;
             let worker: INestMicroservice | undefined;
             await app.listen(config.port);
             await app.listen(config.port);
             if (config.workerOptions.runInMainProcess) {
             if (config.workerOptions.runInMainProcess) {
-                const workerModule = await import('../src/worker/worker.module');
+                const workerModule = await import('@vendure/core/dist/worker/worker.module');
                 worker = await NestFactory.createMicroservice(workerModule.WorkerModule, {
                 worker = await NestFactory.createMicroservice(workerModule.WorkerModule, {
                     transport: config.workerOptions.transport,
                     transport: config.workerOptions.transport,
                     logger: new Logger(),
                     logger: new Logger(),

+ 45 - 0
packages/testing/src/types.ts

@@ -0,0 +1,45 @@
+import { InitialData } from '@vendure/core';
+
+/**
+ * Creates a mutable version of a type with readonly properties.
+ */
+export type Mutable<T> = { -readonly [K in keyof T]: T[K] };
+
+/**
+ * @description
+ * Configuration options used to initialize an instance of the {@link TestServer}.
+ *
+ * @docsCategory testing
+ */
+export interface TestServerOptions {
+    /**
+     * @description
+     * The directory in which the populated SQLite database files will be
+     * saved. These files are a cache to speed up subsequent runs of e2e tests.
+     */
+    dataDir: string;
+    /**
+     * @description
+     * The path to a CSV file containing product data to import.
+     */
+    productsCsvPath: string;
+    /**
+     * @description
+     * An object containing non-product data which is used to populate the database.
+     */
+    initialData: InitialData;
+    /**
+     * @description
+     * The number of fake Customers to populate into the database.
+     *
+     * @default 10
+     */
+    customerCount?: number;
+    /**
+     * @description
+     * Set this to `true` to log some information about the database population process.
+     *
+     * @default false
+     */
+    logging?: boolean;
+}

+ 6 - 4
packages/core/mock-data/create-upload-post-data.spec.ts → packages/testing/src/utils/create-upload-post-data.spec.ts

@@ -4,18 +4,20 @@ import { createUploadPostData } from './create-upload-post-data';
 
 
 describe('createUploadPostData()', () => {
 describe('createUploadPostData()', () => {
     it('creates correct output for createAssets mutation', () => {
     it('creates correct output for createAssets mutation', () => {
-        const result = createUploadPostData(gql`
+        const result = createUploadPostData(
+            gql`
                 mutation CreateAssets($input: [CreateAssetInput!]!) {
                 mutation CreateAssets($input: [CreateAssetInput!]!) {
                     createAssets(input: $input) {
                     createAssets(input: $input) {
                         id
                         id
                         name
                         name
                     }
                     }
-                }`
-            ,
+                }
+            `,
             ['a.jpg', 'b.jpg'],
             ['a.jpg', 'b.jpg'],
             filePaths => ({
             filePaths => ({
                 input: filePaths.map(() => ({ file: null })),
                 input: filePaths.map(() => ({ file: null })),
-            }));
+            }),
+        );
 
 
         expect(result.operations.operationName).toBe('CreateAssets');
         expect(result.operations.operationName).toBe('CreateAssets');
         expect(result.operations.variables).toEqual({
         expect(result.operations.variables).toEqual({

+ 0 - 0
packages/core/mock-data/create-upload-post-data.ts → packages/testing/src/utils/create-upload-post-data.ts


+ 1 - 2
packages/core/mock-data/get-default-channel-token.ts → packages/testing/src/utils/get-default-channel-token.ts

@@ -1,8 +1,7 @@
 import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
 import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
+import { Channel } from '@vendure/core';
 import { ConnectionOptions, getConnection } from 'typeorm';
 import { ConnectionOptions, getConnection } from 'typeorm';
 
 
-import { Channel } from '../src/entity/channel/channel.entity';
-
 // tslint:disable:no-console
 // tslint:disable:no-console
 // tslint:disable:no-floating-promises
 // tslint:disable:no-floating-promises
 /**
 /**

+ 12 - 0
packages/testing/tsconfig.build.json

@@ -0,0 +1,12 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "outDir": "./lib"
+  },
+  "include": [
+    "src/**/*.ts"
+  ],
+  "exclude": [
+    "**/*.spec.ts"
+  ]
+}

+ 10 - 0
packages/testing/tsconfig.json

@@ -0,0 +1,10 @@
+{
+  "extends": "../../tsconfig.json",
+  "compilerOptions": {
+    "declaration": true,
+    "removeComments": true,
+    "noLib": false,
+    "skipLibCheck": true,
+    "sourceMap": true
+  }
+}

+ 1 - 0
scripts/docs/generate-typescript-docs.ts

@@ -23,6 +23,7 @@ const sections: DocsSectionConfig[] = [
             'packages/asset-server-plugin/src/',
             'packages/asset-server-plugin/src/',
             'packages/email-plugin/src/',
             'packages/email-plugin/src/',
             'packages/elasticsearch-plugin/src/',
             'packages/elasticsearch-plugin/src/',
+            'packages/testing/src/',
         ],
         ],
         exclude: [
         exclude: [
             /generated-shop-types/,
             /generated-shop-types/,

+ 1 - 0
scripts/publish-to-verdaccio.sh

@@ -18,3 +18,4 @@ cd ../core && npm publish -reg $VERDACCIO &&\
 cd ../create && npm publish -reg $VERDACCIO &&\
 cd ../create && npm publish -reg $VERDACCIO &&\
 cd ../elasticsearch-plugin && npm publish -reg $VERDACCIO &&\
 cd ../elasticsearch-plugin && npm publish -reg $VERDACCIO &&\
 cd ../email-plugin && npm publish -reg $VERDACCIO
 cd ../email-plugin && npm publish -reg $VERDACCIO
+cd ../testing && npm publish -reg $VERDACCIO

+ 271 - 259
yarn.lock

@@ -931,15 +931,15 @@
     iterall "^1.1.3"
     iterall "^1.1.3"
     uuid "^3.1.0"
     uuid "^3.1.0"
 
 
-"@lerna/add@3.16.2":
-  version "3.16.2"
-  resolved "https://registry.npmjs.org/@lerna/add/-/add-3.16.2.tgz#90ecc1be7051cfcec75496ce122f656295bd6e94"
-  integrity sha512-RAAaF8aODPogj2Ge9Wj3uxPFIBGpog9M+HwSuq03ZnkkO831AmasCTJDqV+GEpl1U2DvnhZQEwHpWmTT0uUeEw==
+"@lerna/add@3.18.0":
+  version "3.18.0"
+  resolved "https://registry.npmjs.org/@lerna/add/-/add-3.18.0.tgz#86e38f14d7a0a7c61315dccb402377feb1c9db83"
+  integrity sha512-Z5EaQbBnJn1LEPb0zb0Q2o9T8F8zOnlCsj6JYpY6aSke17UUT7xx0QMN98iBK+ueUHKjN/vdFdYlNCYRSIdujA==
   dependencies:
   dependencies:
     "@evocateur/pacote" "^9.6.3"
     "@evocateur/pacote" "^9.6.3"
-    "@lerna/bootstrap" "3.16.2"
-    "@lerna/command" "3.16.0"
-    "@lerna/filter-options" "3.16.0"
+    "@lerna/bootstrap" "3.18.0"
+    "@lerna/command" "3.18.0"
+    "@lerna/filter-options" "3.18.0"
     "@lerna/npm-conf" "3.16.0"
     "@lerna/npm-conf" "3.16.0"
     "@lerna/validation-error" "3.13.0"
     "@lerna/validation-error" "3.13.0"
     dedent "^0.7.0"
     dedent "^0.7.0"
@@ -947,31 +947,22 @@
     p-map "^2.1.0"
     p-map "^2.1.0"
     semver "^6.2.0"
     semver "^6.2.0"
 
 
-"@lerna/batch-packages@3.16.0":
-  version "3.16.0"
-  resolved "https://registry.npmjs.org/@lerna/batch-packages/-/batch-packages-3.16.0.tgz#1c16cb697e7d718177db744cbcbdac4e30253c8c"
-  integrity sha512-7AdMkANpubY/FKFI01im01tlx6ygOBJ/0JcixMUWoWP/7Ds3SWQF22ID6fbBr38jUWptYLDs2fagtTDL7YUPuA==
+"@lerna/bootstrap@3.18.0":
+  version "3.18.0"
+  resolved "https://registry.npmjs.org/@lerna/bootstrap/-/bootstrap-3.18.0.tgz#705d9eb51a24d549518796a09f24d24526ed975b"
+  integrity sha512-3DZKWIaKvr7sUImoKqSz6eqn84SsOVMnA5QHwgzXiQjoeZ/5cg9x2r+Xj3+3w/lvLoh0j8U2GNtrIaPNis4bKQ==
   dependencies:
   dependencies:
-    "@lerna/package-graph" "3.16.0"
-    npmlog "^4.1.2"
-
-"@lerna/bootstrap@3.16.2":
-  version "3.16.2"
-  resolved "https://registry.npmjs.org/@lerna/bootstrap/-/bootstrap-3.16.2.tgz#be268d940221d3c3270656b9b791b492559ad9d8"
-  integrity sha512-I+gs7eh6rv9Vyd+CwqL7sftRfOOsSzCle8cv/CGlMN7/p7EAVhxEdAw8SYoHIKHzipXszuqqy1Y3opyleD0qdA==
-  dependencies:
-    "@lerna/batch-packages" "3.16.0"
-    "@lerna/command" "3.16.0"
-    "@lerna/filter-options" "3.16.0"
-    "@lerna/has-npm-version" "3.16.0"
-    "@lerna/npm-install" "3.16.0"
-    "@lerna/package-graph" "3.16.0"
+    "@lerna/command" "3.18.0"
+    "@lerna/filter-options" "3.18.0"
+    "@lerna/has-npm-version" "3.16.5"
+    "@lerna/npm-install" "3.16.5"
+    "@lerna/package-graph" "3.18.0"
     "@lerna/pulse-till-done" "3.13.0"
     "@lerna/pulse-till-done" "3.13.0"
-    "@lerna/rimraf-dir" "3.14.2"
+    "@lerna/rimraf-dir" "3.16.5"
     "@lerna/run-lifecycle" "3.16.2"
     "@lerna/run-lifecycle" "3.16.2"
-    "@lerna/run-parallel-batches" "3.16.0"
-    "@lerna/symlink-binary" "3.16.2"
-    "@lerna/symlink-dependencies" "3.16.2"
+    "@lerna/run-topologically" "3.18.0"
+    "@lerna/symlink-binary" "3.17.0"
+    "@lerna/symlink-dependencies" "3.17.0"
     "@lerna/validation-error" "3.13.0"
     "@lerna/validation-error" "3.13.0"
     dedent "^0.7.0"
     dedent "^0.7.0"
     get-port "^4.2.0"
     get-port "^4.2.0"
@@ -985,88 +976,88 @@
     read-package-tree "^5.1.6"
     read-package-tree "^5.1.6"
     semver "^6.2.0"
     semver "^6.2.0"
 
 
-"@lerna/changed@3.16.4":
-  version "3.16.4"
-  resolved "https://registry.npmjs.org/@lerna/changed/-/changed-3.16.4.tgz#c3e727d01453513140eee32c94b695de577dc955"
-  integrity sha512-NCD7XkK744T23iW0wqKEgF4R9MYmReUbyHCZKopFnsNpQdqumc3SOIvQUAkKCP6hQJmYvxvOieoVgy/CVDpZ5g==
+"@lerna/changed@3.18.3":
+  version "3.18.3"
+  resolved "https://registry.npmjs.org/@lerna/changed/-/changed-3.18.3.tgz#50529e8bd5d7fe2d0ace046a6e274d3de652a493"
+  integrity sha512-xZW7Rm+DlDIGc0EvKGyJZgT9f8FFa4d52mr/Y752dZuXR2qRmf9tXhVloRG39881s2A6yi3jqLtXZggKhsQW4Q==
   dependencies:
   dependencies:
-    "@lerna/collect-updates" "3.16.0"
-    "@lerna/command" "3.16.0"
-    "@lerna/listable" "3.16.0"
+    "@lerna/collect-updates" "3.18.0"
+    "@lerna/command" "3.18.0"
+    "@lerna/listable" "3.18.0"
     "@lerna/output" "3.13.0"
     "@lerna/output" "3.13.0"
-    "@lerna/version" "3.16.4"
+    "@lerna/version" "3.18.3"
 
 
-"@lerna/check-working-tree@3.14.2":
-  version "3.14.2"
-  resolved "https://registry.npmjs.org/@lerna/check-working-tree/-/check-working-tree-3.14.2.tgz#5ce007722180a69643a8456766ed8a91fc7e9ae1"
-  integrity sha512-7safqxM/MYoAoxZxulUDtIJIbnBIgo0PB/FHytueG+9VaX7GMnDte2Bt1EKa0dz2sAyQdmQ3Q8ZXpf/6JDjaeg==
+"@lerna/check-working-tree@3.16.5":
+  version "3.16.5"
+  resolved "https://registry.npmjs.org/@lerna/check-working-tree/-/check-working-tree-3.16.5.tgz#b4f8ae61bb4523561dfb9f8f8d874dd46bb44baa"
+  integrity sha512-xWjVBcuhvB8+UmCSb5tKVLB5OuzSpw96WEhS2uz6hkWVa/Euh1A0/HJwn2cemyK47wUrCQXtczBUiqnq9yX5VQ==
   dependencies:
   dependencies:
-    "@lerna/collect-uncommitted" "3.14.2"
-    "@lerna/describe-ref" "3.14.2"
+    "@lerna/collect-uncommitted" "3.16.5"
+    "@lerna/describe-ref" "3.16.5"
     "@lerna/validation-error" "3.13.0"
     "@lerna/validation-error" "3.13.0"
 
 
-"@lerna/child-process@3.14.2":
-  version "3.14.2"
-  resolved "https://registry.npmjs.org/@lerna/child-process/-/child-process-3.14.2.tgz#950240cba83f7dfe25247cfa6c9cebf30b7d94f6"
-  integrity sha512-xnq+W5yQb6RkwI0p16ZQnrn6HkloH/MWTw4lGE1nKsBLAUbmSU5oTE93W1nrG0X3IMF/xWc9UYvNdUGMWvZZ4w==
+"@lerna/child-process@3.16.5":
+  version "3.16.5"
+  resolved "https://registry.npmjs.org/@lerna/child-process/-/child-process-3.16.5.tgz#38fa3c18064aa4ac0754ad80114776a7b36a69b2"
+  integrity sha512-vdcI7mzei9ERRV4oO8Y1LHBZ3A5+ampRKg1wq5nutLsUA4mEBN6H7JqjWOMY9xZemv6+kATm2ofjJ3lW5TszQg==
   dependencies:
   dependencies:
     chalk "^2.3.1"
     chalk "^2.3.1"
     execa "^1.0.0"
     execa "^1.0.0"
     strong-log-transformer "^2.0.0"
     strong-log-transformer "^2.0.0"
 
 
-"@lerna/clean@3.16.0":
-  version "3.16.0"
-  resolved "https://registry.npmjs.org/@lerna/clean/-/clean-3.16.0.tgz#1c134334cacea1b1dbeacdc580e8b9240db8efa1"
-  integrity sha512-5P9U5Y19WmYZr7UAMGXBpY7xCRdlR7zhHy8MAPDKVx70rFIBS6nWXn5n7Kntv74g7Lm1gJ2rsiH5tj1OPcRJgg==
+"@lerna/clean@3.18.0":
+  version "3.18.0"
+  resolved "https://registry.npmjs.org/@lerna/clean/-/clean-3.18.0.tgz#cc67d7697db969a70e989992fdf077126308fb2e"
+  integrity sha512-BiwBELZNkarRQqj+v5NPB1aIzsOX+Y5jkZ9a5UbwHzEdBUQ5lQa0qaMLSOve/fSkaiZQxe6qnTyatN75lOcDMg==
   dependencies:
   dependencies:
-    "@lerna/command" "3.16.0"
-    "@lerna/filter-options" "3.16.0"
+    "@lerna/command" "3.18.0"
+    "@lerna/filter-options" "3.18.0"
     "@lerna/prompt" "3.13.0"
     "@lerna/prompt" "3.13.0"
     "@lerna/pulse-till-done" "3.13.0"
     "@lerna/pulse-till-done" "3.13.0"
-    "@lerna/rimraf-dir" "3.14.2"
+    "@lerna/rimraf-dir" "3.16.5"
     p-map "^2.1.0"
     p-map "^2.1.0"
     p-map-series "^1.0.0"
     p-map-series "^1.0.0"
     p-waterfall "^1.0.0"
     p-waterfall "^1.0.0"
 
 
-"@lerna/cli@3.13.0":
-  version "3.13.0"
-  resolved "https://registry.npmjs.org/@lerna/cli/-/cli-3.13.0.tgz#3d7b357fdd7818423e9681a7b7f2abd106c8a266"
-  integrity sha512-HgFGlyCZbYaYrjOr3w/EsY18PdvtsTmDfpUQe8HwDjXlPeCCUgliZjXLOVBxSjiOvPeOSwvopwIHKWQmYbwywg==
+"@lerna/cli@3.18.0":
+  version "3.18.0"
+  resolved "https://registry.npmjs.org/@lerna/cli/-/cli-3.18.0.tgz#2b6f8605bee299c6ada65bc2e4b3ed7bf715af3a"
+  integrity sha512-AwDyfGx7fxJgeaZllEuyJ9LZ6Tdv9yqRD9RX762yCJu+PCAFvB9bp6OYuRSGli7QQgM0CuOYnSg4xVNOmuGKDA==
   dependencies:
   dependencies:
     "@lerna/global-options" "3.13.0"
     "@lerna/global-options" "3.13.0"
     dedent "^0.7.0"
     dedent "^0.7.0"
     npmlog "^4.1.2"
     npmlog "^4.1.2"
-    yargs "^12.0.1"
+    yargs "^14.2.0"
 
 
-"@lerna/collect-uncommitted@3.14.2":
-  version "3.14.2"
-  resolved "https://registry.npmjs.org/@lerna/collect-uncommitted/-/collect-uncommitted-3.14.2.tgz#b5ed00d800bea26bb0d18404432b051eee8d030e"
-  integrity sha512-4EkQu4jIOdNL2BMzy/N0ydHB8+Z6syu6xiiKXOoFl0WoWU9H1jEJCX4TH7CmVxXL1+jcs8FIS2pfQz4oew99Eg==
+"@lerna/collect-uncommitted@3.16.5":
+  version "3.16.5"
+  resolved "https://registry.npmjs.org/@lerna/collect-uncommitted/-/collect-uncommitted-3.16.5.tgz#a494d61aac31cdc7aec4bbe52c96550274132e63"
+  integrity sha512-ZgqnGwpDZiWyzIQVZtQaj9tRizsL4dUOhuOStWgTAw1EMe47cvAY2kL709DzxFhjr6JpJSjXV5rZEAeU3VE0Hg==
   dependencies:
   dependencies:
-    "@lerna/child-process" "3.14.2"
+    "@lerna/child-process" "3.16.5"
     chalk "^2.3.1"
     chalk "^2.3.1"
     figgy-pudding "^3.5.1"
     figgy-pudding "^3.5.1"
     npmlog "^4.1.2"
     npmlog "^4.1.2"
 
 
-"@lerna/collect-updates@3.16.0":
-  version "3.16.0"
-  resolved "https://registry.npmjs.org/@lerna/collect-updates/-/collect-updates-3.16.0.tgz#6db3ce8a740a4e2b972c033a63bdfb77f2553d8c"
-  integrity sha512-HwAIl815X2TNlmcp28zCrSdXfoZWNP7GJPEqNWYk7xDJTYLqQ+SrmKUePjb3AMGBwYAraZSEJLbHdBpJ5+cHmQ==
+"@lerna/collect-updates@3.18.0":
+  version "3.18.0"
+  resolved "https://registry.npmjs.org/@lerna/collect-updates/-/collect-updates-3.18.0.tgz#6086c64df3244993cc0a7f8fc0ddd6a0103008a6"
+  integrity sha512-LJMKgWsE/var1RSvpKDIxS8eJ7POADEc0HM3FQiTpEczhP6aZfv9x3wlDjaHpZm9MxJyQilqxZcasRANmRcNgw==
   dependencies:
   dependencies:
-    "@lerna/child-process" "3.14.2"
-    "@lerna/describe-ref" "3.14.2"
+    "@lerna/child-process" "3.16.5"
+    "@lerna/describe-ref" "3.16.5"
     minimatch "^3.0.4"
     minimatch "^3.0.4"
     npmlog "^4.1.2"
     npmlog "^4.1.2"
     slash "^2.0.0"
     slash "^2.0.0"
 
 
-"@lerna/command@3.16.0":
-  version "3.16.0"
-  resolved "https://registry.npmjs.org/@lerna/command/-/command-3.16.0.tgz#ba3dba49cb5ce4d11b48269cf95becd86e30773f"
-  integrity sha512-u7tE4GC4/gfbPA9eQg+0ulnoJ+PMoMqomx033r/IxqZrHtmJR9+pF/37S0fsxJ2hX/RMFPC7c9Q/i8NEufSpdQ==
+"@lerna/command@3.18.0":
+  version "3.18.0"
+  resolved "https://registry.npmjs.org/@lerna/command/-/command-3.18.0.tgz#1e40399324a69d26a78969d59cf60e19b2f13fc3"
+  integrity sha512-JQ0TGzuZc9Ky8xtwtSLywuvmkU8X62NTUT3rMNrUykIkOxBaO+tE0O98u2yo/9BYOeTRji9IsjKZEl5i9Qt0xQ==
   dependencies:
   dependencies:
-    "@lerna/child-process" "3.14.2"
-    "@lerna/package-graph" "3.16.0"
-    "@lerna/project" "3.16.0"
+    "@lerna/child-process" "3.16.5"
+    "@lerna/package-graph" "3.18.0"
+    "@lerna/project" "3.18.0"
     "@lerna/validation-error" "3.13.0"
     "@lerna/validation-error" "3.13.0"
     "@lerna/write-log-file" "3.13.0"
     "@lerna/write-log-file" "3.13.0"
     dedent "^0.7.0"
     dedent "^0.7.0"
@@ -1101,14 +1092,14 @@
     fs-extra "^8.1.0"
     fs-extra "^8.1.0"
     npmlog "^4.1.2"
     npmlog "^4.1.2"
 
 
-"@lerna/create@3.16.0":
-  version "3.16.0"
-  resolved "https://registry.npmjs.org/@lerna/create/-/create-3.16.0.tgz#4de841ec7d98b29bb19fb7d6ad982e65f7a150e8"
-  integrity sha512-OZApR1Iz7awutbmj4sAArwhqCyKgcrnw9rH0aWAUrkYWrD1w4TwkvAcYAsfx5GpQGbLQwoXhoyyPwPfZRRWz3Q==
+"@lerna/create@3.18.0":
+  version "3.18.0"
+  resolved "https://registry.npmjs.org/@lerna/create/-/create-3.18.0.tgz#78ba4af5eced661944a12b9d7da8553c096c390d"
+  integrity sha512-y9oS7ND5T13c+cCTJHa2Y9in02ppzyjsNynVWFuS40eIzZ3z058d9+3qSBt1nkbbQlVyfLoP6+bZPsjyzap5ig==
   dependencies:
   dependencies:
     "@evocateur/pacote" "^9.6.3"
     "@evocateur/pacote" "^9.6.3"
-    "@lerna/child-process" "3.14.2"
-    "@lerna/command" "3.16.0"
+    "@lerna/child-process" "3.16.5"
+    "@lerna/command" "3.18.0"
     "@lerna/npm-conf" "3.16.0"
     "@lerna/npm-conf" "3.16.0"
     "@lerna/validation-error" "3.13.0"
     "@lerna/validation-error" "3.13.0"
     camelcase "^5.0.0"
     camelcase "^5.0.0"
@@ -1125,49 +1116,51 @@
     validate-npm-package-name "^3.0.0"
     validate-npm-package-name "^3.0.0"
     whatwg-url "^7.0.0"
     whatwg-url "^7.0.0"
 
 
-"@lerna/describe-ref@3.14.2":
-  version "3.14.2"
-  resolved "https://registry.npmjs.org/@lerna/describe-ref/-/describe-ref-3.14.2.tgz#edc3c973f5ca9728d23358c4f4d3b55a21f65be5"
-  integrity sha512-qa5pzDRK2oBQXNjyRmRnN7E8a78NMYfQjjlRFB0KNHMsT6mCiL9+8kIS39sSE2NqT8p7xVNo2r2KAS8R/m3CoQ==
+"@lerna/describe-ref@3.16.5":
+  version "3.16.5"
+  resolved "https://registry.npmjs.org/@lerna/describe-ref/-/describe-ref-3.16.5.tgz#a338c25aaed837d3dc70b8a72c447c5c66346ac0"
+  integrity sha512-c01+4gUF0saOOtDBzbLMFOTJDHTKbDFNErEY6q6i9QaXuzy9LNN62z+Hw4acAAZuJQhrVWncVathcmkkjvSVGw==
   dependencies:
   dependencies:
-    "@lerna/child-process" "3.14.2"
+    "@lerna/child-process" "3.16.5"
     npmlog "^4.1.2"
     npmlog "^4.1.2"
 
 
-"@lerna/diff@3.16.0":
-  version "3.16.0"
-  resolved "https://registry.npmjs.org/@lerna/diff/-/diff-3.16.0.tgz#6d09a786f9f5b343a2fdc460eb0be08a05b420aa"
-  integrity sha512-QUpVs5TPl8vBIne10/vyjUxanQBQQp7Lk3iaB8MnCysKr0O+oy7trWeFVDPEkBTCD177By7yPGyW5Yey1nCBbA==
+"@lerna/diff@3.18.0":
+  version "3.18.0"
+  resolved "https://registry.npmjs.org/@lerna/diff/-/diff-3.18.0.tgz#9638ff4b46e2a8b0d4ebf54cf2f267ac2f8fdb29"
+  integrity sha512-3iLNlpurc2nV9k22w8ini2Zjm2UPo3xtQgWyqdA6eJjvge0+5AlNAWfPoV6cV+Hc1xDbJD2YDSFpZPJ1ZGilRw==
   dependencies:
   dependencies:
-    "@lerna/child-process" "3.14.2"
-    "@lerna/command" "3.16.0"
+    "@lerna/child-process" "3.16.5"
+    "@lerna/command" "3.18.0"
     "@lerna/validation-error" "3.13.0"
     "@lerna/validation-error" "3.13.0"
     npmlog "^4.1.2"
     npmlog "^4.1.2"
 
 
-"@lerna/exec@3.16.0":
-  version "3.16.0"
-  resolved "https://registry.npmjs.org/@lerna/exec/-/exec-3.16.0.tgz#2b6c033cee46181b6eede0eb12aad5c2c0181e89"
-  integrity sha512-mH3O5NXf/O88jBaBBTUf+d56CUkxpg782s3Jxy7HWbVuSUULt3iMRPTh+zEXO5/555etsIVVDDyUR76meklrJA==
+"@lerna/exec@3.18.0":
+  version "3.18.0"
+  resolved "https://registry.npmjs.org/@lerna/exec/-/exec-3.18.0.tgz#d9ec0b7ca06b7521f0b9f14a164e2d4ca5e1b3b9"
+  integrity sha512-hwkuzg1+38+pbzdZPhGtLIYJ59z498/BCNzR8d4/nfMYm8lFbw9RgJJajLcdbuJ9LJ08cZ93hf8OlzetL84TYg==
   dependencies:
   dependencies:
-    "@lerna/child-process" "3.14.2"
-    "@lerna/command" "3.16.0"
-    "@lerna/filter-options" "3.16.0"
-    "@lerna/run-topologically" "3.16.0"
+    "@lerna/child-process" "3.16.5"
+    "@lerna/command" "3.18.0"
+    "@lerna/filter-options" "3.18.0"
+    "@lerna/run-topologically" "3.18.0"
     "@lerna/validation-error" "3.13.0"
     "@lerna/validation-error" "3.13.0"
     p-map "^2.1.0"
     p-map "^2.1.0"
 
 
-"@lerna/filter-options@3.16.0":
-  version "3.16.0"
-  resolved "https://registry.npmjs.org/@lerna/filter-options/-/filter-options-3.16.0.tgz#b1660b4480c02a5c6efa4d0cd98b9afde4ed0bba"
-  integrity sha512-InIi1fF8+PxpCwir9bIy+pGxrdE6hvN0enIs1eNGCVS1TTE8osNgiZXa838bMQ1yaEccdcnVX6Z03BNKd56kNg==
+"@lerna/filter-options@3.18.0":
+  version "3.18.0"
+  resolved "https://registry.npmjs.org/@lerna/filter-options/-/filter-options-3.18.0.tgz#406667dc75a8fc813c26a91bde754b6a73e1a868"
+  integrity sha512-UGVcixs3TGzD8XSmFSbwUVVQnAjaZ6Rmt8Vuq2RcR98ULkGB1LiGNMY89XaNBhaaA8vx7yQWiLmJi2AfmD63Qg==
   dependencies:
   dependencies:
-    "@lerna/collect-updates" "3.16.0"
-    "@lerna/filter-packages" "3.16.0"
+    "@lerna/collect-updates" "3.18.0"
+    "@lerna/filter-packages" "3.18.0"
     dedent "^0.7.0"
     dedent "^0.7.0"
+    figgy-pudding "^3.5.1"
+    npmlog "^4.1.2"
 
 
-"@lerna/filter-packages@3.16.0":
-  version "3.16.0"
-  resolved "https://registry.npmjs.org/@lerna/filter-packages/-/filter-packages-3.16.0.tgz#7d34dc8530c71016263d6f67dc65308ecf11c9fc"
-  integrity sha512-eGFzQTx0ogkGDCnbTuXqssryR6ilp8+dcXt6B+aq1MaqL/vOJRZyqMm4TY3CUOUnzZCi9S2WWyMw3PnAJOF+kg==
+"@lerna/filter-packages@3.18.0":
+  version "3.18.0"
+  resolved "https://registry.npmjs.org/@lerna/filter-packages/-/filter-packages-3.18.0.tgz#6a7a376d285208db03a82958cfb8172e179b4e70"
+  integrity sha512-6/0pMM04bCHNATIOkouuYmPg6KH3VkPCIgTfQmdkPJTullERyEQfNUKikrefjxo1vHOoCACDpy65JYyKiAbdwQ==
   dependencies:
   dependencies:
     "@lerna/validation-error" "3.13.0"
     "@lerna/validation-error" "3.13.0"
     multimatch "^3.0.0"
     multimatch "^3.0.0"
@@ -1189,12 +1182,12 @@
     ssri "^6.0.1"
     ssri "^6.0.1"
     tar "^4.4.8"
     tar "^4.4.8"
 
 
-"@lerna/github-client@3.16.0":
-  version "3.16.0"
-  resolved "https://registry.npmjs.org/@lerna/github-client/-/github-client-3.16.0.tgz#619874e461641d4f59ab1b3f1a7ba22dba88125d"
-  integrity sha512-IVJjcKjkYaUEPJsDyAblHGEFFNKCRyMagbIDm14L7Ab94ccN6i4TKOqAFEJn2SJHYvKKBdp3Zj2zNlASOMe3DA==
+"@lerna/github-client@3.16.5":
+  version "3.16.5"
+  resolved "https://registry.npmjs.org/@lerna/github-client/-/github-client-3.16.5.tgz#2eb0235c3bf7a7e5d92d73e09b3761ab21f35c2e"
+  integrity sha512-rHQdn8Dv/CJrO3VouOP66zAcJzrHsm+wFuZ4uGAai2At2NkgKH+tpNhQy2H1PSC0Ezj9LxvdaHYrUzULqVK5Hw==
   dependencies:
   dependencies:
-    "@lerna/child-process" "3.14.2"
+    "@lerna/child-process" "3.16.5"
     "@octokit/plugin-enterprise-rest" "^3.6.1"
     "@octokit/plugin-enterprise-rest" "^3.6.1"
     "@octokit/rest" "^16.28.4"
     "@octokit/rest" "^16.28.4"
     git-url-parse "^11.1.2"
     git-url-parse "^11.1.2"
@@ -1214,21 +1207,21 @@
   resolved "https://registry.npmjs.org/@lerna/global-options/-/global-options-3.13.0.tgz#217662290db06ad9cf2c49d8e3100ee28eaebae1"
   resolved "https://registry.npmjs.org/@lerna/global-options/-/global-options-3.13.0.tgz#217662290db06ad9cf2c49d8e3100ee28eaebae1"
   integrity sha512-SlZvh1gVRRzYLVluz9fryY1nJpZ0FHDGB66U9tFfvnnxmueckRQxLopn3tXj3NU1kc3QANT2I5BsQkOqZ4TEFQ==
   integrity sha512-SlZvh1gVRRzYLVluz9fryY1nJpZ0FHDGB66U9tFfvnnxmueckRQxLopn3tXj3NU1kc3QANT2I5BsQkOqZ4TEFQ==
 
 
-"@lerna/has-npm-version@3.16.0":
-  version "3.16.0"
-  resolved "https://registry.npmjs.org/@lerna/has-npm-version/-/has-npm-version-3.16.0.tgz#55764a4ce792f0c8553cf996a17f554b9e843288"
-  integrity sha512-TIY036dA9J8OyTrZq9J+it2DVKifL65k7hK8HhkUPpitJkw6jwbMObA/8D40LOGgWNPweJWqmlrTbRSwsR7DrQ==
+"@lerna/has-npm-version@3.16.5":
+  version "3.16.5"
+  resolved "https://registry.npmjs.org/@lerna/has-npm-version/-/has-npm-version-3.16.5.tgz#ab83956f211d8923ea6afe9b979b38cc73b15326"
+  integrity sha512-WL7LycR9bkftyqbYop5rEGJ9sRFIV55tSGmbN1HLrF9idwOCD7CLrT64t235t3t4O5gehDnwKI5h2U3oxTrF8Q==
   dependencies:
   dependencies:
-    "@lerna/child-process" "3.14.2"
+    "@lerna/child-process" "3.16.5"
     semver "^6.2.0"
     semver "^6.2.0"
 
 
-"@lerna/import@3.16.0":
-  version "3.16.0"
-  resolved "https://registry.npmjs.org/@lerna/import/-/import-3.16.0.tgz#b57cb453f4acfc60f6541fcbba10674055cb179d"
-  integrity sha512-trsOmGHzw0rL/f8BLNvd+9PjoTkXq2Dt4/V2UCha254hMQaYutbxcYu8iKPxz9x86jSPlH7FpbTkkHXDsoY7Yg==
+"@lerna/import@3.18.0":
+  version "3.18.0"
+  resolved "https://registry.npmjs.org/@lerna/import/-/import-3.18.0.tgz#c6b124b346a097e6c0f3f1ed4921a278d18bc80b"
+  integrity sha512-2pYIkkBTZsEdccfc+dPsKZeSw3tBzKSyl0b2lGrfmNX2Y41qqOzsJCyI1WO1uvEIP8aOaLy4hPpqRIBe4ee7hw==
   dependencies:
   dependencies:
-    "@lerna/child-process" "3.14.2"
-    "@lerna/command" "3.16.0"
+    "@lerna/child-process" "3.16.5"
+    "@lerna/command" "3.18.0"
     "@lerna/prompt" "3.13.0"
     "@lerna/prompt" "3.13.0"
     "@lerna/pulse-till-done" "3.13.0"
     "@lerna/pulse-till-done" "3.13.0"
     "@lerna/validation-error" "3.13.0"
     "@lerna/validation-error" "3.13.0"
@@ -1236,44 +1229,44 @@
     fs-extra "^8.1.0"
     fs-extra "^8.1.0"
     p-map-series "^1.0.0"
     p-map-series "^1.0.0"
 
 
-"@lerna/init@3.16.0":
-  version "3.16.0"
-  resolved "https://registry.npmjs.org/@lerna/init/-/init-3.16.0.tgz#31e0d66bbededee603338b487a42674a072b7a7d"
-  integrity sha512-Ybol/x5xMtBgokx4j7/Y3u0ZmNh0NiSWzBFVaOs2NOJKvuqrWimF67DKVz7yYtTYEjtaMdug64ohFF4jcT/iag==
+"@lerna/init@3.18.0":
+  version "3.18.0"
+  resolved "https://registry.npmjs.org/@lerna/init/-/init-3.18.0.tgz#b23b9170cce1f4630170dd744e8ee75785ea898d"
+  integrity sha512-/vHpmXkMlSaJaq25v5K13mcs/2L7E32O6dSsEkHaZCDRiV2BOqsZng9jjbE/4ynfsWfLLlU9ZcydwG72C3I+mQ==
   dependencies:
   dependencies:
-    "@lerna/child-process" "3.14.2"
-    "@lerna/command" "3.16.0"
+    "@lerna/child-process" "3.16.5"
+    "@lerna/command" "3.18.0"
     fs-extra "^8.1.0"
     fs-extra "^8.1.0"
     p-map "^2.1.0"
     p-map "^2.1.0"
     write-json-file "^3.2.0"
     write-json-file "^3.2.0"
 
 
-"@lerna/link@3.16.2":
-  version "3.16.2"
-  resolved "https://registry.npmjs.org/@lerna/link/-/link-3.16.2.tgz#6c3a5658f6448a64dddca93d9348ac756776f6f6"
-  integrity sha512-eCPg5Lo8HT525fIivNoYF3vWghO3UgEVFdbsiPmhzwI7IQyZro5HWYzLtywSAdEog5XZpd2Bbn0CsoHWBB3gww==
+"@lerna/link@3.18.0":
+  version "3.18.0"
+  resolved "https://registry.npmjs.org/@lerna/link/-/link-3.18.0.tgz#bc72dc62ef4d8fb842b3286887980f98b764781d"
+  integrity sha512-FbbIpH0EpsC+dpAbvxCoF3cn7F1MAyJjEa5Lh3XkDGATOlinMFuKCbmX0NLpOPQZ5zghvrui97cx+jz5F2IlHw==
   dependencies:
   dependencies:
-    "@lerna/command" "3.16.0"
-    "@lerna/package-graph" "3.16.0"
-    "@lerna/symlink-dependencies" "3.16.2"
+    "@lerna/command" "3.18.0"
+    "@lerna/package-graph" "3.18.0"
+    "@lerna/symlink-dependencies" "3.17.0"
     p-map "^2.1.0"
     p-map "^2.1.0"
     slash "^2.0.0"
     slash "^2.0.0"
 
 
-"@lerna/list@3.16.0":
-  version "3.16.0"
-  resolved "https://registry.npmjs.org/@lerna/list/-/list-3.16.0.tgz#883c00b2baf1e03c93e54391372f67a01b773c2f"
-  integrity sha512-TkvstoPsgKqqQ0KfRumpsdMXfRSEhdXqOLq519XyI5IRWYxhoqXqfi8gG37UoBPhBNoe64japn5OjphF3rOmQA==
+"@lerna/list@3.18.0":
+  version "3.18.0"
+  resolved "https://registry.npmjs.org/@lerna/list/-/list-3.18.0.tgz#6e5fe545ce4ba7c1eeb6d6cf69240d06c02bd496"
+  integrity sha512-mpB7Q6T+n2CaiPFz0LuOE+rXphDfHm0mKIwShnyS/XDcii8jXv+z9Iytj8p3rfCH2I1L80j2qL6jWzyGy/uzKA==
   dependencies:
   dependencies:
-    "@lerna/command" "3.16.0"
-    "@lerna/filter-options" "3.16.0"
-    "@lerna/listable" "3.16.0"
+    "@lerna/command" "3.18.0"
+    "@lerna/filter-options" "3.18.0"
+    "@lerna/listable" "3.18.0"
     "@lerna/output" "3.13.0"
     "@lerna/output" "3.13.0"
 
 
-"@lerna/listable@3.16.0":
-  version "3.16.0"
-  resolved "https://registry.npmjs.org/@lerna/listable/-/listable-3.16.0.tgz#e6dc47a2d5a6295222663486f50e5cffc580f043"
-  integrity sha512-mtdAT2EEECqrJSDm/aXlOUFr1MRE4p6hppzY//Klp05CogQy6uGaKk+iKG5yyCLaOXFFZvG4HfO11CmoGSDWzw==
+"@lerna/listable@3.18.0":
+  version "3.18.0"
+  resolved "https://registry.npmjs.org/@lerna/listable/-/listable-3.18.0.tgz#752b014406a9a012486626d22e940edb8205973a"
+  integrity sha512-9gLGKYNLSKeurD+sJ2RA+nz4Ftulr91U127gefz0RlmAPpYSjwcJkxwa0UfJvpQTXv9C7yzHLnn0BjyAQRjuew==
   dependencies:
   dependencies:
-    "@lerna/query-graph" "3.16.0"
+    "@lerna/query-graph" "3.18.0"
     chalk "^2.3.1"
     chalk "^2.3.1"
     columnify "^1.5.4"
     columnify "^1.5.4"
 
 
@@ -1295,10 +1288,10 @@
     config-chain "^1.1.11"
     config-chain "^1.1.11"
     pify "^4.0.1"
     pify "^4.0.1"
 
 
-"@lerna/npm-dist-tag@3.16.0":
-  version "3.16.0"
-  resolved "https://registry.npmjs.org/@lerna/npm-dist-tag/-/npm-dist-tag-3.16.0.tgz#b2184cee5e1f291277396854820e1117a544b7ee"
-  integrity sha512-MQrBkqJJB9+eNphuj9w90QPMOs4NQXMuSRk9NqzeFunOmdDopPCV0Q7IThSxEuWnhJ2n3B7G0vWUP7tNMPdqIQ==
+"@lerna/npm-dist-tag@3.18.1":
+  version "3.18.1"
+  resolved "https://registry.npmjs.org/@lerna/npm-dist-tag/-/npm-dist-tag-3.18.1.tgz#d4dd82ea92e41e960b7117f83102ebcd7a23e511"
+  integrity sha512-vWkZh2T/O9OjPLDrba0BTWO7ug/C3sCwjw7Qyk1aEbxMBXB/eEJPqirwJTWT+EtRJQYB01ky3K8ZFOhElVyjLw==
   dependencies:
   dependencies:
     "@evocateur/npm-registry-fetch" "^4.0.0"
     "@evocateur/npm-registry-fetch" "^4.0.0"
     "@lerna/otplease" "3.16.0"
     "@lerna/otplease" "3.16.0"
@@ -1306,12 +1299,12 @@
     npm-package-arg "^6.1.0"
     npm-package-arg "^6.1.0"
     npmlog "^4.1.2"
     npmlog "^4.1.2"
 
 
-"@lerna/npm-install@3.16.0":
-  version "3.16.0"
-  resolved "https://registry.npmjs.org/@lerna/npm-install/-/npm-install-3.16.0.tgz#8ec76a7a13b183bde438fd46296bf7a0d6f86017"
-  integrity sha512-APUOIilZCzDzce92uLEwzt1r7AEMKT/hWA1ThGJL+PO9Rn8A95Km3o2XZAYG4W0hR+P4O2nSVuKbsjQtz8CjFQ==
+"@lerna/npm-install@3.16.5":
+  version "3.16.5"
+  resolved "https://registry.npmjs.org/@lerna/npm-install/-/npm-install-3.16.5.tgz#d6bfdc16f81285da66515ae47924d6e278d637d3"
+  integrity sha512-hfiKk8Eku6rB9uApqsalHHTHY+mOrrHeWEs+gtg7+meQZMTS3kzv4oVp5cBZigndQr3knTLjwthT/FX4KvseFg==
   dependencies:
   dependencies:
-    "@lerna/child-process" "3.14.2"
+    "@lerna/child-process" "3.16.5"
     "@lerna/get-npm-exec-opts" "3.13.0"
     "@lerna/get-npm-exec-opts" "3.13.0"
     fs-extra "^8.1.0"
     fs-extra "^8.1.0"
     npm-package-arg "^6.1.0"
     npm-package-arg "^6.1.0"
@@ -1334,12 +1327,12 @@
     pify "^4.0.1"
     pify "^4.0.1"
     read-package-json "^2.0.13"
     read-package-json "^2.0.13"
 
 
-"@lerna/npm-run-script@3.14.2":
-  version "3.14.2"
-  resolved "https://registry.npmjs.org/@lerna/npm-run-script/-/npm-run-script-3.14.2.tgz#8c518ea9d241a641273e77aad6f6fddc16779c3f"
-  integrity sha512-LbVFv+nvAoRTYLMrJlJ8RiakHXrLslL7Jp/m1R18vYrB8LYWA3ey+nz5Tel2OELzmjUiemAKZsD9h6i+Re5egg==
+"@lerna/npm-run-script@3.16.5":
+  version "3.16.5"
+  resolved "https://registry.npmjs.org/@lerna/npm-run-script/-/npm-run-script-3.16.5.tgz#9c2ec82453a26c0b46edc0bb7c15816c821f5c15"
+  integrity sha512-1asRi+LjmVn3pMjEdpqKJZFT/3ZNpb+VVeJMwrJaV/3DivdNg7XlPK9LTrORuKU4PSvhdEZvJmSlxCKyDpiXsQ==
   dependencies:
   dependencies:
-    "@lerna/child-process" "3.14.2"
+    "@lerna/child-process" "3.16.5"
     "@lerna/get-npm-exec-opts" "3.13.0"
     "@lerna/get-npm-exec-opts" "3.13.0"
     npmlog "^4.1.2"
     npmlog "^4.1.2"
 
 
@@ -1372,10 +1365,10 @@
     tar "^4.4.10"
     tar "^4.4.10"
     temp-write "^3.4.0"
     temp-write "^3.4.0"
 
 
-"@lerna/package-graph@3.16.0":
-  version "3.16.0"
-  resolved "https://registry.npmjs.org/@lerna/package-graph/-/package-graph-3.16.0.tgz#909c90fb41e02f2c19387342d2a5eefc36d56836"
-  integrity sha512-A2mum/gNbv7zCtAwJqoxzqv89As73OQNK2MgSX1SHWya46qoxO9a9Z2c5lOFQ8UFN5ZxqWMfFYXRCz7qzwmFXw==
+"@lerna/package-graph@3.18.0":
+  version "3.18.0"
+  resolved "https://registry.npmjs.org/@lerna/package-graph/-/package-graph-3.18.0.tgz#eb42d14404a55b26b2472081615e26b0817cd91a"
+  integrity sha512-BLYDHO5ihPh20i3zoXfLZ5ZWDCrPuGANgVhl7k5pCmRj90LCvT+C7V3zrw70fErGAfvkcYepMqxD+oBrAYwquQ==
   dependencies:
   dependencies:
     "@lerna/prerelease-id-from-version" "3.16.0"
     "@lerna/prerelease-id-from-version" "3.16.0"
     "@lerna/validation-error" "3.13.0"
     "@lerna/validation-error" "3.13.0"
@@ -1399,10 +1392,10 @@
   dependencies:
   dependencies:
     semver "^6.2.0"
     semver "^6.2.0"
 
 
-"@lerna/project@3.16.0":
-  version "3.16.0"
-  resolved "https://registry.npmjs.org/@lerna/project/-/project-3.16.0.tgz#2469a4e346e623fd922f38f5a12931dfb8f2a946"
-  integrity sha512-NrKcKK1EqXqhrGvslz6Q36+ZHuK3zlDhGdghRqnxDcHxMPT01NgLcmsnymmQ+gjMljuLRmvKYYCuHrknzX8VrA==
+"@lerna/project@3.18.0":
+  version "3.18.0"
+  resolved "https://registry.npmjs.org/@lerna/project/-/project-3.18.0.tgz#56feee01daeb42c03cbdf0ed8a2a10cbce32f670"
+  integrity sha512-+LDwvdAp0BurOAWmeHE3uuticsq9hNxBI0+FMHiIai8jrygpJGahaQrBYWpwbshbQyVLeQgx3+YJdW2TbEdFWA==
   dependencies:
   dependencies:
     "@lerna/package" "3.16.0"
     "@lerna/package" "3.16.0"
     "@lerna/validation-error" "3.13.0"
     "@lerna/validation-error" "3.13.0"
@@ -1425,22 +1418,22 @@
     inquirer "^6.2.0"
     inquirer "^6.2.0"
     npmlog "^4.1.2"
     npmlog "^4.1.2"
 
 
-"@lerna/publish@3.16.4":
-  version "3.16.4"
-  resolved "https://registry.npmjs.org/@lerna/publish/-/publish-3.16.4.tgz#4cd55d8be9943d9a68e316e930a90cda8590500e"
-  integrity sha512-XZY+gRuF7/v6PDQwl7lvZaGWs8CnX6WIPIu+OCcyFPSL/rdWegdN7HieKBHskgX798qRQc2GrveaY7bNoTKXAw==
+"@lerna/publish@3.18.3":
+  version "3.18.3"
+  resolved "https://registry.npmjs.org/@lerna/publish/-/publish-3.18.3.tgz#478bb94ee712a40b723413e437bcb9e307d3709c"
+  integrity sha512-XlfWOWIhaSK0Y2sX5ppNWI5Y3CDtlxMcQa1hTbZlC5rrDA6vD32iutbmH6Ix3c6wtvVbSkgA39GWsQEXxPS+7w==
   dependencies:
   dependencies:
     "@evocateur/libnpmaccess" "^3.1.2"
     "@evocateur/libnpmaccess" "^3.1.2"
     "@evocateur/npm-registry-fetch" "^4.0.0"
     "@evocateur/npm-registry-fetch" "^4.0.0"
     "@evocateur/pacote" "^9.6.3"
     "@evocateur/pacote" "^9.6.3"
-    "@lerna/check-working-tree" "3.14.2"
-    "@lerna/child-process" "3.14.2"
-    "@lerna/collect-updates" "3.16.0"
-    "@lerna/command" "3.16.0"
-    "@lerna/describe-ref" "3.14.2"
+    "@lerna/check-working-tree" "3.16.5"
+    "@lerna/child-process" "3.16.5"
+    "@lerna/collect-updates" "3.18.0"
+    "@lerna/command" "3.18.0"
+    "@lerna/describe-ref" "3.16.5"
     "@lerna/log-packed" "3.16.0"
     "@lerna/log-packed" "3.16.0"
     "@lerna/npm-conf" "3.16.0"
     "@lerna/npm-conf" "3.16.0"
-    "@lerna/npm-dist-tag" "3.16.0"
+    "@lerna/npm-dist-tag" "3.18.1"
     "@lerna/npm-publish" "3.16.2"
     "@lerna/npm-publish" "3.16.2"
     "@lerna/otplease" "3.16.0"
     "@lerna/otplease" "3.16.0"
     "@lerna/output" "3.13.0"
     "@lerna/output" "3.13.0"
@@ -1449,9 +1442,9 @@
     "@lerna/prompt" "3.13.0"
     "@lerna/prompt" "3.13.0"
     "@lerna/pulse-till-done" "3.13.0"
     "@lerna/pulse-till-done" "3.13.0"
     "@lerna/run-lifecycle" "3.16.2"
     "@lerna/run-lifecycle" "3.16.2"
-    "@lerna/run-topologically" "3.16.0"
+    "@lerna/run-topologically" "3.18.0"
     "@lerna/validation-error" "3.13.0"
     "@lerna/validation-error" "3.13.0"
-    "@lerna/version" "3.16.4"
+    "@lerna/version" "3.18.3"
     figgy-pudding "^3.5.1"
     figgy-pudding "^3.5.1"
     fs-extra "^8.1.0"
     fs-extra "^8.1.0"
     npm-package-arg "^6.1.0"
     npm-package-arg "^6.1.0"
@@ -1468,12 +1461,12 @@
   dependencies:
   dependencies:
     npmlog "^4.1.2"
     npmlog "^4.1.2"
 
 
-"@lerna/query-graph@3.16.0":
-  version "3.16.0"
-  resolved "https://registry.npmjs.org/@lerna/query-graph/-/query-graph-3.16.0.tgz#e6a46ebcd9d5b03f018a06eca2b471735353953c"
-  integrity sha512-p0RO+xmHDO95ChJdWkcy9TNLysLkoDARXeRHzY5U54VCwl3Ot/2q8fMCVlA5UeGXDutEyyByl3URqEpcQCWI7Q==
+"@lerna/query-graph@3.18.0":
+  version "3.18.0"
+  resolved "https://registry.npmjs.org/@lerna/query-graph/-/query-graph-3.18.0.tgz#43801a2f1b80a0ea0bfd9d42d470605326a3035d"
+  integrity sha512-fgUhLx6V0jDuKZaKj562jkuuhrfVcjl5sscdfttJ8dXNVADfDz76nzzwLY0ZU7/0m69jDedohn5Fx5p7hDEVEg==
   dependencies:
   dependencies:
-    "@lerna/package-graph" "3.16.0"
+    "@lerna/package-graph" "3.18.0"
     figgy-pudding "^3.5.1"
     figgy-pudding "^3.5.1"
 
 
 "@lerna/resolve-symlink@3.16.0":
 "@lerna/resolve-symlink@3.16.0":
@@ -1485,12 +1478,12 @@
     npmlog "^4.1.2"
     npmlog "^4.1.2"
     read-cmd-shim "^1.0.1"
     read-cmd-shim "^1.0.1"
 
 
-"@lerna/rimraf-dir@3.14.2":
-  version "3.14.2"
-  resolved "https://registry.npmjs.org/@lerna/rimraf-dir/-/rimraf-dir-3.14.2.tgz#103a49882abd85d42285d05cc76869b89f21ffd2"
-  integrity sha512-eFNkZsy44Bu9v1Hrj5Zk6omzg8O9h/7W6QYK1TTUHeyrjTEwytaNQlqF0lrTLmEvq55sviV42NC/8P3M2cvq8Q==
+"@lerna/rimraf-dir@3.16.5":
+  version "3.16.5"
+  resolved "https://registry.npmjs.org/@lerna/rimraf-dir/-/rimraf-dir-3.16.5.tgz#04316ab5ffd2909657aaf388ea502cb8c2f20a09"
+  integrity sha512-bQlKmO0pXUsXoF8lOLknhyQjOZsCc0bosQDoX4lujBXSWxHVTg1VxURtWf2lUjz/ACsJVDfvHZbDm8kyBk5okA==
   dependencies:
   dependencies:
-    "@lerna/child-process" "3.14.2"
+    "@lerna/child-process" "3.16.5"
     npmlog "^4.1.2"
     npmlog "^4.1.2"
     path-exists "^3.0.0"
     path-exists "^3.0.0"
     rimraf "^2.6.2"
     rimraf "^2.6.2"
@@ -1505,55 +1498,47 @@
     npm-lifecycle "^3.1.2"
     npm-lifecycle "^3.1.2"
     npmlog "^4.1.2"
     npmlog "^4.1.2"
 
 
-"@lerna/run-parallel-batches@3.16.0":
-  version "3.16.0"
-  resolved "https://registry.npmjs.org/@lerna/run-parallel-batches/-/run-parallel-batches-3.16.0.tgz#5ace7911a2dd31dfd1e53c61356034e27df0e1fb"
-  integrity sha512-2J/Nyv+MvogmQEfC7VcS21ifk7w0HVvzo2yOZRPvkCzGRu/rducxtB4RTcr58XCZ8h/Bt1aqQYKExu3c/3GXwg==
-  dependencies:
-    p-map "^2.1.0"
-    p-map-series "^1.0.0"
-
-"@lerna/run-topologically@3.16.0":
-  version "3.16.0"
-  resolved "https://registry.npmjs.org/@lerna/run-topologically/-/run-topologically-3.16.0.tgz#39e29cfc628bbc8e736d8e0d0e984997ac01bbf5"
-  integrity sha512-4Hlpv4zDtKWa5Z0tPkeu0sK+bxZEKgkNESMGmWrUCNfj7xwvAJurcraK8+a2Y0TFYwf0qjSLY/MzX+ZbJA3Cgw==
+"@lerna/run-topologically@3.18.0":
+  version "3.18.0"
+  resolved "https://registry.npmjs.org/@lerna/run-topologically/-/run-topologically-3.18.0.tgz#9508604553cfbeba106cd84b711fade17947f94a"
+  integrity sha512-lrfEewwuUMC3ioxf9Z9NdHUakN6ihekcPfdYbzR2slmdbjYKmIA5srkWdrK8NwOpQCAuekpOovH2s8X3FGEopg==
   dependencies:
   dependencies:
-    "@lerna/query-graph" "3.16.0"
+    "@lerna/query-graph" "3.18.0"
     figgy-pudding "^3.5.1"
     figgy-pudding "^3.5.1"
     p-queue "^4.0.0"
     p-queue "^4.0.0"
 
 
-"@lerna/run@3.16.0":
-  version "3.16.0"
-  resolved "https://registry.npmjs.org/@lerna/run/-/run-3.16.0.tgz#1ea568c6f303e47fa00b3403a457836d40738fd2"
-  integrity sha512-woTeLlB1OAAz4zzjdI6RyIxSGuxiUPHJZm89E1pDEPoWwtQV6HMdMgrsQd9ATsJ5Ez280HH4bF/LStAlqW8Ufg==
+"@lerna/run@3.18.0":
+  version "3.18.0"
+  resolved "https://registry.npmjs.org/@lerna/run/-/run-3.18.0.tgz#b7069880f6313e4c6026b564b7b76e5d0f30a521"
+  integrity sha512-sblxHBZ9djaaG7wefPcfEicDqzrB7CP1m/jIB0JvPEQwG4C2qp++ewBpkjRw/mBtjtzg0t7v0nNMXzaWYrQckQ==
   dependencies:
   dependencies:
-    "@lerna/command" "3.16.0"
-    "@lerna/filter-options" "3.16.0"
-    "@lerna/npm-run-script" "3.14.2"
+    "@lerna/command" "3.18.0"
+    "@lerna/filter-options" "3.18.0"
+    "@lerna/npm-run-script" "3.16.5"
     "@lerna/output" "3.13.0"
     "@lerna/output" "3.13.0"
-    "@lerna/run-topologically" "3.16.0"
+    "@lerna/run-topologically" "3.18.0"
     "@lerna/timer" "3.13.0"
     "@lerna/timer" "3.13.0"
     "@lerna/validation-error" "3.13.0"
     "@lerna/validation-error" "3.13.0"
     p-map "^2.1.0"
     p-map "^2.1.0"
 
 
-"@lerna/symlink-binary@3.16.2":
-  version "3.16.2"
-  resolved "https://registry.npmjs.org/@lerna/symlink-binary/-/symlink-binary-3.16.2.tgz#f98a3d9da9e56f1d302dc0d5c2efeb951483ee66"
-  integrity sha512-kz9XVoFOGSF83gg4gBqH+mG6uxfJfTp8Uy+Cam40CvMiuzfODrGkjuBEFoM/uO2QOAwZvbQDYOBpKUa9ZxHS1Q==
+"@lerna/symlink-binary@3.17.0":
+  version "3.17.0"
+  resolved "https://registry.npmjs.org/@lerna/symlink-binary/-/symlink-binary-3.17.0.tgz#8f8031b309863814883d3f009877f82e38aef45a"
+  integrity sha512-RLpy9UY6+3nT5J+5jkM5MZyMmjNHxZIZvXLV+Q3MXrf7Eaa1hNqyynyj4RO95fxbS+EZc4XVSk25DGFQbcRNSQ==
   dependencies:
   dependencies:
     "@lerna/create-symlink" "3.16.2"
     "@lerna/create-symlink" "3.16.2"
     "@lerna/package" "3.16.0"
     "@lerna/package" "3.16.0"
     fs-extra "^8.1.0"
     fs-extra "^8.1.0"
     p-map "^2.1.0"
     p-map "^2.1.0"
 
 
-"@lerna/symlink-dependencies@3.16.2":
-  version "3.16.2"
-  resolved "https://registry.npmjs.org/@lerna/symlink-dependencies/-/symlink-dependencies-3.16.2.tgz#91d9909d35897aebd76a03644a00cd03c4128240"
-  integrity sha512-wnZqGJQ+Jvr1I3inxrkffrFZfmQI7Ta8gySw/UWCy95QtZWF/f5yk8zVIocCAsjzD0wgb3jJE3CFJ9W5iwWk1A==
+"@lerna/symlink-dependencies@3.17.0":
+  version "3.17.0"
+  resolved "https://registry.npmjs.org/@lerna/symlink-dependencies/-/symlink-dependencies-3.17.0.tgz#48d6360e985865a0e56cd8b51b308a526308784a"
+  integrity sha512-KmjU5YT1bpt6coOmdFueTJ7DFJL4H1w5eF8yAQ2zsGNTtZ+i5SGFBWpb9AQaw168dydc3s4eu0W0Sirda+F59Q==
   dependencies:
   dependencies:
     "@lerna/create-symlink" "3.16.2"
     "@lerna/create-symlink" "3.16.2"
     "@lerna/resolve-symlink" "3.16.0"
     "@lerna/resolve-symlink" "3.16.0"
-    "@lerna/symlink-binary" "3.16.2"
+    "@lerna/symlink-binary" "3.17.0"
     fs-extra "^8.1.0"
     fs-extra "^8.1.0"
     p-finally "^1.0.0"
     p-finally "^1.0.0"
     p-map "^2.1.0"
     p-map "^2.1.0"
@@ -1571,26 +1556,27 @@
   dependencies:
   dependencies:
     npmlog "^4.1.2"
     npmlog "^4.1.2"
 
 
-"@lerna/version@3.16.4":
-  version "3.16.4"
-  resolved "https://registry.npmjs.org/@lerna/version/-/version-3.16.4.tgz#b5cc37f3ad98358d599c6196c30b6efc396d42bf"
-  integrity sha512-ikhbMeIn5ljCtWTlHDzO4YvTmpGTX1lWFFIZ79Vd1TNyOr+OUuKLo/+p06mCl2WEdZu0W2s5E9oxfAAQbyDxEg==
+"@lerna/version@3.18.3":
+  version "3.18.3"
+  resolved "https://registry.npmjs.org/@lerna/version/-/version-3.18.3.tgz#01344b39c0749fdeb6c178714733bacbde4d602f"
+  integrity sha512-IXXRlyM3Q/jrc+QZio+bgjG4ZaK+4LYmY4Yql1xyY0wZhAKsWP/Q6ho7e1EJNjNC5dUJO99Fq7qB05MkDf2OcQ==
   dependencies:
   dependencies:
-    "@lerna/check-working-tree" "3.14.2"
-    "@lerna/child-process" "3.14.2"
-    "@lerna/collect-updates" "3.16.0"
-    "@lerna/command" "3.16.0"
+    "@lerna/check-working-tree" "3.16.5"
+    "@lerna/child-process" "3.16.5"
+    "@lerna/collect-updates" "3.18.0"
+    "@lerna/command" "3.18.0"
     "@lerna/conventional-commits" "3.16.4"
     "@lerna/conventional-commits" "3.16.4"
-    "@lerna/github-client" "3.16.0"
+    "@lerna/github-client" "3.16.5"
     "@lerna/gitlab-client" "3.15.0"
     "@lerna/gitlab-client" "3.15.0"
     "@lerna/output" "3.13.0"
     "@lerna/output" "3.13.0"
     "@lerna/prerelease-id-from-version" "3.16.0"
     "@lerna/prerelease-id-from-version" "3.16.0"
     "@lerna/prompt" "3.13.0"
     "@lerna/prompt" "3.13.0"
     "@lerna/run-lifecycle" "3.16.2"
     "@lerna/run-lifecycle" "3.16.2"
-    "@lerna/run-topologically" "3.16.0"
+    "@lerna/run-topologically" "3.18.0"
     "@lerna/validation-error" "3.13.0"
     "@lerna/validation-error" "3.13.0"
     chalk "^2.3.1"
     chalk "^2.3.1"
     dedent "^0.7.0"
     dedent "^0.7.0"
+    load-json-file "^5.3.0"
     minimatch "^3.0.4"
     minimatch "^3.0.4"
     npmlog "^4.1.2"
     npmlog "^4.1.2"
     p-map "^2.1.0"
     p-map "^2.1.0"
@@ -1600,6 +1586,7 @@
     semver "^6.2.0"
     semver "^6.2.0"
     slash "^2.0.0"
     slash "^2.0.0"
     temp-write "^3.4.0"
     temp-write "^3.4.0"
+    write-json-file "^3.2.0"
 
 
 "@lerna/write-log-file@3.13.0":
 "@lerna/write-log-file@3.13.0":
   version "3.13.0"
   version "3.13.0"
@@ -9446,26 +9433,26 @@ left-pad@^1.3.0:
   resolved "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e"
   resolved "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e"
   integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==
   integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==
 
 
-lerna@^3.14.1:
-  version "3.16.4"
-  resolved "https://registry.npmjs.org/lerna/-/lerna-3.16.4.tgz#158cb4f478b680f46f871d5891f531f3a2cb31ec"
-  integrity sha512-0HfwXIkqe72lBLZcNO9NMRfylh5Ng1l8tETgYQ260ZdHRbPuaLKE3Wqnd2YYRRkWfwPyEyZO8mZweBR+slVe1A==
-  dependencies:
-    "@lerna/add" "3.16.2"
-    "@lerna/bootstrap" "3.16.2"
-    "@lerna/changed" "3.16.4"
-    "@lerna/clean" "3.16.0"
-    "@lerna/cli" "3.13.0"
-    "@lerna/create" "3.16.0"
-    "@lerna/diff" "3.16.0"
-    "@lerna/exec" "3.16.0"
-    "@lerna/import" "3.16.0"
-    "@lerna/init" "3.16.0"
-    "@lerna/link" "3.16.2"
-    "@lerna/list" "3.16.0"
-    "@lerna/publish" "3.16.4"
-    "@lerna/run" "3.16.0"
-    "@lerna/version" "3.16.4"
+lerna@^3.16.4:
+  version "3.18.3"
+  resolved "https://registry.npmjs.org/lerna/-/lerna-3.18.3.tgz#c94556e76f98df9c7ae4ed3bc0166117cc42cd13"
+  integrity sha512-Bnr/RjyDSVA2Vu+NArK7do4UIpyy+EShOON7tignfAekPbi7cNDnMMGgSmbCQdKITkqPACMfCMdyq0hJlg6n3g==
+  dependencies:
+    "@lerna/add" "3.18.0"
+    "@lerna/bootstrap" "3.18.0"
+    "@lerna/changed" "3.18.3"
+    "@lerna/clean" "3.18.0"
+    "@lerna/cli" "3.18.0"
+    "@lerna/create" "3.18.0"
+    "@lerna/diff" "3.18.0"
+    "@lerna/exec" "3.18.0"
+    "@lerna/import" "3.18.0"
+    "@lerna/init" "3.18.0"
+    "@lerna/link" "3.18.0"
+    "@lerna/list" "3.18.0"
+    "@lerna/publish" "3.18.3"
+    "@lerna/run" "3.18.0"
+    "@lerna/version" "3.18.3"
     import-local "^2.0.0"
     import-local "^2.0.0"
     npmlog "^4.1.2"
     npmlog "^4.1.2"
 
 
@@ -14813,7 +14800,7 @@ typescript@^3.0.1, typescript@^3.2.4, typescript@^3.3.4000, typescript@~3.6.2:
   resolved "https://registry.npmjs.org/typescript/-/typescript-3.6.2.tgz#105b0f1934119dde543ac8eb71af3a91009efe54"
   resolved "https://registry.npmjs.org/typescript/-/typescript-3.6.2.tgz#105b0f1934119dde543ac8eb71af3a91009efe54"
   integrity sha512-lmQ4L+J6mnu3xweP8+rOrUwzmN+MRAj7TgtJtDaXE5PMyX2kCrklhg3rvOsOIfNeAWMQWO2F1GPc1kMD2vLAfw==
   integrity sha512-lmQ4L+J6mnu3xweP8+rOrUwzmN+MRAj7TgtJtDaXE5PMyX2kCrklhg3rvOsOIfNeAWMQWO2F1GPc1kMD2vLAfw==
 
 
-typescript@^3.4.5:
+typescript@^3.4.5, typescript@^3.6.4:
   version "3.6.4"
   version "3.6.4"
   resolved "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz#b18752bb3792bc1a0281335f7f6ebf1bbfc5b91d"
   resolved "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz#b18752bb3792bc1a0281335f7f6ebf1bbfc5b91d"
   integrity sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==
   integrity sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==
@@ -15688,6 +15675,14 @@ yargs-parser@^13.0.0, yargs-parser@^13.1.1:
     camelcase "^5.0.0"
     camelcase "^5.0.0"
     decamelize "^1.2.0"
     decamelize "^1.2.0"
 
 
+yargs-parser@^15.0.0:
+  version "15.0.0"
+  resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.0.tgz#cdd7a97490ec836195f59f3f4dbe5ea9e8f75f08"
+  integrity sha512-xLTUnCMc4JhxrPEPUYD5IBR1mWCK/aT6+RJ/K29JY2y1vD+FhtgKK0AXRWvI262q3QSffAQuTouFIKUuHX89wQ==
+  dependencies:
+    camelcase "^5.0.0"
+    decamelize "^1.2.0"
+
 yargs-parser@^5.0.0:
 yargs-parser@^5.0.0:
   version "5.0.0"
   version "5.0.0"
   resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a"
   resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a"
@@ -15702,7 +15697,7 @@ yargs-parser@^7.0.0:
   dependencies:
   dependencies:
     camelcase "^4.1.0"
     camelcase "^4.1.0"
 
 
-yargs@12.0.5, yargs@^12.0.1, yargs@^12.0.5:
+yargs@12.0.5, yargs@^12.0.5:
   version "12.0.5"
   version "12.0.5"
   resolved "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13"
   resolved "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13"
   integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==
   integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==
@@ -15772,6 +15767,23 @@ yargs@^13.0.0, yargs@^13.2.1, yargs@^13.3.0:
     y18n "^4.0.0"
     y18n "^4.0.0"
     yargs-parser "^13.1.1"
     yargs-parser "^13.1.1"
 
 
+yargs@^14.2.0:
+  version "14.2.0"
+  resolved "https://registry.npmjs.org/yargs/-/yargs-14.2.0.tgz#f116a9242c4ed8668790b40759b4906c276e76c3"
+  integrity sha512-/is78VKbKs70bVZH7w4YaZea6xcJWOAwkhbR0CFuZBmYtfTYF0xjGJF43AYd8g2Uii1yJwmS5GR2vBmrc32sbg==
+  dependencies:
+    cliui "^5.0.0"
+    decamelize "^1.2.0"
+    find-up "^3.0.0"
+    get-caller-file "^2.0.1"
+    require-directory "^2.1.1"
+    require-main-filename "^2.0.0"
+    set-blocking "^2.0.0"
+    string-width "^3.0.0"
+    which-module "^2.0.0"
+    y18n "^4.0.0"
+    yargs-parser "^15.0.0"
+
 yargs@^7.1.0:
 yargs@^7.1.0:
   version "7.1.0"
   version "7.1.0"
   resolved "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8"
   resolved "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8"