Browse Source

refactor: Extract e2e testing code into own package

Relates to #198
Michael Bromley 6 years ago
parent
commit
8726ab2ef6
71 changed files with 1499 additions and 1268 deletions
  1. 20 1
      packages/common/src/shared-utils.spec.ts
  2. 12 0
      packages/common/src/shared-utils.ts
  3. 72 0
      packages/common/src/simple-deep-clone.spec.ts
  4. 6 1
      packages/common/src/simple-deep-clone.ts
  5. 21 13
      packages/core/e2e/administrator.e2e-spec.ts
  6. 30 28
      packages/core/e2e/auth.e2e-spec.ts
  7. 106 97
      packages/core/e2e/collection.e2e-spec.ts
  8. 6 51
      packages/core/e2e/config/test-config.ts
  9. 37 23
      packages/core/e2e/country.e2e-spec.ts
  10. 95 92
      packages/core/e2e/custom-fields.e2e-spec.ts
  11. 37 36
      packages/core/e2e/customer.e2e-spec.ts
  12. 23 26
      packages/core/e2e/default-search-plugin.e2e-spec.ts
  13. 7 6
      packages/core/e2e/entity-id-strategy.e2e-spec.ts
  14. 15 15
      packages/core/e2e/entity-uuid-strategy.e2e-spec.ts
  15. 51 39
      packages/core/e2e/facet.e2e-spec.ts
  16. 126 0
      packages/core/e2e/fixtures/test-payment-methods.ts
  17. 32 20
      packages/core/e2e/import.e2e-spec.ts
  18. 27 23
      packages/core/e2e/localization.e2e-spec.ts
  19. 24 28
      packages/core/e2e/order-promotion.e2e-spec.ts
  20. 31 103
      packages/core/e2e/order.e2e-spec.ts
  21. 27 28
      packages/core/e2e/plugin.e2e-spec.ts
  22. 17 12
      packages/core/e2e/product-option.e2e-spec.ts
  23. 7 6
      packages/core/e2e/product.e2e-spec.ts
  24. 41 40
      packages/core/e2e/promotion.e2e-spec.ts
  25. 37 27
      packages/core/e2e/role.e2e-spec.ts
  26. 41 41
      packages/core/e2e/shipping-method.e2e-spec.ts
  27. 95 96
      packages/core/e2e/shop-auth.e2e-spec.ts
  28. 8 8
      packages/core/e2e/shop-catalog.e2e-spec.ts
  29. 7 6
      packages/core/e2e/shop-customer.e2e-spec.ts
  30. 30 60
      packages/core/e2e/shop-order.e2e-spec.ts
  31. 20 39
      packages/core/e2e/stock-control.e2e-spec.ts
  32. 0 34
      packages/core/e2e/test-client.ts
  33. 3 2
      packages/core/e2e/utils/await-running-jobs.ts
  34. 0 9
      packages/core/e2e/utils/test-environment.ts
  35. 6 23
      packages/core/e2e/utils/test-order-utils.ts
  36. 33 25
      packages/core/e2e/zone.e2e-spec.ts
  37. 0 14
      packages/core/mock-data/populate-customers.ts
  38. 1 0
      packages/core/package.json
  39. 0 5
      packages/core/src/common/types/common-types.ts
  40. 2 4
      packages/core/src/config/config-helpers.ts
  41. 2 0
      packages/core/src/config/index.ts
  42. 21 7
      packages/core/src/config/merge-config.spec.ts
  43. 10 17
      packages/core/src/config/merge-config.ts
  44. 14 0
      packages/core/src/config/vendure-config.ts
  45. 0 50
      packages/core/src/data-import/import-cli.ts
  46. 0 4
      packages/core/typings.d.ts
  47. 7 3
      packages/dev-server/load-testing/init-load-test.ts
  48. 1 0
      packages/dev-server/package.json
  49. 1 2
      packages/dev-server/populate-dev-server.ts
  50. 1 0
      packages/testing/.gitignore
  51. 5 0
      packages/testing/README.md
  52. 46 0
      packages/testing/package.json
  53. 53 0
      packages/testing/src/config/test-config.ts
  54. 1 1
      packages/testing/src/config/testing-asset-preview-strategy.ts
  55. 1 2
      packages/testing/src/config/testing-asset-storage-strategy.ts
  56. 1 1
      packages/testing/src/config/testing-entity-id-strategy.ts
  57. 21 0
      packages/testing/src/create-test-environment.ts
  58. 4 8
      packages/testing/src/data-population/clear-all-tables.ts
  59. 3 5
      packages/testing/src/data-population/mock-data.service.ts
  60. 23 0
      packages/testing/src/data-population/populate-customers.ts
  61. 12 22
      packages/testing/src/data-population/populate-for-testing.ts
  62. 7 0
      packages/testing/src/index.ts
  63. 7 26
      packages/testing/src/simple-graphql-client.ts
  64. 20 0
      packages/testing/src/test-client.ts
  65. 42 33
      packages/testing/src/test-server.ts
  66. 14 0
      packages/testing/src/types.ts
  67. 6 4
      packages/testing/src/utils/create-upload-post-data.spec.ts
  68. 0 0
      packages/testing/src/utils/create-upload-post-data.ts
  69. 1 2
      packages/testing/src/utils/get-default-channel-token.ts
  70. 12 0
      packages/testing/tsconfig.build.json
  71. 10 0
      packages/testing/tsconfig.json

