From 0c122bcec8ec117cb93c48bd441acdd14060c00c Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Tue, 17 Dec 2024 12:06:57 +0100 Subject: [PATCH] fix(core): Fix serialization of ShippingMethod Relates to #3277 --- .../core/e2e/entity-serialization.e2e-spec.ts | 230 ++++++++++++++++++ .../shipping-method/shipping-method.entity.ts | 9 + 2 files changed, 239 insertions(+) create mode 100644 packages/core/e2e/entity-serialization.e2e-spec.ts diff --git a/packages/core/e2e/entity-serialization.e2e-spec.ts b/packages/core/e2e/entity-serialization.e2e-spec.ts new file mode 100644 index 0000000000..4759a9896c --- /dev/null +++ b/packages/core/e2e/entity-serialization.e2e-spec.ts @@ -0,0 +1,230 @@ +import { + Channel, + ChannelService, + discountOnItemWithFacets, + hasFacetValues, + Order, + OrderLine, + OrderService, + ProductVariant, + ProductVariantService, + Promotion, + PromotionService, + RequestContextService, + ShippingLine, + ShippingMethod, + ShippingMethodService, + TransactionalConnection, +} from '@vendure/core'; +import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing'; +import { fail } from 'assert'; +import path from 'path'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +import { initialData } from '../../../e2e-common/e2e-initial-data'; +import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config'; + +import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods'; +import * as Codegen from './graphql/generated-e2e-admin-types'; +import { LanguageCode } from './graphql/generated-e2e-admin-types'; +import * as CodegenShop from './graphql/generated-e2e-shop-types'; +import { CREATE_PROMOTION } from './graphql/shared-definitions'; +import { + ADD_ITEM_TO_ORDER, + ADD_PAYMENT, + SET_SHIPPING_METHOD, + TRANSITION_TO_STATE, +} from './graphql/shop-definitions'; + +/** + * This suite tests to make sure entities can be serialized. It focuses on those + * entities that contain methods and/or properties which potentially reference + * non-serializable objects. + * + * See https://github.com/vendure-ecommerce/vendure/issues/3277 + */ +describe('Entity serialization', () => { + type OrderSuccessResult = + | CodegenShop.UpdatedOrderFragment + | CodegenShop.TestOrderFragmentFragment + | CodegenShop.TestOrderWithPaymentsFragment + | CodegenShop.ActiveOrderCustomerFragment + | CodegenShop.OrderWithAddressesFragment; + const orderResultGuard: ErrorResultGuard = createErrorResultGuard( + input => !!input.lines, + ); + + const { server, adminClient, shopClient } = createTestEnvironment({ + ...testConfig(), + paymentOptions: { + paymentMethodHandlers: [testSuccessfulPaymentMethod], + }, + }); + + beforeAll(async () => { + await server.init({ + initialData: { + ...initialData, + paymentMethods: [ + { + name: testSuccessfulPaymentMethod.code, + handler: { code: testSuccessfulPaymentMethod.code, arguments: [] }, + }, + ], + }, + productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'), + customerCount: 1, + }); + await adminClient.asSuperAdmin(); + }, TEST_SETUP_TIMEOUT_MS); + + afterAll(async () => { + await server.destroy(); + }); + + it('serialize ShippingMethod', async () => { + const ctx = await createCtx(); + const result = await server.app.get(ShippingMethodService).findAll(ctx); + + expect(result.items.length).toBeGreaterThan(0); + const shippingMethod = result.items[0]; + expect(shippingMethod instanceof ShippingMethod).toBe(true); + + assertCanBeSerialized(shippingMethod); + const json = JSON.stringify(shippingMethod); + const parsed = JSON.parse(json); + expect(parsed.createdAt).toBe(shippingMethod.createdAt.toISOString()); + }); + + it('serialize Channel', async () => { + const result = await server.app.get(ChannelService).getDefaultChannel(); + expect(result instanceof Channel).toBe(true); + + assertCanBeSerialized(result); + }); + + it('serialize Order', async () => { + await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test'); + await shopClient.query< + CodegenShop.AddItemToOrderMutation, + CodegenShop.AddItemToOrderMutationVariables + >(ADD_ITEM_TO_ORDER, { + productVariantId: 'T_1', + quantity: 1, + }); + await shopClient.query< + CodegenShop.SetShippingMethodMutation, + CodegenShop.SetShippingMethodMutationVariables + >(SET_SHIPPING_METHOD, { + id: 'T_1', + }); + const result = await shopClient.query< + CodegenShop.TransitionToStateMutation, + CodegenShop.TransitionToStateMutationVariables + >(TRANSITION_TO_STATE, { state: 'ArrangingPayment' }); + + const { addPaymentToOrder } = await shopClient.query< + CodegenShop.AddPaymentToOrderMutation, + CodegenShop.AddPaymentToOrderMutationVariables + >(ADD_PAYMENT, { + input: { + method: testSuccessfulPaymentMethod.code, + metadata: { + foo: 'bar', + }, + }, + }); + orderResultGuard.assertSuccess(addPaymentToOrder); + + const ctx = await createCtx(); + const order = await server.app.get(OrderService).findOneByCode(ctx, addPaymentToOrder.code); + + expect(order).not.toBeNull(); + expect(order instanceof Order).toBe(true); + assertCanBeSerialized(order); + }); + + it('serialize OrderLine', async () => { + const ctx = await createCtx(); + const orderLine = await server.app.get(TransactionalConnection).getEntityOrThrow(ctx, OrderLine, 1); + + expect(orderLine instanceof OrderLine).toBe(true); + assertCanBeSerialized(orderLine); + }); + + it('serialize ProductVariant', async () => { + const ctx = await createCtx(); + const productVariant = await server.app.get(ProductVariantService).findOne(ctx, 1); + + expect(productVariant instanceof ProductVariant).toBe(true); + assertCanBeSerialized(productVariant); + }); + + it('serialize Promotion', async () => { + await adminClient.query( + CREATE_PROMOTION, + { + input: { + enabled: true, + startsAt: new Date('2019-10-30T00:00:00.000Z'), + endsAt: new Date('2019-12-01T00:00:00.000Z'), + translations: [ + { + languageCode: LanguageCode.en, + name: 'test promotion', + description: 'a test promotion', + }, + ], + conditions: [ + { + code: hasFacetValues.code, + arguments: [ + { name: 'minimum', value: '2' }, + { name: 'facets', value: `["T_1"]` }, + ], + }, + ], + actions: [ + { + code: discountOnItemWithFacets.code, + arguments: [ + { name: 'discount', value: '50' }, + { name: 'facets', value: `["T_1"]` }, + ], + }, + ], + }, + }, + ); + + const ctx = await createCtx(); + const promotion = await server.app.get(PromotionService).findOne(ctx, 1); + + expect(promotion instanceof Promotion).toBe(true); + assertCanBeSerialized(promotion); + }); + + it('serialize ShippingLine', async () => { + const ctx = await createCtx(); + const shippingLine = await server.app + .get(TransactionalConnection) + .getEntityOrThrow(ctx, ShippingLine, 1); + + expect(shippingLine instanceof ShippingLine).toBe(true); + assertCanBeSerialized(shippingLine); + }); + + function assertCanBeSerialized(entity: any) { + try { + const json = JSON.stringify(entity); + } catch (e: any) { + fail(`Could not serialize entity: ${e.message as string}`); + } + } + + async function createCtx() { + return server.app.get(RequestContextService).create({ + apiType: 'admin', + }); + } +}); diff --git a/packages/core/src/entity/shipping-method/shipping-method.entity.ts b/packages/core/src/entity/shipping-method/shipping-method.entity.ts index ef04b89b5f..3cf5b40653 100644 --- a/packages/core/src/entity/shipping-method/shipping-method.entity.ts +++ b/packages/core/src/entity/shipping-method/shipping-method.entity.ts @@ -1,4 +1,5 @@ import { ConfigurableOperation } from '@vendure/common/lib/generated-types'; +import { omit } from '@vendure/common/lib/omit'; import { DeepPartial } from '@vendure/common/lib/shared-types'; import { Column, Entity, JoinTable, ManyToMany, OneToMany } from 'typeorm'; @@ -96,4 +97,12 @@ export class ShippingMethod return false; } } + + /** + * This is a fix for https://github.com/vendure-ecommerce/vendure/issues/3277, + * to prevent circular references which cause the JSON.stringify() to fail. + */ + protected toJSON() { + return omit(this, ['allCheckers', 'allCalculators'] as any); + } }