entity-hydrator.e2e-spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. /* eslint-disable @typescript-eslint/no-non-null-assertion */
  2. import {
  3. ChannelService,
  4. EntityHydrator,
  5. mergeConfig,
  6. Order,
  7. Product,
  8. ProductVariant,
  9. RequestContext,
  10. ActiveOrderService,
  11. } from '@vendure/core';
  12. import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
  13. import gql from 'graphql-tag';
  14. import path from 'path';
  15. import { afterAll, beforeAll, describe, expect, it } from 'vitest';
  16. import { initialData } from '../../../e2e-common/e2e-initial-data';
  17. import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
  18. import { HydrationTestPlugin } from './fixtures/test-plugins/hydration-test-plugin';
  19. import { UpdateChannelMutation, UpdateChannelMutationVariables } from './graphql/generated-e2e-admin-types';
  20. import {
  21. AddItemToOrderDocument,
  22. AddItemToOrderMutation,
  23. AddItemToOrderMutationVariables,
  24. UpdatedOrderFragment,
  25. } from './graphql/generated-e2e-shop-types';
  26. import { UPDATE_CHANNEL } from './graphql/shared-definitions';
  27. import { ADD_ITEM_TO_ORDER } from './graphql/shop-definitions';
  28. const orderResultGuard: ErrorResultGuard<UpdatedOrderFragment> = createErrorResultGuard(
  29. input => !!input.lines,
  30. );
  31. describe('Entity hydration', () => {
  32. const { server, adminClient, shopClient } = createTestEnvironment(
  33. mergeConfig(testConfig(), {
  34. plugins: [HydrationTestPlugin],
  35. }),
  36. );
  37. beforeAll(async () => {
  38. await server.init({
  39. initialData,
  40. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  41. customerCount: 1,
  42. });
  43. await adminClient.asSuperAdmin();
  44. }, TEST_SETUP_TIMEOUT_MS);
  45. afterAll(async () => {
  46. await server.destroy();
  47. });
  48. it('includes existing relations', async () => {
  49. const { hydrateProduct } = await adminClient.query<HydrateProductQuery>(GET_HYDRATED_PRODUCT, {
  50. id: 'T_1',
  51. });
  52. expect(hydrateProduct.facetValues).toBeDefined();
  53. expect(hydrateProduct.facetValues.length).toBe(2);
  54. });
  55. it('hydrates top-level single relation', async () => {
  56. const { hydrateProduct } = await adminClient.query<HydrateProductQuery>(GET_HYDRATED_PRODUCT, {
  57. id: 'T_1',
  58. });
  59. expect(hydrateProduct.featuredAsset.name).toBe('derick-david-409858-unsplash.jpg');
  60. });
  61. it('hydrates top-level array relation', async () => {
  62. const { hydrateProduct } = await adminClient.query<HydrateProductQuery>(GET_HYDRATED_PRODUCT, {
  63. id: 'T_1',
  64. });
  65. expect(hydrateProduct.assets.length).toBe(1);
  66. expect(hydrateProduct.assets[0].asset.name).toBe('derick-david-409858-unsplash.jpg');
  67. });
  68. it('hydrates nested single relation', async () => {
  69. const { hydrateProduct } = await adminClient.query<HydrateProductQuery>(GET_HYDRATED_PRODUCT, {
  70. id: 'T_1',
  71. });
  72. expect(hydrateProduct.variants[0].product.id).toBe('T_1');
  73. });
  74. it('hydrates nested array relation', async () => {
  75. const { hydrateProduct } = await adminClient.query<HydrateProductQuery>(GET_HYDRATED_PRODUCT, {
  76. id: 'T_1',
  77. });
  78. expect(hydrateProduct.variants[0].options.length).toBe(2);
  79. });
  80. it('translates top-level translatable', async () => {
  81. const { hydrateProduct } = await adminClient.query<HydrateProductQuery>(GET_HYDRATED_PRODUCT, {
  82. id: 'T_1',
  83. });
  84. expect(hydrateProduct.variants.map(v => v.name).sort()).toEqual([
  85. 'Laptop 13 inch 16GB',
  86. 'Laptop 13 inch 8GB',
  87. 'Laptop 15 inch 16GB',
  88. 'Laptop 15 inch 8GB',
  89. ]);
  90. });
  91. it('translates nested translatable', async () => {
  92. const { hydrateProduct } = await adminClient.query<HydrateProductQuery>(GET_HYDRATED_PRODUCT, {
  93. id: 'T_1',
  94. });
  95. expect(
  96. getVariantWithName(hydrateProduct, 'Laptop 13 inch 8GB')
  97. .options.map(o => o.name)
  98. .sort(),
  99. ).toEqual(['13 inch', '8GB']);
  100. });
  101. it('translates nested translatable 2', async () => {
  102. const { hydrateProduct } = await adminClient.query<HydrateProductQuery>(GET_HYDRATED_PRODUCT, {
  103. id: 'T_1',
  104. });
  105. expect(hydrateProduct.assets[0].product.name).toBe('Laptop');
  106. });
  107. it('populates ProductVariant price data', async () => {
  108. const { hydrateProduct } = await adminClient.query<HydrateProductQuery>(GET_HYDRATED_PRODUCT, {
  109. id: 'T_1',
  110. });
  111. expect(getVariantWithName(hydrateProduct, 'Laptop 13 inch 8GB').price).toBe(129900);
  112. expect(getVariantWithName(hydrateProduct, 'Laptop 13 inch 8GB').priceWithTax).toBe(155880);
  113. expect(getVariantWithName(hydrateProduct, 'Laptop 13 inch 16GB').price).toBe(219900);
  114. expect(getVariantWithName(hydrateProduct, 'Laptop 13 inch 16GB').priceWithTax).toBe(263880);
  115. expect(getVariantWithName(hydrateProduct, 'Laptop 15 inch 8GB').price).toBe(139900);
  116. expect(getVariantWithName(hydrateProduct, 'Laptop 15 inch 8GB').priceWithTax).toBe(167880);
  117. expect(getVariantWithName(hydrateProduct, 'Laptop 15 inch 16GB').price).toBe(229900);
  118. expect(getVariantWithName(hydrateProduct, 'Laptop 15 inch 16GB').priceWithTax).toBe(275880);
  119. });
  120. // https://github.com/vendure-ecommerce/vendure/issues/1153
  121. it('correctly handles empty array relations', async () => {
  122. // Product T_5 has no asset defined
  123. const { hydrateProductAsset } = await adminClient.query<{ hydrateProductAsset: Product }>(
  124. GET_HYDRATED_PRODUCT_ASSET,
  125. {
  126. id: 'T_5',
  127. },
  128. );
  129. expect(hydrateProductAsset.assets).toEqual([]);
  130. });
  131. // https://github.com/vendure-ecommerce/vendure/issues/1324
  132. it('correctly handles empty nested array relations', async () => {
  133. const { hydrateProductWithNoFacets } = await adminClient.query<{
  134. hydrateProductWithNoFacets: Product;
  135. }>(GET_HYDRATED_PRODUCT_NO_FACETS);
  136. expect(hydrateProductWithNoFacets.facetValues).toEqual([]);
  137. });
  138. // https://github.com/vendure-ecommerce/vendure/issues/1161
  139. it('correctly expands missing relations', async () => {
  140. const { hydrateProductVariant } = await adminClient.query<{ hydrateProductVariant: ProductVariant }>(
  141. GET_HYDRATED_VARIANT,
  142. { id: 'T_1' },
  143. );
  144. expect(hydrateProductVariant.product.id).toBe('T_1');
  145. expect(hydrateProductVariant.product.facetValues.map(fv => fv.id).sort()).toEqual(['T_1', 'T_2']);
  146. });
  147. // https://github.com/vendure-ecommerce/vendure/issues/1172
  148. it('can hydrate entity with getters (Order)', async () => {
  149. const { addItemToOrder } = await shopClient.query<
  150. AddItemToOrderMutation,
  151. AddItemToOrderMutationVariables
  152. >(ADD_ITEM_TO_ORDER, {
  153. productVariantId: 'T_1',
  154. quantity: 1,
  155. });
  156. orderResultGuard.assertSuccess(addItemToOrder);
  157. const { hydrateOrder } = await adminClient.query<{ hydrateOrder: Order }>(GET_HYDRATED_ORDER, {
  158. id: addItemToOrder.id,
  159. });
  160. expect(hydrateOrder.id).toBe('T_1');
  161. expect(hydrateOrder.payments).toEqual([]);
  162. });
  163. // https://github.com/vendure-ecommerce/vendure/issues/1229
  164. it('deep merges existing properties', async () => {
  165. await shopClient.asAnonymousUser();
  166. const { addItemToOrder } = await shopClient.query<
  167. AddItemToOrderMutation,
  168. AddItemToOrderMutationVariables
  169. >(ADD_ITEM_TO_ORDER, {
  170. productVariantId: 'T_1',
  171. quantity: 2,
  172. });
  173. orderResultGuard.assertSuccess(addItemToOrder);
  174. const { hydrateOrderReturnQuantities } = await adminClient.query<{
  175. hydrateOrderReturnQuantities: number[];
  176. }>(GET_HYDRATED_ORDER_QUANTITIES, {
  177. id: addItemToOrder.id,
  178. });
  179. expect(hydrateOrderReturnQuantities).toEqual([2]);
  180. });
  181. // https://github.com/vendure-ecommerce/vendure/issues/1284
  182. it('hydrates custom field relations', async () => {
  183. await adminClient.query<UpdateChannelMutation, UpdateChannelMutationVariables>(UPDATE_CHANNEL, {
  184. input: {
  185. id: 'T_1',
  186. customFields: {
  187. thumbId: 'T_2',
  188. },
  189. },
  190. });
  191. const { hydrateChannel } = await adminClient.query<{
  192. hydrateChannel: any;
  193. }>(GET_HYDRATED_CHANNEL, {
  194. id: 'T_1',
  195. });
  196. expect(hydrateChannel.customFields.thumb).toBeDefined();
  197. expect(hydrateChannel.customFields.thumb.id).toBe('T_2');
  198. });
  199. // https://github.com/vendure-ecommerce/vendure/issues/2013
  200. describe('hydration of OrderLine ProductVariantPrices', () => {
  201. let order: Order | undefined;
  202. it('Create order with 3 items', async () => {
  203. await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
  204. await shopClient.query(AddItemToOrderDocument, {
  205. productVariantId: '1',
  206. quantity: 1,
  207. });
  208. await shopClient.query(AddItemToOrderDocument, {
  209. productVariantId: '2',
  210. quantity: 1,
  211. });
  212. const { addItemToOrder } = await shopClient.query(AddItemToOrderDocument, {
  213. productVariantId: '3',
  214. quantity: 1,
  215. });
  216. orderResultGuard.assertSuccess(addItemToOrder);
  217. const channel = await server.app.get(ChannelService).getDefaultChannel();
  218. // This is ugly, but in our real life example we use a CTX constructed by Vendure
  219. const internalOrderId = +addItemToOrder.id.replace(/^\D+/g, '');
  220. const ctx = new RequestContext({
  221. channel,
  222. authorizedAsOwnerOnly: true,
  223. apiType: 'shop',
  224. isAuthorized: true,
  225. session: {
  226. activeOrderId: internalOrderId,
  227. activeChannelId: 1,
  228. user: {
  229. id: 2,
  230. },
  231. } as any,
  232. });
  233. order = await server.app.get(ActiveOrderService).getActiveOrder(ctx, undefined);
  234. await server.app.get(EntityHydrator).hydrate(ctx, order!, {
  235. relations: ['lines.productVariant'],
  236. applyProductVariantPrices: true,
  237. });
  238. });
  239. it('Variant of orderLine 1 has a price', async () => {
  240. expect(order!.lines[0].productVariant.priceWithTax).toBeGreaterThan(0);
  241. });
  242. it('Variant of orderLine 2 has a price', async () => {
  243. expect(order!.lines[1].productVariant.priceWithTax).toBeGreaterThan(0);
  244. });
  245. it('Variant of orderLine 3 has a price', async () => {
  246. expect(order!.lines[1].productVariant.priceWithTax).toBeGreaterThan(0);
  247. });
  248. });
  249. });
  250. function getVariantWithName(product: Product, name: string) {
  251. return product.variants.find(v => v.name === name)!;
  252. }
  253. type HydrateProductQuery = { hydrateProduct: Product };
  254. const GET_HYDRATED_PRODUCT = gql`
  255. query GetHydratedProduct($id: ID!) {
  256. hydrateProduct(id: $id)
  257. }
  258. `;
  259. const GET_HYDRATED_PRODUCT_NO_FACETS = gql`
  260. query GetHydratedProductWithNoFacets {
  261. hydrateProductWithNoFacets
  262. }
  263. `;
  264. const GET_HYDRATED_PRODUCT_ASSET = gql`
  265. query GetHydratedProductAsset($id: ID!) {
  266. hydrateProductAsset(id: $id)
  267. }
  268. `;
  269. const GET_HYDRATED_VARIANT = gql`
  270. query GetHydratedVariant($id: ID!) {
  271. hydrateProductVariant(id: $id)
  272. }
  273. `;
  274. const GET_HYDRATED_ORDER = gql`
  275. query GetHydratedOrder($id: ID!) {
  276. hydrateOrder(id: $id)
  277. }
  278. `;
  279. const GET_HYDRATED_ORDER_QUANTITIES = gql`
  280. query GetHydratedOrderQuantities($id: ID!) {
  281. hydrateOrderReturnQuantities(id: $id)
  282. }
  283. `;
  284. const GET_HYDRATED_CHANNEL = gql`
  285. query GetHydratedChannel($id: ID!) {
  286. hydrateChannel(id: $id)
  287. }
  288. `;