Browse Source

test(server): Write a bunch of e2e tests for Order resolver

Michael Bromley 7 years ago
parent
commit
751ea8d27a

+ 412 - 55
server/e2e/order.e2e-spec.ts

@@ -1,7 +1,8 @@
 import gql from 'graphql-tag';
-import { GetCustomerList } from 'shared/generated-types';
+import { CreateAddressInput, GetCustomerList } from 'shared/generated-types';
 
 import { GET_CUSTOMER_LIST } from '../../admin-ui/src/app/data/definitions/customer-definitions';
+import { PaymentMethodHandler } from '../src/config/payment-method/payment-method-handler';
 
 import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
 import { TestClient } from './test-client';
@@ -12,10 +13,17 @@ describe('Orders', () => {
     const server = new TestServer();
 
     beforeAll(async () => {
-        const token = await server.init({
-            productCount: 10,
-            customerCount: 1,
-        });
+        const token = await server.init(
+            {
+                productCount: 10,
+                customerCount: 2,
+            },
+            {
+                paymentOptions: {
+                    paymentMethodHandlers: [testPaymentMethod, testFailingPaymentMethod],
+                },
+            },
+        );
         await client.init();
     }, TEST_SETUP_TIMEOUT_MS);
 
@@ -30,20 +38,20 @@ describe('Orders', () => {
             await client.asAnonymousUser();
         });
 
-        it('addItemToOrder() starts with no session token', () => {
+        it('addItemToOrder starts with no session token', () => {
             expect(client.getAuthToken()).toBe('');
         });
 
-        it('activeOrder() returns null before any items have been added', async () => {
+        it('activeOrder returns null before any items have been added', async () => {
             const result = await client.query(GET_ACTIVE_ORDER);
             expect(result.activeOrder).toBeNull();
         });
 
-        it('activeOrder() creates an anonymous session', () => {
+        it('activeOrder creates an anonymous session', () => {
             expect(client.getAuthToken()).not.toBe('');
         });
 
-        it('addItemToOrder() creates a new Order with an item', async () => {
+        it('addItemToOrder creates a new Order with an item', async () => {
             const result = await client.query(ADD_ITEM_TO_ORDER, {
                 productVariantId: 'T_1',
                 quantity: 1,
@@ -56,7 +64,7 @@ describe('Orders', () => {
             firstOrderItemId = result.addItemToOrder.lines[0].id;
         });
 
-        it('addItemToOrder() errors with an invalid productVariantId', async () => {
+        it('addItemToOrder errors with an invalid productVariantId', async () => {
             try {
                 await client.query(ADD_ITEM_TO_ORDER, {
                     productVariantId: 'T_999',
@@ -70,7 +78,7 @@ describe('Orders', () => {
             }
         });
 
-        it('addItemToOrder() errors with a negative quantity', async () => {
+        it('addItemToOrder errors with a negative quantity', async () => {
             try {
                 await client.query(ADD_ITEM_TO_ORDER, {
                     productVariantId: 'T_999',
@@ -84,7 +92,7 @@ describe('Orders', () => {
             }
         });
 
-        it('addItemToOrder() with an existing productVariantId adds quantity to the existing OrderLine', async () => {
+        it('addItemToOrder with an existing productVariantId adds quantity to the existing OrderLine', async () => {
             const result = await client.query(ADD_ITEM_TO_ORDER, {
                 productVariantId: 'T_1',
                 quantity: 2,
@@ -94,7 +102,7 @@ describe('Orders', () => {
             expect(result.addItemToOrder.lines[0].quantity).toBe(3);
         });
 
-        it('adjustItemQuantity() adjusts the quantity', async () => {
+        it('adjustItemQuantity adjusts the quantity', async () => {
             const result = await client.query(ADJUST_ITEM_QUENTITY, {
                 orderItemId: firstOrderItemId,
                 quantity: 50,
@@ -104,7 +112,7 @@ describe('Orders', () => {
             expect(result.adjustItemQuantity.lines[0].quantity).toBe(50);
         });
 
-        it('adjustItemQuantity() errors with a negative quantity', async () => {
+        it('adjustItemQuantity errors with a negative quantity', async () => {
             try {
                 await client.query(ADJUST_ITEM_QUENTITY, {
                     orderItemId: firstOrderItemId,
@@ -118,7 +126,7 @@ describe('Orders', () => {
             }
         });
 
-        it('adjustItemQuantity() errors with an invalid orderItemId', async () => {
+        it('adjustItemQuantity errors with an invalid orderItemId', async () => {
             try {
                 await client.query(ADJUST_ITEM_QUENTITY, {
                     orderItemId: 'T_999',
@@ -132,7 +140,7 @@ describe('Orders', () => {
             }
         });
 
-        it('removeItemFromOrder() removes the correct item', async () => {
+        it('removeItemFromOrder removes the correct item', async () => {
             const result1 = await client.query(ADD_ITEM_TO_ORDER, {
                 productVariantId: 'T_3',
                 quantity: 3,
@@ -147,7 +155,7 @@ describe('Orders', () => {
             expect(result2.removeItemFromOrder.lines.map(i => i.productVariant.id)).toEqual(['T_3']);
         });
 
-        it('removeItemFromOrder() errors with an invalid orderItemId', async () => {
+        it('removeItemFromOrder errors with an invalid orderItemId', async () => {
             try {
                 await client.query(REMOVE_ITEM_FROM_ORDER, {
                     orderItemId: 'T_999',
@@ -160,7 +168,7 @@ describe('Orders', () => {
             }
         });
 
-        it('nextOrderStates() returns next valid states', async () => {
+        it('nextOrderStates returns next valid states', async () => {
             const result = await client.query(gql`
                 query {
                     nextOrderStates
@@ -170,16 +178,9 @@ describe('Orders', () => {
             expect(result.nextOrderStates).toEqual(['ArrangingPayment']);
         });
 
-        it('transitionOrderToState() throws for an invalid state', async () => {
+        it('transitionOrderToState throws for an invalid state', async () => {
             try {
-                await client.query(gql`
-                    mutation {
-                        transitionOrderToState(state: "Completed") {
-                            id
-                            state
-                        }
-                    }
-                `);
+                await client.query(TRANSITION_TO_STATE, { state: 'Completed' });
                 fail('Should have thrown');
             } catch (err) {
                 expect(err.message).toEqual(
@@ -188,15 +189,8 @@ describe('Orders', () => {
             }
         });
 
-        it('transitionOrderToState() transitions Order to the next valid state', async () => {
-            const result = await client.query(gql`
-                mutation {
-                    transitionOrderToState(state: "ArrangingPayment") {
-                        id
-                        state
-                    }
-                }
-            `);
+        it('transitionOrderToState transitions Order to the next valid state', async () => {
+            const result = await client.query(TRANSITION_TO_STATE, { state: 'ArrangingPayment' });
 
             expect(result.transitionOrderToState).toEqual({ id: 'T_1', state: 'ArrangingPayment' });
         });
@@ -204,8 +198,9 @@ describe('Orders', () => {
 
     describe('as authenticated user', () => {
         let firstOrderItemId: string;
-        let activeOrderId: string;
+        let activeOrder: any;
         let authenticatedUserEmailAddress: string;
+        let customers: GetCustomerList.Items[];
         const password = 'test';
 
         beforeAll(async () => {
@@ -214,21 +209,21 @@ describe('Orders', () => {
                 GET_CUSTOMER_LIST,
                 {
                     options: {
-                        take: 1,
+                        take: 2,
                     },
                 },
             );
-            const customer = result.customers.items[0];
-            authenticatedUserEmailAddress = customer.emailAddress;
+            customers = result.customers.items;
+            authenticatedUserEmailAddress = customers[0].emailAddress;
             await client.asUserWithCredentials(authenticatedUserEmailAddress, password);
         });
 
-        it('activeOrder() returns null before any items have been added', async () => {
+        it('activeOrder returns null before any items have been added', async () => {
             const result = await client.query(GET_ACTIVE_ORDER);
             expect(result.activeOrder).toBeNull();
         });
 
-        it('addItemToOrder() creates a new Order with an item', async () => {
+        it('addItemToOrder creates a new Order with an item', async () => {
             const result = await client.query(ADD_ITEM_TO_ORDER, {
                 productVariantId: 'T_1',
                 quantity: 1,
@@ -237,11 +232,17 @@ describe('Orders', () => {
             expect(result.addItemToOrder.lines.length).toBe(1);
             expect(result.addItemToOrder.lines[0].quantity).toBe(1);
             expect(result.addItemToOrder.lines[0].productVariant.id).toBe('T_1');
-            activeOrderId = result.addItemToOrder.id;
+            activeOrder = result.addItemToOrder;
             firstOrderItemId = result.addItemToOrder.lines[0].id;
         });
 
-        it('addItemToOrder() with an existing productVariantId adds quantity to the existing OrderLine', async () => {
+        it('activeOrder returns order after item has been added', async () => {
+            const result = await client.query(GET_ACTIVE_ORDER);
+            expect(result.activeOrder.id).toBe(activeOrder.id);
+            expect(result.activeOrder.state).toBe('AddingItems');
+        });
+
+        it('addItemToOrder with an existing productVariantId adds quantity to the existing OrderLine', async () => {
             const result = await client.query(ADD_ITEM_TO_ORDER, {
                 productVariantId: 'T_1',
                 quantity: 2,
@@ -251,7 +252,7 @@ describe('Orders', () => {
             expect(result.addItemToOrder.lines[0].quantity).toBe(3);
         });
 
-        it('adjustItemQuantity() adjusts the quantity', async () => {
+        it('adjustItemQuantity adjusts the quantity', async () => {
             const result = await client.query(ADJUST_ITEM_QUENTITY, {
                 orderItemId: firstOrderItemId,
                 quantity: 50,
@@ -261,7 +262,7 @@ describe('Orders', () => {
             expect(result.adjustItemQuantity.lines[0].quantity).toBe(50);
         });
 
-        it('removeItemFromOrder() removes the correct item', async () => {
+        it('removeItemFromOrder removes the correct item', async () => {
             const result1 = await client.query(ADD_ITEM_TO_ORDER, {
                 productVariantId: 'T_3',
                 quantity: 3,
@@ -276,12 +277,8 @@ describe('Orders', () => {
             expect(result2.removeItemFromOrder.lines.map(i => i.productVariant.id)).toEqual(['T_3']);
         });
 
-        it('nextOrderStates() returns next valid states', async () => {
-            const result = await client.query(gql`
-                query {
-                    nextOrderStates
-                }
-            `);
+        it('nextOrderStates returns next valid states', async () => {
+            const result = await client.query(GET_NEXT_STATES);
 
             expect(result.nextOrderStates).toEqual(['ArrangingPayment']);
         });
@@ -293,14 +290,286 @@ describe('Orders', () => {
 
             await client.asUserWithCredentials(authenticatedUserEmailAddress, password);
             const result2 = await client.query(GET_ACTIVE_ORDER);
-            expect(result2.activeOrder.id).toBe(activeOrderId);
+            expect(result2.activeOrder.id).toBe(activeOrder.id);
+        });
+
+        describe('shipping', () => {
+            let shippingMethods: any;
+
+            it('setOrderShippingAddress sets shipping address', async () => {
+                const address: CreateAddressInput = {
+                    fullName: 'name',
+                    company: 'company',
+                    streetLine1: '12 the street',
+                    streetLine2: 'line 2',
+                    city: 'foo',
+                    province: 'bar',
+                    postalCode: '123456',
+                    country: 'baz',
+                    phoneNumber: '4444444',
+                };
+                const result = await client.query(SET_SHIPPING_ADDRESS, {
+                    input: address,
+                });
+
+                expect(result.setOrderShippingAddress.shippingAddress).toEqual(address);
+            });
+
+            it('eligibleShippingMethods lists shipping methods', async () => {
+                const result = await client.query(GET_ELIGIBLE_SHIPPING_METHODS);
+
+                shippingMethods = result.eligibleShippingMethods;
+
+                expect(shippingMethods).toEqual([
+                    { id: 'T_1', price: 500, description: 'Standard Shipping' },
+                    { id: 'T_2', price: 1000, description: 'Express Shipping' },
+                ]);
+            });
+
+            it('shipping is initially unset', async () => {
+                const result = await client.query(GET_ACTIVE_ORDER);
+
+                expect(result.activeOrder.shipping).toEqual(0);
+                expect(result.activeOrder.shippingMethod).toEqual(null);
+            });
+
+            it('setOrderShippingMethod sets the shipping method', async () => {
+                const result = await client.query(SET_SHIPPING_METHOD, {
+                    id: shippingMethods[1].id,
+                });
+
+                const activeOrderResult = await client.query(GET_ACTIVE_ORDER);
+
+                const order = activeOrderResult.activeOrder;
+
+                expect(order.shipping).toBe(shippingMethods[1].price);
+                expect(order.shippingMethod.id).toBe(shippingMethods[1].id);
+                expect(order.shippingMethod.description).toBe(shippingMethods[1].description);
+            });
+
+            it('shipping method is preserved after adjustItemQuantity', async () => {
+                const activeOrderResult = await client.query(GET_ACTIVE_ORDER);
+                activeOrder = activeOrderResult.activeOrder;
+                const result = await client.query(ADJUST_ITEM_QUENTITY, {
+                    orderItemId: activeOrder.lines[0].id,
+                    quantity: 10,
+                });
+
+                expect(result.adjustItemQuantity.shipping).toBe(shippingMethods[1].price);
+                expect(result.adjustItemQuantity.shippingMethod.id).toBe(shippingMethods[1].id);
+                expect(result.adjustItemQuantity.shippingMethod.description).toBe(
+                    shippingMethods[1].description,
+                );
+            });
+        });
+
+        describe('payment', () => {
+            it('attempting add a Payment throws error when in AddingItems state', async () => {
+                try {
+                    await client.query(ADD_PAYMENT, {
+                        input: {
+                            method: testPaymentMethod.code,
+                            metadata: {},
+                        },
+                    });
+                    fail('Should have thrown');
+                } catch (err) {
+                    expect(err.message).toEqual(
+                        expect.stringContaining(
+                            `A Payment may only be added when Order is in "ArrangingPayment" state`,
+                        ),
+                    );
+                }
+            });
+
+            it('transitions to the ArrangingPayment state', async () => {
+                const result = await client.query(TRANSITION_TO_STATE, { state: 'ArrangingPayment' });
+                expect(result.transitionOrderToState).toEqual({
+                    id: activeOrder.id,
+                    state: 'ArrangingPayment',
+                });
+            });
+
+            it('attempting to add an item throws error when in ArrangingPayment state', async () => {
+                try {
+                    const result = await client.query(ADD_ITEM_TO_ORDER, {
+                        productVariantId: 'T_4',
+                        quantity: 1,
+                    });
+                    fail('Should have thrown');
+                } catch (err) {
+                    expect(err.message).toEqual(
+                        expect.stringContaining(
+                            `Order contents may only be modified when in the "AddingItems" state`,
+                        ),
+                    );
+                }
+            });
+
+            it('attempting to modify item quantity throws error when in ArrangingPayment state', async () => {
+                try {
+                    const result = await client.query(ADJUST_ITEM_QUENTITY, {
+                        orderItemId: activeOrder.lines[0].id,
+                        quantity: 12,
+                    });
+                    fail('Should have thrown');
+                } catch (err) {
+                    expect(err.message).toEqual(
+                        expect.stringContaining(
+                            `Order contents may only be modified when in the "AddingItems" state`,
+                        ),
+                    );
+                }
+            });
+
+            it('attempting to remove an item throws error when in ArrangingPayment state', async () => {
+                try {
+                    const result = await client.query(REMOVE_ITEM_FROM_ORDER, {
+                        orderItemId: activeOrder.lines[0].id,
+                    });
+                    fail('Should have thrown');
+                } catch (err) {
+                    expect(err.message).toEqual(
+                        expect.stringContaining(
+                            `Order contents may only be modified when in the "AddingItems" state`,
+                        ),
+                    );
+                }
+            });
+
+            it('attempting to setOrderShippingMethod throws error when in ArrangingPayment state', async () => {
+                const shippingMethodsResult = await client.query(GET_ELIGIBLE_SHIPPING_METHODS);
+                const shippingMethods = shippingMethodsResult.eligibleShippingMethods;
+
+                try {
+                    await client.query(SET_SHIPPING_METHOD, {
+                        id: shippingMethods[0].id,
+                    });
+                    fail('Should have thrown');
+                } catch (err) {
+                    expect(err.message).toEqual(
+                        expect.stringContaining(
+                            `Order contents may only be modified when in the "AddingItems" state`,
+                        ),
+                    );
+                }
+            });
+
+            it('adds a declined payment', async () => {
+                const result = await client.query(ADD_PAYMENT, {
+                    input: {
+                        method: testFailingPaymentMethod.code,
+                        metadata: {
+                            foo: 'bar',
+                        },
+                    },
+                });
+
+                const payment = result.addPaymentToOrder.payments[0];
+                expect(result.addPaymentToOrder.payments.length).toBe(1);
+                expect(payment.method).toBe(testFailingPaymentMethod.code);
+                expect(payment.state).toBe('Declined');
+                expect(payment.transactionId).toBe(null);
+                expect(payment.metadata).toEqual({
+                    foo: 'bar',
+                });
+            });
+
+            it('adds a successful payment and transitions Order state', async () => {
+                const result = await client.query(ADD_PAYMENT, {
+                    input: {
+                        method: testPaymentMethod.code,
+                        metadata: {
+                            baz: 'quux',
+                        },
+                    },
+                });
+
+                const payment = result.addPaymentToOrder.payments[0];
+                expect(result.addPaymentToOrder.state).toBe('PaymentSettled');
+                expect(result.addPaymentToOrder.active).toBe(false);
+                expect(result.addPaymentToOrder.payments.length).toBe(1);
+                expect(payment.method).toBe(testPaymentMethod.code);
+                expect(payment.state).toBe('Settled');
+                expect(payment.transactionId).toBe('12345');
+                expect(payment.metadata).toEqual({
+                    baz: 'quux',
+                });
+            });
+        });
+
+        describe('orderByCode', () => {
+            it('works for own Order', async () => {
+                const result = await client.query(GET_ORDER_BY_CODE, {
+                    code: activeOrder.code,
+                });
+
+                expect(result.orderByCode.id).toBe(activeOrder.id);
+            });
+
+            it("throws error for another user's Order", async () => {
+                authenticatedUserEmailAddress = customers[1].emailAddress;
+                await client.asUserWithCredentials(authenticatedUserEmailAddress, password);
+
+                try {
+                    await client.query(GET_ORDER_BY_CODE, {
+                        code: activeOrder.code,
+                    });
+                    fail('Should have thrown');
+                } catch (err) {
+                    expect(err.message).toEqual(expect.stringContaining(`This action is forbidden`));
+                }
+            });
+
+            it('throws error when not logged in', async () => {
+                await client.asAnonymousUser();
+
+                try {
+                    await client.query(GET_ORDER_BY_CODE, {
+                        code: activeOrder.code,
+                    });
+                    fail('Should have thrown');
+                } catch (err) {
+                    expect(err.message).toEqual(expect.stringContaining(`This action is forbidden`));
+                }
+            });
         });
     });
 });
 
+const testPaymentMethod = new PaymentMethodHandler({
+    code: 'test-payment-method',
+    name: 'Test Payment Method',
+    args: {},
+    createPayment: (order, args, metadata) => {
+        return {
+            amount: order.total,
+            state: 'Settled',
+            transactionId: '12345',
+            metadata,
+        };
+    },
+});
+
+const testFailingPaymentMethod = new PaymentMethodHandler({
+    code: 'test-failing-payment-method',
+    name: 'Test Failing Payment Method',
+    args: {},
+    createPayment: (order, args, metadata) => {
+        return {
+            amount: order.total,
+            state: 'Declined',
+            metadata,
+        };
+    },
+});
+
 const TEST_ORDER_FRAGMENT = gql`
     fragment TestOrderFragment on Order {
         id
+        code
+        state
+        active
         lines {
             id
             quantity
@@ -308,16 +577,22 @@ const TEST_ORDER_FRAGMENT = gql`
                 id
             }
         }
+        shipping
+        shippingMethod {
+            id
+            code
+            description
+        }
     }
 `;
 
 const GET_ACTIVE_ORDER = gql`
     query {
         activeOrder {
-            id
-            state
+            ...TestOrderFragment
         }
     }
+    ${TEST_ORDER_FRAGMENT}
 `;
 
 const ADD_ITEM_TO_ORDER = gql`
@@ -346,3 +621,85 @@ const REMOVE_ITEM_FROM_ORDER = gql`
     }
     ${TEST_ORDER_FRAGMENT}
 `;
+
+const GET_NEXT_STATES = gql`
+    query {
+        nextOrderStates
+    }
+`;
+
+const TRANSITION_TO_STATE = gql`
+    mutation TransitionToState($state: String!) {
+        transitionOrderToState(state: $state) {
+            id
+            state
+        }
+    }
+`;
+
+const GET_ELIGIBLE_SHIPPING_METHODS = gql`
+    query {
+        eligibleShippingMethods {
+            id
+            price
+            description
+        }
+    }
+`;
+
+const SET_SHIPPING_ADDRESS = gql`
+    mutation SetShippingAddress($input: CreateAddressInput!) {
+        setOrderShippingAddress(input: $input) {
+            shippingAddress {
+                fullName
+                company
+                streetLine1
+                streetLine2
+                city
+                province
+                postalCode
+                country
+                phoneNumber
+            }
+        }
+    }
+`;
+
+const SET_SHIPPING_METHOD = gql`
+    mutation SetShippingMethod($id: ID!) {
+        setOrderShippingMethod(shippingMethodId: $id) {
+            shipping
+            shippingMethod {
+                id
+                code
+                description
+            }
+        }
+    }
+`;
+
+const ADD_PAYMENT = gql`
+    mutation AddPaymentToOrder($input: PaymentInput!) {
+        addPaymentToOrder(input: $input) {
+            ...TestOrderFragment
+            payments {
+                id
+                transactionId
+                method
+                amount
+                state
+                metadata
+            }
+        }
+    }
+    ${TEST_ORDER_FRAGMENT}
+`;
+
+const GET_ORDER_BY_CODE = gql`
+    query GetOrderByCode($code: String!) {
+        orderByCode(code: $code) {
+            ...TestOrderFragment
+        }
+    }
+    ${TEST_ORDER_FRAGMENT}
+`;

+ 1 - 1
server/src/entity/payment-method/payment-method.entity.ts

@@ -26,7 +26,7 @@ export class PaymentMethod extends VendureEntity {
         if (!handler) {
             throw new I18nError(`error.no-payment-handler-with-code`, { code: this.code });
         }
-        const result = await handler.createPayment(order, this.configArgs, metadata);
+        const result = await handler.createPayment(order, this.configArgs, metadata || {});
         return new Payment(result);
     }
 }

+ 4 - 1
server/src/i18n/messages/en.json

@@ -6,8 +6,11 @@
     "channel-not-found":  "No channel with the token \"{ token }\" exists",
     "entity-has-no-translation-in-language": "Translatable entity '{ entityName }' has not been translated into the requested language ({ languageCode })",
     "entity-with-id-not-found": "No { entityName } with the id '{ id }' could be found",
+    "forbidden": "This action is forbidden",
     "invalid-sort-field": "The sort field '{ fieldName }' is invalid. Valid fields are: { validFields }",
+    "order-contents-may-only-be-modified-in-addingitems-state": "Order contents may only be modified when in the \"AddingItems\" state",
     "order-does-not-contain-line-with-id": "This order does not contain an OrderLine with the id { id }",
-    "order-item-quantity-must-be-positive": "{ quantity } is not a valid quantity for an OrderItem"
+    "order-item-quantity-must-be-positive": "{ quantity } is not a valid quantity for an OrderItem",
+    "payment-may-only-be-added-in-arrangingpayment-state": "A Payment may only be added when Order is in \"ArrangingPayment\" state"
   }
 }