+ 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()', () => {
     it('works with an empty input array', () => {
@@ -28,3 +28,22 @@ describe('generateAllCombinations()', () => {
         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}`);
 }
 
+/**
+ * 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
  * 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
  * 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;
     }
+    if (isClassInstance(input)) {
+        return input;
+    }
     // handle case: object
     output = {};
     for (i in input) {
-        if ((input).hasOwnProperty(i)) {
+        if (input.hasOwnProperty(i)) {
             output[i] = simpleDeepClone((input as any)[i]);
         }
     }

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

@@ -1,25 +1,33 @@
+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 { 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 { TestAdminClient } from './test-client';
-import { TestServer } from './test-server';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 
 describe('Administrator resolver', () => {
-    const client = new TestAdminClient();
-    const server = new TestServer();
+    const { server, adminClient } = createTestEnvironment(testConfig);
     let createdAdmin: Administrator.Fragment;
 
     beforeAll(async () => {
         const token = await server.init({
+            dataDir,
+            initialData,
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
             customerCount: 1,
         });
-        await client.init();
+        await adminClient.init();
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
 
     afterAll(async () => {
@@ -27,7 +35,7 @@ describe('Administrator resolver', () => {
     });
 
     it('administrators', async () => {
-        const result = await client.query<GetAdministrators.Query, GetAdministrators.Variables>(
+        const result = await adminClient.query<GetAdministrators.Query, GetAdministrators.Variables>(
             GET_ADMINISTRATORS,
         );
         expect(result.administrators.items.length).toBe(1);
@@ -35,7 +43,7 @@ describe('Administrator resolver', () => {
     });
 
     it('createAdministrator', async () => {
-        const result = await client.query<CreateAdministrator.Mutation, CreateAdministrator.Variables>(
+        const result = await adminClient.query<CreateAdministrator.Mutation, CreateAdministrator.Variables>(
             CREATE_ADMINISTRATOR,
             {
                 input: {
@@ -53,7 +61,7 @@ describe('Administrator resolver', () => {
     });
 
     it('administrator', async () => {
-        const result = await client.query<GetAdministrator.Query, GetAdministrator.Variables>(
+        const result = await adminClient.query<GetAdministrator.Query, GetAdministrator.Variables>(
             GET_ADMINISTRATOR,
             {
                 id: createdAdmin.id,
@@ -63,7 +71,7 @@ describe('Administrator resolver', () => {
     });
 
     it('updateAdministrator', async () => {
-        const result = await client.query<UpdateAdministrator.Mutation, UpdateAdministrator.Variables>(
+        const result = await adminClient.query<UpdateAdministrator.Mutation, UpdateAdministrator.Variables>(
             UPDATE_ADMINISTRATOR,
             {
                 input: {
@@ -80,7 +88,7 @@ describe('Administrator resolver', () => {
     });
 
     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,
             {
                 input: {
@@ -98,7 +106,7 @@ describe('Administrator resolver', () => {
         'updateAdministrator throws with invalid roleId',
         assertThrowsWithMessage(
             () =>
-                client.query<UpdateAdministrator.Mutation, UpdateAdministrator.Variables>(
+                adminClient.query<UpdateAdministrator.Mutation, UpdateAdministrator.Variables>(
                     UPDATE_ADMINISTRATOR,
                     {
                         input: {

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

@@ -1,10 +1,12 @@
 /* tslint:disable:no-non-null-assertion */
 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 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 { CURRENT_USER_FRAGMENT } from './graphql/fragments';
 import {
     CreateAdministrator,
@@ -23,20 +25,20 @@ import {
     GET_PRODUCT_LIST,
     UPDATE_PRODUCT,
 } from './graphql/shared-definitions';
-import { TestAdminClient } from './test-client';
-import { TestServer } from './test-server';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 
 describe('Authorization & permissions', () => {
-    const client = new TestAdminClient();
-    const server = new TestServer();
+    const { server, adminClient } = createTestEnvironment(testConfig);
 
     beforeAll(async () => {
         const token = await server.init({
+            dataDir,
+            initialData,
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
             customerCount: 1,
         });
-        await client.init();
+        await adminClient.init();
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
 
     afterAll(async () => {
@@ -46,13 +48,13 @@ describe('Authorization & permissions', () => {
     describe('admin permissions', () => {
         describe('Anonymous user', () => {
             beforeAll(async () => {
-                await client.asAnonymousUser();
+                await adminClient.asAnonymousUser();
             });
 
             it(
                 'me is not permitted',
                 assertThrowsWithMessage(async () => {
-                    await client.query<Me.Query>(ME);
+                    await adminClient.query<Me.Query>(ME);
                 }, 'You are not currently authorized to perform this action'),
             );
 
@@ -67,15 +69,15 @@ describe('Authorization & permissions', () => {
 
         describe('ReadCatalog permission', () => {
             beforeAll(async () => {
-                await client.asSuperAdmin();
+                await adminClient.asSuperAdmin();
                 const { identifier, password } = await createAdministratorWithPermissions('ReadCatalog', [
                     Permission.ReadCatalog,
                 ]);
-                await client.asUserWithCredentials(identifier, password);
+                await adminClient.asUserWithCredentials(identifier, password);
             });
 
             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([
                     Permission.Authenticated,
@@ -107,18 +109,18 @@ describe('Authorization & permissions', () => {
 
         describe('CRUD on Customers permissions', () => {
             beforeAll(async () => {
-                await client.asSuperAdmin();
+                await adminClient.asSuperAdmin();
                 const { identifier, password } = await createAdministratorWithPermissions('CRUDCustomer', [
                     Permission.CreateCustomer,
                     Permission.ReadCustomer,
                     Permission.UpdateCustomer,
                     Permission.DeleteCustomer,
                 ]);
-                await client.asUserWithCredentials(identifier, password);
+                await adminClient.asUserWithCredentials(identifier, password);
             });
 
             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([
                     Permission.Authenticated,
@@ -156,7 +158,7 @@ describe('Authorization & permissions', () => {
 
     async function assertRequestAllowed<V>(operation: DocumentNode, variables?: V) {
         try {
-            const status = await client.queryStatus(operation, variables);
+            const status = await adminClient.queryStatus(operation, variables);
             expect(status).toBe(200);
         } catch (e) {
             const errorCode = getErrorCode(e);
@@ -170,7 +172,7 @@ describe('Authorization & permissions', () => {
 
     async function assertRequestForbidden<V>(operation: DocumentNode, variables: V) {
         try {
-            const status = await client.query(operation, variables);
+            const status = await adminClient.query(operation, variables);
             fail(`Should have thrown`);
         } catch (e) {
             expect(getErrorCode(e)).toBe('FORBIDDEN');
@@ -185,7 +187,7 @@ describe('Authorization & permissions', () => {
         code: string,
         permissions: Permission[],
     ): 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: {
                 code,
                 description: '',
@@ -200,18 +202,18 @@ describe('Authorization & permissions', () => {
             .substr(2, 8)}`;
         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;
 
         return {

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

@@ -1,15 +1,17 @@
 /* tslint:disable:no-non-null-assertion */
 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 path from 'path';
 
 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,
@@ -45,14 +47,12 @@ import {
     UPDATE_PRODUCT,
     UPDATE_PRODUCT_VARIANTS,
 } from './graphql/shared-definitions';
-import { TestAdminClient } from './test-client';
-import { TestServer } from './test-server';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 import { awaitRunningJobs } from './utils/await-running-jobs';
 
 describe('Collection resolver', () => {
-    const client = new TestAdminClient();
-    const server = new TestServer();
+    const { server, adminClient } = createTestEnvironment(testConfig);
+
     let assets: GetAssetList.Items[];
     let facetValues: FacetValueFragment[];
     let electronicsCollection: Collection.Fragment;
@@ -61,19 +61,25 @@ describe('Collection resolver', () => {
 
     beforeAll(async () => {
         const token = await server.init({
+            dataDir,
+            initialData,
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-collections.csv'),
             customerCount: 1,
         });
-        await client.init();
-        const assetsResult = await client.query<GetAssetList.Query, GetAssetList.Variables>(GET_ASSET_LIST, {
-            options: {
-                sort: {
-                    name: SortOrder.ASC,
+        await adminClient.init();
+        await adminClient.asSuperAdmin();
+        const assetsResult = await adminClient.query<GetAssetList.Query, GetAssetList.Variables>(
+            GET_ASSET_LIST,
+            {
+                options: {
+                    sort: {
+                        name: SortOrder.ASC,
+                    },
                 },
             },
-        });
+        );
         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(
             (values, facet) => [...values, ...facet.values],
             [] as FacetValueFragment[],
@@ -88,7 +94,7 @@ describe('Collection resolver', () => {
      * Test case for https://github.com/vendure-ecommerce/vendure/issues/97
      */
     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',
         });
         expect(result.collection!.breadcrumbs[0].name).toBe(ROOT_COLLECTION_NAME);
@@ -96,7 +102,7 @@ describe('Collection resolver', () => {
 
     describe('createCollection', () => {
         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,
                 {
                     input: {
@@ -132,7 +138,7 @@ describe('Collection resolver', () => {
         });
 
         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,
                 {
                     input: {
@@ -163,7 +169,7 @@ describe('Collection resolver', () => {
         });
 
         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,
                 {
                     input: {
@@ -196,7 +202,7 @@ describe('Collection resolver', () => {
 
     describe('updateCollection', () => {
         it('updates with assets', async () => {
-            const { updateCollection } = await client.query<
+            const { updateCollection } = await adminClient.query<
                 UpdateCollection.Mutation,
                 UpdateCollection.Variables
             >(UPDATE_COLLECTION, {
@@ -212,7 +218,7 @@ describe('Collection resolver', () => {
         });
 
         it('updating existing assets', async () => {
-            const { updateCollection } = await client.query<
+            const { updateCollection } = await adminClient.query<
                 UpdateCollection.Mutation,
                 UpdateCollection.Variables
             >(UPDATE_COLLECTION, {
@@ -227,7 +233,7 @@ describe('Collection resolver', () => {
         });
 
         it('removes all assets', async () => {
-            const { updateCollection } = await client.query<
+            const { updateCollection } = await adminClient.query<
                 UpdateCollection.Mutation,
                 UpdateCollection.Variables
             >(UPDATE_COLLECTION, {
@@ -243,7 +249,7 @@ describe('Collection resolver', () => {
     });
 
     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,
         });
         if (!result.collection) {
@@ -254,7 +260,7 @@ describe('Collection resolver', () => {
     });
 
     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,
         });
         if (!result.collection) {
@@ -265,7 +271,7 @@ describe('Collection resolver', () => {
     });
 
     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,
         });
         if (!result.collection) {
@@ -277,12 +283,12 @@ describe('Collection resolver', () => {
     });
 
     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) {
             fail(`did not return the collection`);
             return;
@@ -296,12 +302,12 @@ describe('Collection resolver', () => {
     });
 
     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) {
             fail(`did not return the collection`);
             return;
@@ -310,7 +316,7 @@ describe('Collection resolver', () => {
     });
 
     it('collections.assets', async () => {
-        const { collections } = await client.query<GetCollectionsWithAssets.Query>(gql`
+        const { collections } = await adminClient.query<GetCollectionsWithAssets.Query>(gql`
             query GetCollectionsWithAssets {
                 collections {
                     items {
@@ -327,7 +333,7 @@ describe('Collection resolver', () => {
 
     describe('moveCollection', () => {
         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,
                 {
                     input: {
@@ -345,10 +351,10 @@ describe('Collection resolver', () => {
         });
 
         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([
                 'Laptop 13 inch 8GB',
                 'Laptop 15 inch 8GB',
@@ -359,7 +365,7 @@ describe('Collection resolver', () => {
         });
 
         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: {
                     collectionId: computersCollection.id,
                     parentId: electronicsCollection.id,
@@ -372,7 +378,7 @@ describe('Collection resolver', () => {
         });
 
         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: {
                     collectionId: pearCollection.id,
                     parentId: electronicsCollection.id,
@@ -385,7 +391,7 @@ describe('Collection resolver', () => {
         });
 
         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: {
                     collectionId: pearCollection.id,
                     parentId: electronicsCollection.id,
@@ -398,7 +404,7 @@ describe('Collection resolver', () => {
         });
 
         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: {
                     collectionId: pearCollection.id,
                     parentId: electronicsCollection.id,
@@ -414,7 +420,7 @@ describe('Collection resolver', () => {
             'throws if attempting to move into self',
             assertThrowsWithMessage(
                 () =>
-                    client.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
+                    adminClient.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
                         input: {
                             collectionId: pearCollection.id,
                             parentId: pearCollection.id,
@@ -429,7 +435,7 @@ describe('Collection resolver', () => {
             'throws if attempting to move into a decendant of self',
             assertThrowsWithMessage(
                 () =>
-                    client.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
+                    adminClient.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
                         input: {
                             collectionId: pearCollection.id,
                             parentId: pearCollection.id,
@@ -441,7 +447,7 @@ describe('Collection resolver', () => {
         );
 
         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);
         }
     });
@@ -452,7 +458,7 @@ describe('Collection resolver', () => {
         let laptopProductId: string;
 
         beforeAll(async () => {
-            const result1 = await client.query<CreateCollection.Mutation, CreateCollection.Variables>(
+            const result1 = await adminClient.query<CreateCollection.Mutation, CreateCollection.Variables>(
                 CREATE_COLLECTION,
                 {
                     input: {
@@ -481,7 +487,7 @@ describe('Collection resolver', () => {
             );
             collectionToDeleteParent = result1.createCollection;
 
-            const result2 = await client.query<CreateCollection.Mutation, CreateCollection.Variables>(
+            const result2 = await adminClient.query<CreateCollection.Mutation, CreateCollection.Variables>(
                 CREATE_COLLECTION,
                 {
                     input: {
@@ -499,14 +505,17 @@ describe('Collection resolver', () => {
         it(
             'throws for invalid collection id',
             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"),
         );
 
         it('collection and product related prior to deletion', async () => {
-            const { collection } = await client.query<
+            const { collection } = await adminClient.query<
                 GetCollectionProducts.Query,
                 GetCollectionProducts.Variables
             >(GET_COLLECTION_PRODUCT_VARIANTS, {
@@ -521,7 +530,7 @@ describe('Collection resolver', () => {
 
             laptopProductId = collection!.productVariants.items[0].productId;
 
-            const { product } = await client.query<
+            const { product } = await adminClient.query<
                 GetProductCollections.Query,
                 GetProductCollections.Variables
             >(GET_PRODUCT_COLLECTIONS, {
@@ -538,7 +547,7 @@ describe('Collection resolver', () => {
         });
 
         it('deleteCollection works', async () => {
-            const { deleteCollection } = await client.query<
+            const { deleteCollection } = await adminClient.query<
                 DeleteCollection.Mutation,
                 DeleteCollection.Variables
             >(DELETE_COLLECTION, {
@@ -549,7 +558,7 @@ describe('Collection resolver', () => {
         });
 
         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,
                 {
                     id: collectionToDeleteParent.id,
@@ -559,7 +568,7 @@ describe('Collection resolver', () => {
         });
 
         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,
                 {
                     id: collectionToDeleteChild.id,
@@ -569,7 +578,7 @@ describe('Collection resolver', () => {
         });
 
         it('product no longer lists collection', async () => {
-            const { product } = await client.query<
+            const { product } = await adminClient.query<
                 GetProductCollections.Query,
                 GetProductCollections.Variables
             >(GET_PRODUCT_COLLECTIONS, {
@@ -586,7 +595,7 @@ describe('Collection resolver', () => {
 
     describe('filters', () => {
         it('Collection with no filters has no productVariants', async () => {
-            const result = await client.query<
+            const result = await adminClient.query<
                 CreateCollectionSelectVariants.Mutation,
                 CreateCollectionSelectVariants.Variables
             >(CREATE_COLLECTION_SELECT_VARIANTS, {
@@ -600,7 +609,7 @@ describe('Collection resolver', () => {
 
         describe('facetValue filter', () => {
             it('electronics', async () => {
-                const result = await client.query<
+                const result = await adminClient.query<
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Variables
                 >(GET_COLLECTION_PRODUCT_VARIANTS, {
@@ -632,7 +641,7 @@ describe('Collection resolver', () => {
             });
 
             it('computers', async () => {
-                const result = await client.query<
+                const result = await adminClient.query<
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Variables
                 >(GET_COLLECTION_PRODUCT_VARIANTS, {
@@ -660,7 +669,7 @@ describe('Collection resolver', () => {
             });
 
             it('photo AND pear', async () => {
-                const result = await client.query<
+                const result = await adminClient.query<
                     CreateCollectionSelectVariants.Mutation,
                     CreateCollectionSelectVariants.Variables
                 >(CREATE_COLLECTION_SELECT_VARIANTS, {
@@ -690,8 +699,8 @@ describe('Collection resolver', () => {
                     } 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,
                     {
                         id: result.createCollection.id,
@@ -702,7 +711,7 @@ describe('Collection resolver', () => {
             });
 
             it('photo OR pear', async () => {
-                const result = await client.query<
+                const result = await adminClient.query<
                     CreateCollectionSelectVariants.Mutation,
                     CreateCollectionSelectVariants.Variables
                 >(CREATE_COLLECTION_SELECT_VARIANTS, {
@@ -732,8 +741,8 @@ describe('Collection resolver', () => {
                     } 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,
                     {
                         id: result.createCollection.id,
@@ -754,7 +763,7 @@ describe('Collection resolver', () => {
             });
 
             it('bell OR pear in computers', async () => {
-                const result = await client.query<
+                const result = await adminClient.query<
                     CreateCollectionSelectVariants.Mutation,
                     CreateCollectionSelectVariants.Variables
                 >(CREATE_COLLECTION_SELECT_VARIANTS, {
@@ -787,8 +796,8 @@ describe('Collection resolver', () => {
                     } 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,
                     {
                         id: result.createCollection.id,
@@ -811,7 +820,7 @@ describe('Collection resolver', () => {
                 operator: string,
                 term: string,
             ): Promise<Collection.Fragment> {
-                const { createCollection } = await client.query<
+                const { createCollection } = await adminClient.query<
                     CreateCollection.Mutation,
                     CreateCollection.Variables
                 >(CREATE_COLLECTION, {
@@ -844,7 +853,7 @@ describe('Collection resolver', () => {
             it('contains operator', async () => {
                 const collection = await createVariantNameFilteredCollection('contains', 'camera');
 
-                const result = await client.query<
+                const result = await adminClient.query<
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Variables
                 >(GET_COLLECTION_PRODUCT_VARIANTS, {
@@ -860,7 +869,7 @@ describe('Collection resolver', () => {
             it('startsWith operator', async () => {
                 const collection = await createVariantNameFilteredCollection('startsWith', 'camera');
 
-                const result = await client.query<
+                const result = await adminClient.query<
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Variables
                 >(GET_COLLECTION_PRODUCT_VARIANTS, {
@@ -872,7 +881,7 @@ describe('Collection resolver', () => {
             it('endsWith operator', async () => {
                 const collection = await createVariantNameFilteredCollection('endsWith', 'camera');
 
-                const result = await client.query<
+                const result = await adminClient.query<
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Variables
                 >(GET_COLLECTION_PRODUCT_VARIANTS, {
@@ -887,7 +896,7 @@ describe('Collection resolver', () => {
             it('doesNotContain operator', async () => {
                 const collection = await createVariantNameFilteredCollection('doesNotContain', 'camera');
 
-                const result = await client.query<
+                const result = await adminClient.query<
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Variables
                 >(GET_COLLECTION_PRODUCT_VARIANTS, {
@@ -921,7 +930,7 @@ describe('Collection resolver', () => {
             let products: GetProductsWithVariantIds.Items[];
 
             beforeAll(async () => {
-                const result = await client.query<GetProductsWithVariantIds.Query>(gql`
+                const result = await adminClient.query<GetProductsWithVariantIds.Query>(gql`
                     query GetProductsWithVariantIds {
                         products {
                             items {
@@ -938,7 +947,7 @@ describe('Collection resolver', () => {
             });
 
             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: {
                         id: products[1].id,
                         facetValueIds: [
@@ -949,9 +958,9 @@ describe('Collection resolver', () => {
                     },
                 });
 
-                await awaitRunningJobs(client);
+                await awaitRunningJobs(adminClient);
 
-                const result = await client.query<
+                const result = await adminClient.query<
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Variables
                 >(GET_COLLECTION_PRODUCT_VARIANTS, { id: pearCollection.id });
@@ -968,7 +977,7 @@ describe('Collection resolver', () => {
 
             it('updates contents when ProductVariant is updated', async () => {
                 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,
                     {
                         input: [
@@ -980,9 +989,9 @@ describe('Collection resolver', () => {
                     },
                 );
 
-                await awaitRunningJobs(client);
+                await awaitRunningJobs(adminClient);
 
-                const result = await client.query<
+                const result = await adminClient.query<
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Variables
                 >(GET_COLLECTION_PRODUCT_VARIANTS, { id: pearCollection.id });
@@ -1000,7 +1009,7 @@ describe('Collection resolver', () => {
 
             it('correctly filters when ProductVariant and Product both have matching FacetValue', async () => {
                 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,
                     {
                         input: [
@@ -1011,7 +1020,7 @@ describe('Collection resolver', () => {
                         ],
                     },
                 );
-                const result = await client.query<
+                const result = await adminClient.query<
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Variables
                 >(GET_COLLECTION_PRODUCT_VARIANTS, { id: pearCollection.id });
@@ -1029,7 +1038,7 @@ describe('Collection resolver', () => {
         });
 
         it('filter inheritance of nested collections (issue #158)', async () => {
-            const { createCollection: pearElectronics } = await client.query<
+            const { createCollection: pearElectronics } = await adminClient.query<
                 CreateCollectionSelectVariants.Mutation,
                 CreateCollectionSelectVariants.Variables
             >(CREATE_COLLECTION_SELECT_VARIANTS, {
@@ -1058,12 +1067,12 @@ describe('Collection resolver', () => {
                 } 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([
                 'Laptop 13 inch 8GB',
                 'Laptop 15 inch 8GB',
@@ -1080,7 +1089,7 @@ describe('Collection resolver', () => {
 
     describe('Product collections property', () => {
         it('returns all collections to which the Product belongs', async () => {
-            const result = await client.query<
+            const result = await adminClient.query<
                 GetCollectionsForProducts.Query,
                 GetCollectionsForProducts.Variables
             >(GET_COLLECTIONS_FOR_PRODUCTS, { term: 'camera' });
@@ -1097,10 +1106,10 @@ describe('Collection resolver', () => {
     });
 
     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
         });
-        const { collection } = await client.query<
+        const { collection } = await adminClient.query<
             GetCollectionProducts.Query,
             GetCollectionProducts.Variables
         >(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 { 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
  * 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);
 }
 
-/**
- * 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: {
         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,
-        },
-    },
-};
+});

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

@@ -1,7 +1,9 @@
+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 { COUNTRY_FRAGMENT } from './graphql/fragments';
 import {
     CreateCountry,
@@ -13,24 +15,24 @@ import {
     UpdateCountry,
 } from './graphql/generated-e2e-admin-types';
 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
 
 describe('Facet resolver', () => {
-    const client = new TestAdminClient();
-    const server = new TestServer();
+    const { server, adminClient } = createTestEnvironment(testConfig);
     let countries: GetCountryList.Items[];
     let GB: GetCountryList.Items;
     let AT: GetCountryList.Items;
 
     beforeAll(async () => {
         await server.init({
+            dataDir,
+            initialData,
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
             customerCount: 1,
         });
-        await client.init();
+        await adminClient.init();
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
 
     afterAll(async () => {
@@ -38,7 +40,7 @@ describe('Facet resolver', () => {
     });
 
     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);
         countries = result.countries.items;
@@ -47,7 +49,7 @@ describe('Facet resolver', () => {
     });
 
     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,
         });
 
@@ -55,50 +57,62 @@ describe('Facet resolver', () => {
     });
 
     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);
     });
 
     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');
     });
 
     describe('deletion', () => {
         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({
                 result: DeletionResult.DELETED,
                 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();
         });
 
         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({
                 result: DeletionResult.NOT_DELETED,
                 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();
         });
     });

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

@@ -1,109 +1,112 @@
 // Force the timezone to avoid tests failing in other locales
 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 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';
 
 // 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', () => {
-    const adminClient = new TestAdminClient();
-    const shopClient = new TestShopClient();
-    const server = new TestServer();
+    const { server, adminClient, shopClient } = createTestEnvironment(customConfig);
 
     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 server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
+            customerCount: 1,
+        });
         await adminClient.init();
+        await adminClient.asSuperAdmin();
         await shopClient.init();
     }, TEST_SETUP_TIMEOUT_MS);
 

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

@@ -1,14 +1,18 @@
 import { OnModuleInit } from '@nestjs/common';
 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 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 {
     CreateAddress,
@@ -25,32 +29,45 @@ import {
 import { AddItemToOrder } from './graphql/generated-e2e-shop-types';
 import { GET_CUSTOMER, GET_CUSTOMER_LIST } from './graphql/shared-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';
 
 // tslint:disable:no-non-null-assertion
 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', () => {
-    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 secondCustomer: GetCustomerList.Items;
     let thirdCustomer: GetCustomerList.Items;
 
     beforeAll(async () => {
-        const token = await server.init(
-            {
-                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
-                customerCount: 5,
-            },
-            {
-                plugins: [TestEmailPlugin],
-            },
-        );
+        const token = await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
+            customerCount: 5,
+        });
         await adminClient.init();
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
 
     afterAll(async () => {
@@ -486,19 +503,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);
-        });
-    }
-}

+ 23 - 26
packages/core/e2e/default-search-plugin.e2e-spec.ts

@@ -1,12 +1,13 @@
 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, TestClient } from '@vendure/testing';
 import gql from 'graphql-tag';
 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 {
     CreateCollection,
     CreateFacet,
@@ -29,26 +30,22 @@ import {
     UPDATE_TAX_RATE,
 } from './graphql/shared-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';
 
 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 () => {
-        const token = await server.init(
-            {
-                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
-                customerCount: 1,
-            },
-            {
-                plugins: [DefaultSearchPlugin],
-            },
-        );
+        const token = await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
+            customerCount: 1,
+        });
         await adminClient.init();
+        await adminClient.asSuperAdmin();
         await shopClient.init();
     }, TEST_SETUP_TIMEOUT_MS);
 
@@ -56,7 +53,7 @@ describe('Default search plugin', () => {
         await server.destroy();
     });
 
-    async function testGroupByProduct(client: SimpleGraphQLClient) {
+    async function testGroupByProduct(client: TestClient) {
         const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
             SEARCH_PRODUCTS_SHOP,
             {
@@ -68,7 +65,7 @@ describe('Default search plugin', () => {
         expect(result.search.totalItems).toBe(20);
     }
 
-    async function testNoGrouping(client: SimpleGraphQLClient) {
+    async function testNoGrouping(client: TestClient) {
         const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
             SEARCH_PRODUCTS_SHOP,
             {
@@ -80,7 +77,7 @@ describe('Default search plugin', () => {
         expect(result.search.totalItems).toBe(34);
     }
 
-    async function testMatchSearchTerm(client: SimpleGraphQLClient) {
+    async function testMatchSearchTerm(client: TestClient) {
         const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
             SEARCH_PRODUCTS_SHOP,
             {
@@ -97,7 +94,7 @@ describe('Default search plugin', () => {
         ]);
     }
 
-    async function testMatchFacetIds(client: SimpleGraphQLClient) {
+    async function testMatchFacetIds(client: TestClient) {
         const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
             SEARCH_PRODUCTS_SHOP,
             {
@@ -117,7 +114,7 @@ describe('Default search plugin', () => {
         ]);
     }
 
-    async function testMatchCollectionId(client: SimpleGraphQLClient) {
+    async function testMatchCollectionId(client: TestClient) {
         const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
             SEARCH_PRODUCTS_SHOP,
             {
@@ -134,7 +131,7 @@ describe('Default search plugin', () => {
         ]);
     }
 
-    async function testSinglePrices(client: SimpleGraphQLClient) {
+    async function testSinglePrices(client: TestClient) {
         const result = await client.query<SearchGetPrices.Query, SearchGetPrices.Variables>(
             SEARCH_GET_PRICES,
             {
@@ -160,7 +157,7 @@ describe('Default search plugin', () => {
         ]);
     }
 
-    async function testPriceRanges(client: SimpleGraphQLClient) {
+    async function testPriceRanges(client: TestClient) {
         const result = await client.query<SearchGetPrices.Query, SearchGetPrices.Variables>(
             SEARCH_GET_PRICES,
             {

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

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

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

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

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

@@ -1,7 +1,9 @@
+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 { FACET_VALUE_FRAGMENT, FACET_WITH_VALUES_FRAGMENT } from './graphql/fragments';
 import {
     CreateFacet,
@@ -28,23 +30,24 @@ import {
     UPDATE_PRODUCT,
     UPDATE_PRODUCT_VARIANTS,
 } from './graphql/shared-definitions';
-import { TestAdminClient } from './test-client';
-import { TestServer } from './test-server';
 
 // tslint:disable:no-non-null-assertion
 
 describe('Facet resolver', () => {
-    const client = new TestAdminClient();
-    const server = new TestServer();
+    const { server, adminClient } = createTestEnvironment(testConfig);
+
     let brandFacet: FacetWithValues.Fragment;
     let speakerTypeFacet: FacetWithValues.Fragment;
 
     beforeAll(async () => {
         await server.init({
+            dataDir,
+            initialData,
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
             customerCount: 1,
         });
-        await client.init();
+        await adminClient.init();
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
 
     afterAll(async () => {
@@ -52,7 +55,7 @@ describe('Facet resolver', () => {
     });
 
     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: {
                 isPrivate: false,
                 code: 'speaker-type',
@@ -71,7 +74,7 @@ describe('Facet resolver', () => {
     });
 
     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: {
                 id: speakerTypeFacet.id,
                 translations: [{ languageCode: LanguageCode.en, name: 'Speaker Category' }],
@@ -82,7 +85,7 @@ describe('Facet resolver', () => {
     });
 
     it('createFacetValues', async () => {
-        const result = await client.query<CreateFacetValues.Mutation, CreateFacetValues.Variables>(
+        const result = await adminClient.query<CreateFacetValues.Mutation, CreateFacetValues.Variables>(
             CREATE_FACET_VALUES,
             {
                 input: [
@@ -104,7 +107,7 @@ describe('Facet resolver', () => {
     });
 
     it('updateFacetValues', async () => {
-        const result = await client.query<UpdateFacetValues.Mutation, UpdateFacetValues.Variables>(
+        const result = await adminClient.query<UpdateFacetValues.Mutation, UpdateFacetValues.Variables>(
             UPDATE_FACET_VALUES,
             {
                 input: [
@@ -120,7 +123,7 @@ describe('Facet resolver', () => {
     });
 
     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;
         expect(items.length).toBe(2);
@@ -132,7 +135,7 @@ describe('Facet resolver', () => {
     });
 
     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,
             {
                 id: speakerTypeFacet.id,
@@ -147,19 +150,19 @@ describe('Facet resolver', () => {
 
         beforeAll(async () => {
             // 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,
             );
             products = result1.products.items;
 
-            await client.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
+            await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
                 input: {
                     id: products[0].id,
                     facetValueIds: [speakerTypeFacet.values[0].id],
                 },
             });
 
-            await client.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
+            await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
                 UPDATE_PRODUCT_VARIANTS,
                 {
                     input: [
@@ -171,7 +174,7 @@ describe('Facet resolver', () => {
                 },
             );
 
-            await client.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
+            await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
                 input: {
                     id: products[1].id,
                     facetValueIds: [speakerTypeFacet.values[1].id],
@@ -181,14 +184,14 @@ describe('Facet resolver', () => {
 
         it('deleteFacetValues deletes unused facetValue', async () => {
             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,
                 {
                     ids: [facetValueToDelete.id],
                     force: false,
                 },
             );
-            const result2 = await client.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
+            const result2 = await adminClient.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
                 GET_FACET_WITH_VALUES,
                 {
                     id: speakerTypeFacet.id,
@@ -207,14 +210,14 @@ describe('Facet resolver', () => {
 
         it('deleteFacetValues for FacetValue in use returns NOT_DELETED', async () => {
             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,
                 {
                     ids: [facetValueToDelete.id],
                     force: false,
                 },
             );
-            const result2 = await client.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
+            const result2 = await adminClient.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
                 GET_FACET_WITH_VALUES,
                 {
                     id: speakerTypeFacet.id,
@@ -233,7 +236,7 @@ describe('Facet resolver', () => {
 
         it('deleteFacetValues for FacetValue in use can be force deleted', async () => {
             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,
                 {
                     ids: [facetValueToDelete.id],
@@ -249,7 +252,7 @@ describe('Facet resolver', () => {
             ]);
 
             // 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,
                 {
                     id: speakerTypeFacet.id,
@@ -258,7 +261,7 @@ describe('Facet resolver', () => {
             expect(result2.facet!.values[0]).not.toEqual(facetValueToDelete);
 
             // FacetValue no longer in the Product.facetValues array
-            const result3 = await client.query<
+            const result3 = await adminClient.query<
                 GetProductWithVariants.Query,
                 GetProductWithVariants.Variables
             >(GET_PRODUCT_WITH_VARIANTS, {
@@ -268,11 +271,14 @@ describe('Facet resolver', () => {
         });
 
         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,
                 {
                     id: speakerTypeFacet.id,
@@ -288,10 +294,13 @@ describe('Facet resolver', () => {
         });
 
         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({
                 result: DeletionResult.DELETED,
@@ -299,7 +308,7 @@ describe('Facet resolver', () => {
             });
 
             // 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,
                 {
                     id: speakerTypeFacet.id,
@@ -308,7 +317,7 @@ describe('Facet resolver', () => {
             expect(result2.facet).toBe(null);
 
             // FacetValue no longer in the Product.facetValues array
-            const result3 = await client.query<
+            const result3 = await adminClient.query<
                 GetProductWithVariants.Query,
                 GetProductWithVariants.Variables
             >(GET_PRODUCT_WITH_VARIANTS, {
@@ -318,7 +327,7 @@ describe('Facet resolver', () => {
         });
 
         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,
                 {
                     input: {
@@ -328,10 +337,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);
         });
     });

+ 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,
+    }),
+});

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

@@ -1,28 +1,28 @@
+import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
 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', () => {
-    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 () => {
-        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();
+        const token = await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-empty.csv'),
+            customerCount: 0,
+        });
+        await adminClient.init();
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
 
     afterAll(async () => {
@@ -44,7 +44,19 @@ describe('Import resolver', () => {
         });
 
         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([
             'Invalid Record Length: header length is 16, got 1 on line 8',
@@ -52,7 +64,7 @@ describe('Import resolver', () => {
         expect(result.importProducts.imported).toBe(4);
         expect(result.importProducts.processed).toBe(4);
 
-        const productResult = await client.query(
+        const productResult = await adminClient.query(
             gql`
                 query GetProducts($options: ProductListOptions) {
                     products(options: $options) {

+ 27 - 23
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 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 {
     GetProductWithVariants,
     LanguageCode,
@@ -11,22 +12,22 @@ import {
     UpdateProduct,
 } from './graphql/generated-e2e-admin-types';
 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 */
 describe('Role resolver', () => {
-    const client = new TestAdminClient();
-    const server = new TestServer();
+    const { server, adminClient } = createTestEnvironment(testConfig);
 
     beforeAll(async () => {
         const token = await server.init({
+            dataDir,
+            initialData,
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
             customerCount: 1,
         });
-        await client.init();
+        await adminClient.init();
+        await adminClient.asSuperAdmin();
 
-        const { updateProduct } = await client.query<UpdateProduct.Mutation, UpdateProduct.Variables>(
+        const { updateProduct } = await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(
             UPDATE_PRODUCT,
             {
                 input: {
@@ -55,16 +56,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);
 
     afterAll(async () => {
@@ -72,7 +76,7 @@ describe('Role resolver', () => {
     });
 
     it('returns default language when none specified', async () => {
-        const { product } = await client.query<
+        const { product } = await adminClient.query<
             GetProductWithVariants.Query,
             GetProductWithVariants.Variables
         >(GET_PRODUCT_WITH_VARIANTS, {
@@ -86,7 +90,7 @@ describe('Role resolver', () => {
     });
 
     it('returns specified language', async () => {
-        const { product } = await client.query<
+        const { product } = await adminClient.query<
             GetProductWithVariants.Query,
             GetProductWithVariants.Variables
         >(
@@ -104,7 +108,7 @@ describe('Role resolver', () => {
     });
 
     it('falls back to default language code', async () => {
-        const { product } = await client.query<
+        const { product } = await adminClient.query<
             GetProductWithVariants.Query,
             GetProductWithVariants.Variables
         >(
@@ -122,7 +126,7 @@ describe('Role resolver', () => {
     });
 
     it('nested entites are translated', async () => {
-        const { product } = await client.query<
+        const { product } = await adminClient.query<
             GetProductWithVariants.Query,
             GetProductWithVariants.Variables
         >(
@@ -138,7 +142,7 @@ describe('Role resolver', () => {
     });
 
     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,
             {
                 input: {

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

@@ -1,15 +1,18 @@
 /* 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 {
+    atLeastNWithFacets,
     discountOnItemWithFacets,
+    minimumOrderAmount,
     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 {
     CreatePromotion,
     CreatePromotionInput,
@@ -36,19 +39,16 @@ import {
     REMOVE_COUPON_CODE,
     SET_CUSTOMER,
 } from './graphql/shop-definitions';
-import { TestAdminClient, TestShopClient } from './test-client';
-import { TestServer } from './test-server';
 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', () => {
-    const adminClient = new TestAdminClient();
-    const shopClient = new TestShopClient();
-    const server = new TestServer();
+    const { server, adminClient, shopClient } = createTestEnvironment({
+        ...testConfig,
+        paymentOptions: {
+            paymentMethodHandlers: [testSuccessfulPaymentMethod],
+        },
+    });
 
     const freeOrderAction = {
         code: orderPercentageDiscount.code,
@@ -65,19 +65,15 @@ describe('Promotions applied to Orders', () => {
     let products: GetPromoProducts.Items[];
 
     beforeAll(async () => {
-        const token = await server.init(
-            {
-                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-promotions.csv'),
-                customerCount: 2,
-            },
-            {
-                paymentOptions: {
-                    paymentMethodHandlers: [testSuccessfulPaymentMethod],
-                },
-            },
-        );
+        const token = await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-promotions.csv'),
+            customerCount: 2,
+        });
         await shopClient.init();
         await adminClient.init();
+        await adminClient.asSuperAdmin();
 
         await getProducts();
         await createGlobalPromotions();

+ 31 - 103
packages/core/e2e/order.e2e-spec.ts

@@ -1,13 +1,16 @@
 /* tslint:disable:no-non-null-assertion */
-import { LanguageCode } from '@vendure/common/lib/generated-shop-types';
+import { pick } from '@vendure/common/lib/pick';
+import { createTestEnvironment, TestClient } from '@vendure/testing';
 import gql from 'graphql-tag';
 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 {
     AddNoteToOrder,
@@ -22,10 +25,12 @@ import {
     GetOrderListFulfillments,
     GetProductWithVariants,
     GetStockMovement,
+    HistoryEntryType,
     OrderItemFragment,
     RefundOrder,
     SettlePayment,
     SettleRefund,
+    StockMovementType,
     UpdateProductVariants,
 } from './graphql/generated-e2e-admin-types';
 import { AddItemToOrder, GetActiveOrder } from './graphql/generated-e2e-shop-types';
@@ -36,35 +41,33 @@ import {
     UPDATE_PRODUCT_VARIANTS,
 } from './graphql/shared-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 { addPaymentToOrder, proceedToArrangingPayment } from './utils/test-order-utils';
 
 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[];
     const password = 'test';
 
     beforeAll(async () => {
-        const token = await server.init(
-            {
-                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
-                customerCount: 3,
-            },
-            {
-                paymentOptions: {
-                    paymentMethodHandlers: [
-                        twoStagePaymentMethod,
-                        failsToSettlePaymentMethod,
-                        singleStageRefundablePaymentMethod,
-                    ],
-                },
-            },
-        );
+        const token = await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
+            customerCount: 3,
+        });
         await adminClient.init();
+        await adminClient.asSuperAdmin();
+        await shopClient.init();
 
         // Create a couple of orders to be queried
         const result = await adminClient.query<GetCustomerList.Query, GetCustomerList.Variables>(
@@ -1191,84 +1194,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(
-    adminClient: TestAdminClient,
-    shopClient: TestShopClient,
+    adminClient: TestClient,
+    shopClient: TestClient,
     emailAddress: string,
     password: string,
 ): Promise<{

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

@@ -1,10 +1,11 @@
 import { LanguageCode } from '@vendure/common/lib/generated-types';
+import { ConfigService } from '@vendure/core';
+import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
 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 {
     TestAPIExtensionPlugin,
     TestLazyExtensionPlugin,
@@ -12,41 +13,39 @@ import {
     TestPluginWithConfigAndBootstrap,
     TestPluginWithProvider,
 } from './fixtures/test-plugins';
-import { TestAdminClient, TestShopClient } from './test-client';
-import { TestServer } from './test-server';
 
 describe('Plugins', () => {
-    const adminClient = new TestAdminClient();
-    const shopClient = new TestShopClient();
-    const server = new TestServer();
     const bootstrapMockFn = jest.fn();
     const onBootstrapFn = jest.fn();
     const onWorkerBootstrapFn = jest.fn();
     const onCloseFn = 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 () => {
-        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,
-                ],
-            },
-        );
+        const token = await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
+            customerCount: 1,
+        });
         await adminClient.init();
+        await adminClient.asSuperAdmin();
         await shopClient.init();
     }, TEST_SETUP_TIMEOUT_MS);
 

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

@@ -1,9 +1,11 @@
+import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
 import path from 'path';
 
 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 {
     CreateProductOption,
     CreateProductOptionGroup,
@@ -12,24 +14,24 @@ import {
     UpdateProductOption,
     UpdateProductOptionGroup,
 } from './graphql/generated-e2e-admin-types';
-import { TestAdminClient } from './test-client';
-import { TestServer } from './test-server';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 
 // tslint:disable:no-non-null-assertion
 
 describe('ProductOption resolver', () => {
-    const client = new TestAdminClient();
-    const server = new TestServer();
+    const { server, adminClient } = createTestEnvironment(testConfig);
     let sizeGroup: ProductOptionGroupFragment;
     let mediumOption: CreateProductOption.CreateProductOption;
 
     beforeAll(async () => {
         const token = await server.init({
+            dataDir,
+            initialData,
             customerCount: 1,
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
         });
-        await client.init();
+        await adminClient.init();
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
 
     afterAll(async () => {
@@ -37,7 +39,7 @@ describe('ProductOption resolver', () => {
     });
 
     it('createProductOptionGroup', async () => {
-        const { createProductOptionGroup } = await client.query<
+        const { createProductOptionGroup } = await adminClient.query<
             CreateProductOptionGroup.Mutation,
             CreateProductOptionGroup.Variables
         >(CREATE_PRODUCT_OPTION_GROUP, {
@@ -75,7 +77,7 @@ describe('ProductOption resolver', () => {
     });
 
     it('updateProductOptionGroup', async () => {
-        const { updateProductOptionGroup } = await client.query<
+        const { updateProductOptionGroup } = await adminClient.query<
             UpdateProductOptionGroup.Mutation,
             UpdateProductOptionGroup.Variables
         >(UPDATE_PRODUCT_OPTION_GROUP, {
@@ -93,7 +95,7 @@ describe('ProductOption resolver', () => {
     it(
         'createProductOption throws with invalid productOptionGroupId',
         assertThrowsWithMessage(async () => {
-            const { createProductOption } = await client.query<
+            const { createProductOption } = await adminClient.query<
                 CreateProductOption.Mutation,
                 CreateProductOption.Variables
             >(CREATE_PRODUCT_OPTION, {
@@ -106,11 +108,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 () => {
-        const { createProductOption } = await client.query<
+        const { createProductOption } = await adminClient.query<
             CreateProductOption.Mutation,
             CreateProductOption.Variables
         >(CREATE_PRODUCT_OPTION, {
@@ -134,7 +136,10 @@ describe('ProductOption resolver', () => {
     });
 
     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: {
                 id: 'T_7',
                 translations: [

+ 7 - 6
packages/core/e2e/product.e2e-spec.ts

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

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

@@ -1,12 +1,11 @@
 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 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 {
     CreatePromotion,
@@ -15,25 +14,28 @@ import {
     GetAdjustmentOperations,
     GetPromotion,
     GetPromotionList,
+    LanguageCode,
     Promotion,
     UpdatePromotion,
 } from './graphql/generated-e2e-admin-types';
 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';
 
 // tslint:disable:no-non-null-assertion
 
 describe('Promotion resolver', () => {
-    const client = new TestAdminClient();
-    const server = new TestServer();
-
     const promoCondition = generateTestCondition('promo_condition');
     const promoCondition2 = generateTestCondition('promo_condition2');
-
     const promoAction = generateTestAction('promo_action');
 
+    const { server, adminClient, shopClient } = createTestEnvironment({
+        ...testConfig,
+        promotionOptions: {
+            promotionConditions: [promoCondition, promoCondition2],
+            promotionActions: [promoAction],
+        },
+    });
+
     const snapshotProps: Array<keyof Promotion.Fragment> = [
         'name',
         'actions',
@@ -46,19 +48,14 @@ describe('Promotion resolver', () => {
     let promotion: Promotion.Fragment;
 
     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.init();
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
 
     afterAll(async () => {
@@ -66,7 +63,7 @@ describe('Promotion resolver', () => {
     });
 
     it('createPromotion', async () => {
-        const result = await client.query<CreatePromotion.Mutation, CreatePromotion.Variables>(
+        const result = await adminClient.query<CreatePromotion.Mutation, CreatePromotion.Variables>(
             CREATE_PROMOTION,
             {
                 input: {
@@ -103,7 +100,7 @@ describe('Promotion resolver', () => {
     it(
         'createPromotion throws with empty conditions and no couponCode',
         assertThrowsWithMessage(async () => {
-            await client.query<CreatePromotion.Mutation, CreatePromotion.Variables>(CREATE_PROMOTION, {
+            await adminClient.query<CreatePromotion.Mutation, CreatePromotion.Variables>(CREATE_PROMOTION, {
                 input: {
                     name: 'bad promotion',
                     enabled: true,
@@ -126,7 +123,7 @@ describe('Promotion resolver', () => {
     );
 
     it('updatePromotion', async () => {
-        const result = await client.query<UpdatePromotion.Mutation, UpdatePromotion.Variables>(
+        const result = await adminClient.query<UpdatePromotion.Mutation, UpdatePromotion.Variables>(
             UPDATE_PROMOTION,
             {
                 input: {
@@ -153,7 +150,7 @@ describe('Promotion resolver', () => {
     it(
         'updatePromotion throws with empty conditions and no couponCode',
         assertThrowsWithMessage(async () => {
-            await client.query<UpdatePromotion.Mutation, UpdatePromotion.Variables>(UPDATE_PROMOTION, {
+            await adminClient.query<UpdatePromotion.Mutation, UpdatePromotion.Variables>(UPDATE_PROMOTION, {
                 input: {
                     id: promotion.id,
                     couponCode: '',
@@ -164,7 +161,7 @@ describe('Promotion resolver', () => {
     );
 
     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,
         });
 
@@ -172,7 +169,7 @@ describe('Promotion resolver', () => {
     });
 
     it('promotions', async () => {
-        const result = await client.query<GetPromotionList.Query, GetPromotionList.Variables>(
+        const result = await adminClient.query<GetPromotionList.Query, GetPromotionList.Variables>(
             GET_PROMOTION_LIST,
             {},
         );
@@ -182,9 +179,10 @@ describe('Promotion resolver', () => {
     });
 
     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.promotionConditions).toMatchSnapshot();
@@ -195,13 +193,13 @@ describe('Promotion resolver', () => {
         let promotionToDelete: GetPromotionList.Items;
 
         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;
         });
 
         it('deletes a promotion', async () => {
             promotionToDelete = allPromotions[0];
-            const result = await client.query<DeletePromotion.Mutation, DeletePromotion.Variables>(
+            const result = await adminClient.query<DeletePromotion.Mutation, DeletePromotion.Variables>(
                 DELETE_PROMOTION,
                 { id: promotionToDelete.id },
             );
@@ -210,15 +208,18 @@ describe('Promotion resolver', () => {
         });
 
         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);
         });
 
         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.map(c => c.id).includes(promotionToDelete.id)).toBe(false);
@@ -228,7 +229,7 @@ describe('Promotion resolver', () => {
             'updatePromotion throws for deleted promotion',
             assertThrowsWithMessage(
                 () =>
-                    client.query<UpdatePromotion.Mutation, UpdatePromotion.Variables>(UPDATE_PROMOTION, {
+                    adminClient.query<UpdatePromotion.Mutation, UpdatePromotion.Variables>(UPDATE_PROMOTION, {
                         input: {
                             id: promotionToDelete.id,
                             enabled: false,

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

@@ -1,9 +1,11 @@
 import { omit } from '@vendure/common/lib/omit';
 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 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 {
     CreateRole,
@@ -14,22 +16,22 @@ import {
     UpdateRole,
 } from './graphql/generated-e2e-admin-types';
 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';
 
 describe('Role resolver', () => {
-    const client = new TestAdminClient();
-    const server = new TestServer();
+    const { server, adminClient } = createTestEnvironment(testConfig);
     let createdRole: Role.Fragment;
     let defaultRoles: Role.Fragment[];
 
     beforeAll(async () => {
         const token = await server.init({
+            dataDir,
+            initialData,
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
             customerCount: 1,
         });
-        await client.init();
+        await adminClient.init();
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
 
     afterAll(async () => {
@@ -37,7 +39,7 @@ describe('Role resolver', () => {
     });
 
     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;
         expect(result.roles.items.length).toBe(2);
@@ -47,7 +49,7 @@ describe('Role resolver', () => {
     it(
         'createRole with invalid permission',
         assertThrowsWithMessage(async () => {
-            await client.query<CreateRole.Mutation, CreateRole.Variables>(CREATE_ROLE, {
+            await adminClient.query<CreateRole.Mutation, CreateRole.Variables>(CREATE_ROLE, {
                 input: {
                     code: 'test',
                     description: 'test role',
@@ -58,13 +60,16 @@ describe('Role resolver', () => {
     );
 
     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({
             code: 'test',
@@ -75,13 +80,16 @@ describe('Role resolver', () => {
     });
 
     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({
             code: 'test2',
@@ -92,7 +100,7 @@ describe('Role resolver', () => {
     });
 
     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: {
                 code: 'test',
                 description: 'test role',
@@ -110,12 +118,14 @@ describe('Role resolver', () => {
     });
 
     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);
     });
 
     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: {
                 id: createdRole.id,
                 code: 'test-modified',
@@ -138,7 +148,7 @@ describe('Role resolver', () => {
     });
 
     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: {
                 id: createdRole.id,
                 code: 'test-modified-again',
@@ -156,7 +166,7 @@ describe('Role resolver', () => {
     });
 
     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: {
                 id: createdRole.id,
                 permissions: [
@@ -179,7 +189,7 @@ describe('Role resolver', () => {
                 fail(`Could not find SuperAdmin role`);
                 return;
             }
-            return client.query<UpdateRole.Mutation, UpdateRole.Variables>(UPDATE_ROLE, {
+            return adminClient.query<UpdateRole.Mutation, UpdateRole.Variables>(UPDATE_ROLE, {
                 input: {
                     id: superAdminRole.id,
                     code: 'superadmin-modified',
@@ -198,7 +208,7 @@ describe('Role resolver', () => {
                 fail(`Could not find Customer role`);
                 return;
             }
-            return client.query<UpdateRole.Mutation, UpdateRole.Variables>(UPDATE_ROLE, {
+            return adminClient.query<UpdateRole.Mutation, UpdateRole.Variables>(UPDATE_ROLE, {
                 input: {
                     id: customerRole.id,
                     code: 'customer-modified',

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

@@ -1,13 +1,15 @@
 /* tslint:disable:no-non-null-assertion */
+import {
+    defaultShippingCalculator,
+    defaultShippingEligibilityChecker,
+    ShippingCalculator,
+} from '@vendure/core';
+import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
 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 {
     CreateShippingMethod,
     DeleteShippingMethod,
@@ -16,32 +18,48 @@ import {
     GetEligibilityCheckers,
     GetShippingMethod,
     GetShippingMethodList,
+    LanguageCode,
     TestEligibleMethods,
     TestShippingMethod,
     UpdateShippingMethod,
 } 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', () => {
-    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 () => {
-        const token = await server.init(
-            {
-                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
-                customerCount: 1,
-            },
-            {
-                shippingOptions: {
-                    shippingEligibilityCheckers: [defaultShippingEligibilityChecker],
-                    shippingCalculators: [defaultShippingCalculator, calculatorWithMetadata],
-                },
-            },
-        );
+        const token = await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
+            customerCount: 1,
+        });
         await adminClient.init();
+        await adminClient.asSuperAdmin();
         await shopClient.init();
     }, TEST_SETUP_TIMEOUT_MS);
 
@@ -279,24 +297,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`
     fragment ShippingMethod on ShippingMethod {
         id

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

@@ -2,19 +2,23 @@
 import { OnModuleInit } from '@nestjs/common';
 import { RegisterCustomerInput } from '@vendure/common/lib/generated-shop-types';
 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 gql from 'graphql-tag';
 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 {
     CreateAdministrator,
     CreateRole,
@@ -42,29 +46,52 @@ import {
     UPDATE_EMAIL_ADDRESS,
     VERIFY_EMAIL,
 } from './graphql/shop-definitions';
-import { TestAdminClient, TestShopClient } from './test-client';
-import { TestServer } from './test-server';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 
 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', () => {
-    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 () => {
-        const token = await server.init(
-            {
-                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
-                customerCount: 2,
-            },
-            {
-                plugins: [TestEmailPlugin],
-            },
-        );
+        const token = await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
+            customerCount: 2,
+        });
         await shopClient.init();
         await adminClient.init();
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
 
     afterAll(async () => {
@@ -508,25 +535,25 @@ describe('Shop auth & accounts', () => {
 });
 
 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 () => {
-        const token = await server.init(
-            {
-                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
-                customerCount: 1,
-            },
-            {
-                plugins: [TestEmailPlugin],
-                authOptions: {
-                    verificationTokenDuration: '1ms',
-                },
-            },
-        );
+        await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
+            customerCount: 1,
+        });
         await shopClient.init();
         await adminClient.init();
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
 
     beforeEach(() => {
@@ -598,23 +625,23 @@ describe('Expiring tokens', () => {
 });
 
 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';
 
     beforeAll(async () => {
-        const token = await server.init(
-            {
-                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
-                customerCount: 1,
-            },
-            {
-                plugins: [TestEmailPlugin],
-                authOptions: {
-                    requireVerification: false,
-                },
-            },
-        );
+        const token = await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
+            customerCount: 1,
+        });
         await shopClient.init();
     }, TEST_SETUP_TIMEOUT_MS);
 
@@ -672,35 +699,32 @@ describe('Registration 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;
     const NEW_EMAIL_ADDRESS = 'new@address.com';
 
     beforeAll(async () => {
-        const token = await server.init(
-            {
-                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
-                customerCount: 1,
-            },
-            {
-                plugins: [TestEmailPlugin],
-                authOptions: {
-                    requireVerification: false,
-                },
-            },
-        );
+        await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
+            customerCount: 1,
+        });
         await shopClient.init();
         await adminClient.init();
-    }, TEST_SETUP_TIMEOUT_MS);
-
-    beforeAll(async () => {
+        await adminClient.asSuperAdmin();
         const result = await adminClient.query<GetCustomer.Query, GetCustomer.Variables>(GET_CUSTOMER, {
             id: 'T_1',
         });
         customer = result.customer!;
-    });
+    }, TEST_SETUP_TIMEOUT_MS);
 
     beforeEach(() => {
         sendEmailFn = jest.fn();
@@ -729,31 +753,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> {
     return new Promise<any>(resolve => {
         sendEmailFn.mockImplementation((event: AccountRegistrationEvent) => {

+ 8 - 8
packages/core/e2e/shop-catalog.e2e-spec.ts

@@ -1,10 +1,11 @@
 /* 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 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 {
     CreateCollection,
     CreateFacet,
@@ -36,22 +37,21 @@ import {
     UPDATE_PRODUCT,
     UPDATE_PRODUCT_VARIANTS,
 } from './graphql/shared-definitions';
-import { TestAdminClient, TestShopClient } from './test-client';
-import { TestServer } from './test-server';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 
 describe('Shop catalog', () => {
-    const shopClient = new TestShopClient();
-    const adminClient = new TestAdminClient();
-    const server = new TestServer();
+    const { server, adminClient, shopClient } = createTestEnvironment(testConfig);
 
     beforeAll(async () => {
         const token = await server.init({
+            dataDir,
+            initialData,
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
             customerCount: 1,
         });
         await shopClient.init();
         await adminClient.init();
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
 
     afterAll(async () => {

+ 7 - 6
packages/core/e2e/shop-customer.e2e-spec.ts

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

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

@@ -1,15 +1,21 @@
 /* tslint:disable:no-non-null-assertion */
+import { mergeConfig } from '@vendure/core';
 import path from 'path';
 
-import { PaymentMethodHandler } from '../src/config/payment-method/payment-method-handler';
+import { createTestEnvironment } from '../../testing/lib/create-test-environment';
 
-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 {
     CreateAddressInput,
     GetCountryList,
     GetCustomer,
     GetCustomerList,
-    LanguageCode,
     UpdateCountry,
 } from './graphql/generated-e2e-admin-types';
 import {
@@ -52,37 +58,34 @@ import {
     SET_SHIPPING_METHOD,
     TRANSITION_TO_STATE,
 } from './graphql/shop-definitions';
-import { TestAdminClient, TestShopClient } from './test-client';
-import { TestServer } from './test-server';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
-import { testSuccessfulPaymentMethod } from './utils/test-order-utils';
 
 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,
             },
-        );
+        }),
+    );
+
+    beforeAll(async () => {
+        const token = await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
+            customerCount: 2,
+        });
         await shopClient.init();
         await adminClient.init();
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
 
     afterAll(async () => {
@@ -867,36 +870,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 - 39
packages/core/e2e/stock-control.e2e-spec.ts

@@ -1,16 +1,16 @@
 /* tslint:disable:no-non-null-assertion */
+import { mergeConfig, OrderState } from '@vendure/core';
+import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
 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 {
     CreateAddressInput,
     GetStockMovement,
-    LanguageCode,
     StockMovementType,
     UpdateProductVariantInput,
     UpdateStock,
@@ -30,29 +30,27 @@ import {
     SET_SHIPPING_ADDRESS,
     TRANSITION_TO_STATE,
 } from './graphql/shop-definitions';
-import { TestAdminClient, TestShopClient } from './test-client';
-import { TestServer } from './test-server';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 
 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 () => {
-        const token = await server.init(
-            {
-                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-stock-control.csv'),
-                customerCount: 2,
-            },
-            {
-                paymentOptions: {
-                    paymentMethodHandlers: [testPaymentMethod],
-                },
-            },
-        );
+        const token = await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-stock-control.csv'),
+            customerCount: 2,
+        });
         await shopClient.init();
         await adminClient.init();
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
 
     afterAll(async () => {
@@ -202,7 +200,7 @@ describe('Stock control', () => {
                 AddPaymentToOrder.Variables
             >(ADD_PAYMENT, {
                 input: {
-                    method: testPaymentMethod.code,
+                    method: testSuccessfulPaymentMethod.code,
                     metadata: {},
                 } as PaymentInput,
             });
@@ -236,23 +234,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`
     mutation UpdateStock($input: [UpdateProductVariantInput!]!) {
         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 { TestClient } from '@vendure/testing';
+
 import { GetRunningJobs, JobState } from '../graphql/generated-e2e-admin-types';
 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
  * the test until those jobs have completed;
  */
-export async function awaitRunningJobs(adminClient: TestAdminClient, timeout: number = 5000) {
+export async function awaitRunningJobs(adminClient: TestClient, timeout: number = 5000) {
     let runningJobs = 0;
     const startTime = +new Date();
     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 */
-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 { TestClient } from '@vendure/testing';
+
 import {
     AddPaymentToOrder,
     GetShippingMethods,
@@ -16,9 +17,8 @@ import {
     SET_SHIPPING_METHOD,
     TRANSITION_TO_STATE,
 } from '../graphql/shop-definitions';
-import { TestShopClient } from '../test-client';
 
-export async function proceedToArrangingPayment(shopClient: TestShopClient): Promise<ID> {
+export async function proceedToArrangingPayment(shopClient: TestClient): Promise<ID> {
     await shopClient.query<SetShippingAddress.Mutation, SetShippingAddress.Variables>(SET_SHIPPING_ADDRESS, {
         input: {
             fullName: 'name',
@@ -46,7 +46,7 @@ export async function proceedToArrangingPayment(shopClient: TestShopClient): Pro
 }
 
 export async function addPaymentToOrder(
-    shopClient: TestShopClient,
+    shopClient: TestClient,
     handler: PaymentMethodHandler,
 ): Promise<NonNullable<AddPaymentToOrder.Mutation['addPaymentToOrder']>> {
     const result = await shopClient.query<AddPaymentToOrder.Mutation, AddPaymentToOrder.Variables>(
@@ -63,20 +63,3 @@ export async function addPaymentToOrder(
     const order = result.addPaymentToOrder!;
     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,
-    }),
-});

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

@@ -1,26 +1,27 @@
+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 { ZONE_FRAGMENT } from './graphql/fragments';
 import {
     AddMembersToZone,
-    CreateZone, DeleteZone,
+    CreateZone,
+    DeleteZone,
     DeletionResult,
     GetCountryList,
-    GetZone, GetZones,
+    GetZone,
+    GetZones,
     RemoveMembersFromZone,
     UpdateZone,
 } from './graphql/generated-e2e-admin-types';
 import { GET_COUNTRY_LIST } from './graphql/shared-definitions';
-import { TestAdminClient } from './test-client';
-import { TestServer } from './test-server';
 
 // tslint:disable:no-non-null-assertion
 
 describe('Facet resolver', () => {
-    const client = new TestAdminClient();
-    const server = new TestServer();
+    const { server, adminClient } = createTestEnvironment(testConfig);
     let countries: GetCountryList.Items[];
     let zones: Array<{ id: string; name: string }>;
     let oceania: { id: string; name: string };
@@ -28,12 +29,15 @@ describe('Facet resolver', () => {
 
     beforeAll(async () => {
         await server.init({
+            dataDir,
+            initialData,
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
             customerCount: 1,
         });
-        await client.init();
+        await adminClient.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;
     }, TEST_SETUP_TIMEOUT_MS);
 
@@ -42,14 +46,14 @@ describe('Facet resolver', () => {
     });
 
     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);
         zones = result.zones;
         oceania = zones[0];
     });
 
     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,
         });
 
@@ -57,7 +61,7 @@ describe('Facet resolver', () => {
     });
 
     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: {
                 id: oceania.id,
                 name: 'oceania2',
@@ -68,7 +72,7 @@ describe('Facet resolver', () => {
     });
 
     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: {
                 name: 'Pangaea',
                 memberIds: [countries[0].id, countries[1].id],
@@ -81,7 +85,7 @@ describe('Facet resolver', () => {
     });
 
     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,
             {
                 zoneId: oceania.id,
@@ -94,13 +98,13 @@ describe('Facet resolver', () => {
     });
 
     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[2].name)).toBe(false);
@@ -109,19 +113,23 @@ describe('Facet resolver', () => {
 
     describe('deletion', () => {
         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({
                 result: DeletionResult.DELETED,
                 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();
         });
 
         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({
                 result: DeletionResult.NOT_DELETED,
@@ -130,7 +138,7 @@ describe('Facet resolver', () => {
                     '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();
         });
     });

+ 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);
-}

+ 1 - 0
packages/core/package.json

@@ -70,6 +70,7 @@
     "typeorm": "0.2.20"
   },
   "devDependencies": {
+    "@vendure/testing": "0.5.0",
     "@types/bcrypt": "^3.0.0",
     "@types/cookie-session": "^2.0.36",
     "@types/csv-parse": "^1.1.11",

+ 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] };
 
-/**
- * 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.
  */

+ 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 { mergeConfig } from './merge-config';
-import { RuntimeVendureConfig, VendureConfig } from './vendure-config';
+import { PartialVendureConfig, RuntimeVendureConfig } from './vendure-config';
 
 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
  * bootstrapping the app.
  */
-export function setConfig(userConfig: DeepPartial<VendureConfig>): void {
+export function setConfig(userConfig: PartialVendureConfig): void {
     activeConfig = mergeConfig(activeConfig, userConfig);
 }
 

+ 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 './logger/vendure-logger';
 export * from './logger/default-logger';
+export * from './logger/noop-logger';
 export * from './payment-method/example-payment-method-config';
 export * from './payment-method/payment-method-handler';
 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-eligibility-checker';
 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';
 
 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', () => {
         const input: any = {
             a: 1,
             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,
             b: 3,
             c: 5,
@@ -21,8 +35,8 @@ describe('mergeConfig()', () => {
             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,
             b: { c: 5 },
         });
@@ -40,9 +54,9 @@ describe('mergeConfig()', () => {
             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');
     });
 });

+ 10 - 17
packages/core/src/config/merge-config.ts

@@ -1,37 +1,30 @@
-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';
-
-/**
- * Simple object check.
- * From https://stackoverflow.com/a/34749873/772859
- */
-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';
-}
+import { PartialVendureConfig, VendureConfig } from './vendure-config';
 
 /**
  * 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) {
         return target;
     }
 
+    if (depth === 0) {
+        target = simpleDeepClone(target);
+    }
+
     if (isObject(target) && isObject(source)) {
         for (const key in source) {
             if (isObject((source 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])) {
-                    mergeConfig((target as any)[key], (source as any)[key]);
+                    mergeConfig((target as any)[key], (source as any)[key], depth + 1);
                 } else {
                     (target as any)[key] = (source as any)[key];
                 }

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

@@ -541,3 +541,17 @@ export interface RuntimeVendureConfig extends Required<VendureConfig> {
     orderOptions: Required<OrderOptions>;
     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.
 
-declare module 'node-libcurl' {
-    export const Curl: any;
-}
-
 declare module 'opn' {
     declare const opn: (path: string) => Promise<any>;
     export default opn;

+ 7 - 3
packages/dev-server/load-testing/init-load-test.ts

@@ -3,15 +3,19 @@
 import { bootstrap } from '@vendure/core';
 import { populate } from '@vendure/core/cli/populate';
 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 fs from 'fs';
 import path from 'path';
 
-import { clearAllTables } from '../../core/mock-data/clear-all-tables';
 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
 

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

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

+ 1 - 2
packages/dev-server/populate-dev-server.ts

@@ -2,11 +2,10 @@
 /// <reference path="../core/typings.d.ts" />
 import { bootstrap, VendureConfig } from '@vendure/core';
 import { populate } from '@vendure/core/cli/populate';
+import { clearAllTables, populateCustomers } from '@vendure/testing';
 import path from 'path';
 
-import { clearAllTables } from '../core/mock-data/clear-all-tables';
 import { initialData } from '../core/mock-data/data-sources/initial-data';
-import { populateCustomers } from '../core/mock-data/populate-customers';
 
 import { devConfig } from './dev-config';
 

+ 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.
+
+

+ 46 - 0
packages/testing/package.json

@@ -0,0 +1,46 @@
+{
+  "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"
+  },
+  "bugs": {
+    "url": "https://github.com/vendure-ecommerce/vendure/issues"
+  },
+  "dependencies": {
+    "@vendure/common": "^0.5.0",
+    "graphql": "^14.5.8",
+    "graphql-tag": "^2.10.1"
+  },
+  "devDependencies": {
+    "node-fetch": "^2.6.0",
+    "node-libcurl": "^2.0.2",
+    "rimraf": "^3.0.0",
+    "typescript": "^3.6.4"
+  },
+  "peerDependencies": {
+    "@vendure/core": "^0.5.0"
+  }
+}

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

@@ -0,0 +1,53 @@
+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';
+
+/**
+ * Config settings used for e2e tests
+ */
+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 =
     // 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 { Readable, Stream, Writable } from 'stream';
 
-import { AssetStorageStrategy } from '../../src/config/asset-storage-strategy/asset-storage-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

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

@@ -0,0 +1,21 @@
+import { VendureConfig } from '@vendure/core';
+
+import { TestClient } from './test-client';
+import { TestServer } from './test-server';
+
+export interface TestEnvironment {
+    server: TestServer;
+    adminClient: TestClient;
+    shopClient: TestClient;
+}
+
+export function createTestEnvironment(config: Required<VendureConfig>): TestEnvironment {
+    const server = new TestServer(config);
+    const adminClient = new TestClient(config, config.adminApiPath);
+    const shopClient = new TestClient(config, 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 { 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-floating-promises
 /**
@@ -12,9 +9,8 @@ import { VendureConfig } from '../src/config/vendure-config';
  */
 export async function clearAllTables(config: VendureConfig, logging = true) {
     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) {
         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 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
 /**
@@ -37,7 +35,7 @@ export class MockDataService {
                     lastName,
                     emailAddress: faker.internet.email(firstName, lastName),
                     phoneNumber: faker.phone.phoneNumber(),
-                } as CreateCustomerInput,
+                },
                 password: 'test',
             };
 
@@ -63,7 +61,7 @@ export class MockDataService {
                         province: faker.address.county(),
                         postalCode: faker.address.zipCode(),
                         countryCode: 'GB',
-                    } as CreateAddressInput,
+                    },
                     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 */
 import { INestApplication, INestMicroservice } from '@nestjs/common';
 import { LanguageCode } from '@vendure/common/lib/generated-types';
+import { InitialData, VendureConfig } from '@vendure/core';
 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 { populateCustomers } from './populate-customers';
 
-export interface PopulateOptions {
-    logging?: boolean;
-    customerCount: number;
-    productsCsvPath: string;
-    initialDataPath: string;
-}
-
 // tslint:disable:no-floating-promises
 /**
  * Clears all tables from the database and populates with (deterministic) random data.
  */
 export async function populateForTesting(
-    config: VendureConfig,
+    config: Required<VendureConfig>,
     bootstrapFn: (config: VendureConfig) => Promise<[INestApplication, INestMicroservice | undefined]>,
-    options: PopulateOptions,
+    options: TestServerOptions,
 ): Promise<[INestApplication, INestMicroservice | undefined]> {
     (config.dbConnectionOptions as any).logging = false;
     const logging = options.logging === undefined ? true : options.logging;
     const originalRequireVerification = config.authOptions.requireVerification;
     config.authOptions.requireVerification = false;
 
-    setConfig(config);
     await clearAllTables(config, logging);
     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 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;
     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);
     try {
         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) {
         return;
     }
@@ -75,7 +65,7 @@ async function populateCollections(app: INestApplication, initialDataPath: strin
 }
 
 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 productData = await fs.readFile(productsCsvPath, 'utf-8');
 

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

@@ -0,0 +1,7 @@
+export * from './simple-graphql-client';
+export * from './test-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';

+ 7 - 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 { VendureConfig } from '@vendure/core';
 import { DocumentNode } from 'graphql';
 import gql from 'graphql-tag';
 import { print } from 'graphql/language/printer';
@@ -7,10 +7,7 @@ import fetch, { Response } from 'node-fetch';
 import { Curl } from 'node-libcurl';
 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`
     mutation($username: String!, $password: String!) {
@@ -37,7 +34,7 @@ export class SimpleGraphQLClient {
     private channelToken: string;
     private headers: { [key: string]: any } = {};
 
-    constructor(private apiUrl: string = '') {}
+    constructor(private vendureConfig: Required<VendureConfig>, private apiUrl: string = '') {}
 
     setAuthToken(token: string) {
         this.authToken = token;
@@ -49,7 +46,7 @@ export class SimpleGraphQLClient {
     }
 
     setChannelToken(token: string) {
-        this.headers[getConfig().channelTokenKey] = token;
+        this.headers[this.vendureConfig.channelTokenKey] = token;
     }
 
     /**
@@ -79,22 +76,6 @@ export class SimpleGraphQLClient {
         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 }),
-        });
-    }
-
     async asUserWithCredentials(username: string, password: string) {
         // first log out as the current user
         if (this.authToken) {
@@ -142,7 +123,7 @@ export class SimpleGraphQLClient {
             headers: { 'Content-Type': 'application/json', ...this.headers },
             body,
         });
-        const authToken = response.headers.get(getConfig().authOptions.authTokenHeaderKey || '');
+        const authToken = response.headers.get(this.vendureConfig.authOptions.authTokenHeaderKey || '');
         if (authToken != null) {
             this.setAuthToken(authToken);
         }
@@ -165,7 +146,7 @@ export class SimpleGraphQLClient {
      * Upload spec: https://github.com/jaydenseric/graphql-multipart-request-spec
      * Discussion of issue: https://github.com/jaydenseric/apollo-upload-client/issues/32
      */
-    private fileUploadMutation(options: {
+    fileUploadMutation(options: {
         mutation: DocumentNode;
         filePaths: string[];
         mapVariables: (filePaths: string[]) => any;
@@ -197,7 +178,7 @@ export class SimpleGraphQLClient {
             curl.setOpt(Curl.option.HTTPPOST, processedPostData);
             curl.setOpt(Curl.option.HTTPHEADER, [
                 `Authorization: Bearer ${this.authToken}`,
-                `${getConfig().channelTokenKey}: ${this.channelToken}`,
+                `${this.vendureConfig.channelTokenKey}: ${this.channelToken}`,
             ]);
             curl.perform();
             curl.on('end', (statusCode: any, body: any) => {

+ 20 - 0
packages/testing/src/test-client.ts

@@ -0,0 +1,20 @@
+import { VendureConfig } from '@vendure/core';
+
+import { testConfig } from './config/test-config';
+import { SimpleGraphQLClient } from './simple-graphql-client';
+import { getDefaultChannelToken } from './utils/get-default-channel-token';
+
+// tslint:disable:no-console
+/**
+ * A GraphQL client for use in e2e tests.
+ */
+export class TestClient extends SimpleGraphQLClient {
+    constructor(config: Required<VendureConfig>, apiPath: string) {
+        super(config, `http://localhost:${testConfig.port}/${apiPath}`);
+    }
+
+    async init() {
+        const token = await getDefaultChannelToken(false);
+        this.setChannelToken(token);
+    }
+}

+ 42 - 33
packages/core/e2e/test-server.ts → packages/testing/src/test-server.ts

@@ -1,20 +1,13 @@
 import { INestApplication, INestMicroservice } from '@nestjs/common';
 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 path from 'path';
-import { ConnectionOptions } from 'typeorm';
 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
 /**
@@ -24,6 +17,8 @@ export class TestServer {
     app: INestApplication;
     worker?: INestMicroservice;
 
+    constructor(private vendureConfig: Required<VendureConfig>) {}
+
     /**
      * 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.
@@ -31,24 +26,19 @@ export class TestServer {
      * 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.
      */
-    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 (options.logging) {
                 console.log(`Test data not found. Populating database and caching...`);
             }
-            await this.populateInitialData(testingConfig, options);
+            await this.populateInitialData(this.vendureConfig, options);
         }
         if (options.logging) {
             console.log(`Loading test data from "${dbFilePath}"`);
         }
-        const [app, worker] = await this.bootstrapForTesting(testingConfig);
+        const [app, worker] = await this.bootstrapForTesting(this.vendureConfig);
         if (app) {
             this.app = app;
         } else {
@@ -72,30 +62,49 @@ export class TestServer {
         }
     }
 
-    private getDbFilePath() {
-        const dbDataDir = '__data__';
+    private getDbFilePath(dataDir: string) {
         // 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 dbFilePath = path.join(path.dirname(testFilePath), dbDataDir, dbFileName);
+        const dbFilePath = path.join(dataDir, dbFileName);
         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.
      */
     private async populateInitialData(
-        testingConfig: VendureConfig,
-        options: Omit<PopulateOptions, 'initialDataPath'>,
+        testingConfig: Required<VendureConfig>,
+        options: TestServerOptions,
     ): Promise<void> {
         (testingConfig.dbConnectionOptions as Mutable<SqljsConnectionOptions>).autoSave = true;
 
         const [app, worker] = await populateForTesting(testingConfig, this.bootstrapForTesting, {
             logging: false,
-            ...{
-                ...options,
-                initialDataPath: path.join(__dirname, 'fixtures/e2e-initial-data.ts'),
-            },
+            ...options,
         });
         await app.close();
         if (worker) {
@@ -113,14 +122,14 @@ export class TestServer {
     ): Promise<[INestApplication, INestMicroservice | undefined]> {
         const config = await preBootstrapConfig(userConfig);
         Logger.useLogger(config.logger);
-        const appModule = await import('../src/app.module');
+        const appModule = await import('@vendure/core/dist/app.module');
         try {
             DefaultLogger.hideNestBoostrapLogs();
             const app = await NestFactory.create(appModule.AppModule, { cors: config.cors, logger: false });
             let worker: INestMicroservice | undefined;
             await app.listen(config.port);
             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, {
                     transport: config.workerOptions.transport,
                     logger: new Logger(),

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

@@ -0,0 +1,14 @@
+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] };
+
+export interface TestServerOptions {
+    dataDir: string;
+    productsCsvPath: string;
+    initialData: InitialData;
+    customerCount?: number;
+    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()', () => {
     it('creates correct output for createAssets mutation', () => {
-        const result = createUploadPostData(gql`
+        const result = createUploadPostData(
+            gql`
                 mutation CreateAssets($input: [CreateAssetInput!]!) {
                     createAssets(input: $input) {
                         id
                         name
                     }
-                }`
-            ,
+                }
+            `,
             ['a.jpg', 'b.jpg'],
             filePaths => ({
                 input: filePaths.map(() => ({ file: null })),
-            }));
+            }),
+        );
 
         expect(result.operations.operationName).toBe('CreateAssets');
         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 { Channel } from '@vendure/core';
 import { ConnectionOptions, getConnection } from 'typeorm';
 
-import { Channel } from '../src/entity/channel/channel.entity';
-
 // tslint:disable:no-console
 // 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
+  }
+}