duplicate-entity.e2e-spec.ts 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086
  1. /* eslint-disable @typescript-eslint/no-non-null-assertion */
  2. import { pick } from '@vendure/common/lib/pick';
  3. import {
  4. Collection,
  5. CollectionService,
  6. defaultEntityDuplicators,
  7. EntityDuplicator,
  8. freeShipping,
  9. LanguageCode,
  10. mergeConfig,
  11. minimumOrderAmount,
  12. PermissionDefinition,
  13. TransactionalConnection,
  14. variantIdCollectionFilter,
  15. } from '@vendure/core';
  16. import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
  17. import gql from 'graphql-tag';
  18. import path from 'path';
  19. import { afterAll, beforeAll, describe, expect, it } from 'vitest';
  20. import { initialData } from '../../../e2e-common/e2e-initial-data';
  21. import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
  22. import * as Codegen from './graphql/generated-e2e-admin-types';
  23. import {
  24. AdministratorFragment,
  25. CreateAdministratorMutation,
  26. CreateAdministratorMutationVariables,
  27. CreateRoleMutation,
  28. CreateRoleMutationVariables,
  29. Permission,
  30. RoleFragment,
  31. } from './graphql/generated-e2e-admin-types';
  32. import {
  33. ASSIGN_PRODUCT_TO_CHANNEL,
  34. CREATE_ADMINISTRATOR,
  35. CREATE_CHANNEL,
  36. CREATE_COLLECTION,
  37. CREATE_PROMOTION,
  38. CREATE_ROLE,
  39. GET_COLLECTION,
  40. GET_COLLECTIONS,
  41. GET_FACET_WITH_VALUES,
  42. GET_PRODUCT_WITH_VARIANTS,
  43. GET_PROMOTION,
  44. UPDATE_PRODUCT_VARIANTS,
  45. } from './graphql/shared-definitions';
  46. const customPermission = new PermissionDefinition({
  47. name: 'custom',
  48. });
  49. let collectionService: CollectionService;
  50. let connection: TransactionalConnection;
  51. const customCollectionDuplicator = new EntityDuplicator({
  52. code: 'custom-collection-duplicator',
  53. description: [{ languageCode: LanguageCode.en, value: 'Custom Collection Duplicator' }],
  54. args: {
  55. throwError: {
  56. type: 'boolean',
  57. defaultValue: false,
  58. },
  59. },
  60. forEntities: ['Collection'],
  61. requiresPermission: customPermission.Permission,
  62. init(injector) {
  63. collectionService = injector.get(CollectionService);
  64. connection = injector.get(TransactionalConnection);
  65. },
  66. duplicate: async input => {
  67. const { ctx, id, args } = input;
  68. const original = await connection.getEntityOrThrow(ctx, Collection, id, {
  69. relations: {
  70. assets: true,
  71. featuredAsset: true,
  72. },
  73. });
  74. const newCollection = await collectionService.create(ctx, {
  75. isPrivate: original.isPrivate,
  76. customFields: original.customFields,
  77. assetIds: original.assets.map(a => a.id),
  78. featuredAssetId: original.featuredAsset?.id,
  79. parentId: original.parentId,
  80. filters: original.filters.map(f => ({
  81. code: f.code,
  82. arguments: f.args,
  83. })),
  84. inheritFilters: original.inheritFilters,
  85. translations: original.translations.map(t => ({
  86. languageCode: t.languageCode,
  87. name: `${t.name} (copy)`,
  88. slug: `${t.slug}-copy`,
  89. description: t.description,
  90. customFields: t.customFields,
  91. })),
  92. });
  93. if (args.throwError) {
  94. throw new Error('Dummy error');
  95. }
  96. return newCollection;
  97. },
  98. });
  99. describe('Duplicating entities', () => {
  100. const { server, adminClient } = createTestEnvironment(
  101. mergeConfig(testConfig(), {
  102. authOptions: {
  103. customPermissions: [customPermission],
  104. },
  105. entityOptions: {
  106. entityDuplicators: [...defaultEntityDuplicators, customCollectionDuplicator],
  107. },
  108. }),
  109. );
  110. const duplicateEntityGuard: ErrorResultGuard<{ newEntityId: string }> = createErrorResultGuard(
  111. result => !!result.newEntityId,
  112. );
  113. let testRole: RoleFragment;
  114. let testAdmin: AdministratorFragment;
  115. let newEntityId: string;
  116. beforeAll(async () => {
  117. await server.init({
  118. initialData,
  119. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  120. customerCount: 1,
  121. });
  122. await adminClient.asSuperAdmin();
  123. // create a new role and Admin and sign in as that Admin
  124. const { createRole } = await adminClient.query<CreateRoleMutation, CreateRoleMutationVariables>(
  125. CREATE_ROLE,
  126. {
  127. input: {
  128. channelIds: ['T_1'],
  129. code: 'test-role',
  130. description: 'Testing custom permissions',
  131. permissions: [
  132. Permission.CreateCollection,
  133. Permission.UpdateCollection,
  134. Permission.ReadCollection,
  135. ],
  136. },
  137. },
  138. );
  139. testRole = createRole;
  140. const { createAdministrator } = await adminClient.query<
  141. CreateAdministratorMutation,
  142. CreateAdministratorMutationVariables
  143. >(CREATE_ADMINISTRATOR, {
  144. input: {
  145. firstName: 'Test',
  146. lastName: 'Admin',
  147. emailAddress: 'test@admin.com',
  148. password: 'test',
  149. roleIds: [testRole.id],
  150. },
  151. });
  152. testAdmin = createAdministrator;
  153. }, TEST_SETUP_TIMEOUT_MS);
  154. afterAll(async () => {
  155. await server.destroy();
  156. });
  157. it('get entity duplicators', async () => {
  158. const { entityDuplicators } =
  159. await adminClient.query<Codegen.GetEntityDuplicatorsQuery>(GET_ENTITY_DUPLICATORS);
  160. expect(entityDuplicators.find(d => d.code === 'custom-collection-duplicator')).toEqual({
  161. args: [
  162. {
  163. defaultValue: false,
  164. name: 'throwError',
  165. type: 'boolean',
  166. },
  167. ],
  168. code: 'custom-collection-duplicator',
  169. description: 'Custom Collection Duplicator',
  170. forEntities: ['Collection'],
  171. requiresPermission: ['custom'],
  172. });
  173. });
  174. it('cannot duplicate if lacking permissions', async () => {
  175. await adminClient.asUserWithCredentials(testAdmin.emailAddress, 'test');
  176. const { duplicateEntity } = await adminClient.query<
  177. Codegen.DuplicateEntityMutation,
  178. Codegen.DuplicateEntityMutationVariables
  179. >(DUPLICATE_ENTITY, {
  180. input: {
  181. entityName: 'Collection',
  182. entityId: 'T_2',
  183. duplicatorInput: {
  184. code: 'custom-collection-duplicator',
  185. arguments: [
  186. {
  187. name: 'throwError',
  188. value: 'false',
  189. },
  190. ],
  191. },
  192. },
  193. });
  194. duplicateEntityGuard.assertErrorResult(duplicateEntity);
  195. expect(duplicateEntity.message).toBe('The entity could not be duplicated');
  196. expect(duplicateEntity.duplicationError).toBe(
  197. 'You do not have the required permissions to duplicate this entity',
  198. );
  199. });
  200. it('errors thrown in duplicator cause ErrorResult', async () => {
  201. await adminClient.asSuperAdmin();
  202. const { duplicateEntity } = await adminClient.query<
  203. Codegen.DuplicateEntityMutation,
  204. Codegen.DuplicateEntityMutationVariables
  205. >(DUPLICATE_ENTITY, {
  206. input: {
  207. entityName: 'Collection',
  208. entityId: 'T_2',
  209. duplicatorInput: {
  210. code: 'custom-collection-duplicator',
  211. arguments: [
  212. {
  213. name: 'throwError',
  214. value: 'true',
  215. },
  216. ],
  217. },
  218. },
  219. });
  220. duplicateEntityGuard.assertErrorResult(duplicateEntity);
  221. expect(duplicateEntity.message).toBe('The entity could not be duplicated');
  222. expect(duplicateEntity.duplicationError).toBe('Dummy error');
  223. });
  224. it('errors thrown cause all DB changes to be rolled back', async () => {
  225. await adminClient.asSuperAdmin();
  226. const { collections } = await adminClient.query<Codegen.GetCollectionsQuery>(GET_COLLECTIONS);
  227. expect(collections.items.length).toBe(1);
  228. expect(collections.items.map(i => i.name)).toEqual(['Plants']);
  229. });
  230. it('returns ID of new entity', async () => {
  231. await adminClient.asSuperAdmin();
  232. const { duplicateEntity } = await adminClient.query<
  233. Codegen.DuplicateEntityMutation,
  234. Codegen.DuplicateEntityMutationVariables
  235. >(DUPLICATE_ENTITY, {
  236. input: {
  237. entityName: 'Collection',
  238. entityId: 'T_2',
  239. duplicatorInput: {
  240. code: 'custom-collection-duplicator',
  241. arguments: [
  242. {
  243. name: 'throwError',
  244. value: 'false',
  245. },
  246. ],
  247. },
  248. },
  249. });
  250. duplicateEntityGuard.assertSuccess(duplicateEntity);
  251. expect(duplicateEntity.newEntityId).toBeDefined();
  252. newEntityId = duplicateEntity.newEntityId;
  253. });
  254. it('duplicate gets created', async () => {
  255. const { collection } = await adminClient.query<
  256. Codegen.GetCollectionQuery,
  257. Codegen.GetCollectionQueryVariables
  258. >(GET_COLLECTION, {
  259. id: newEntityId,
  260. });
  261. expect(pick(collection, ['id', 'name', 'slug'])).toEqual({
  262. id: newEntityId,
  263. name: 'Plants (copy)',
  264. slug: 'plants-copy',
  265. });
  266. });
  267. describe('default entity duplicators', () => {
  268. describe('Product duplicator', () => {
  269. let originalProduct: NonNullable<Codegen.GetProductWithVariantsQuery['product']>;
  270. let originalFirstVariant: NonNullable<
  271. Codegen.GetProductWithVariantsQuery['product']
  272. >['variants'][0];
  273. let newProduct1Id: string;
  274. let newProduct2Id: string;
  275. beforeAll(async () => {
  276. await adminClient.asSuperAdmin();
  277. // Add asset and facet values to the first product variant
  278. const { updateProductVariants } = await adminClient.query<
  279. Codegen.UpdateProductVariantsMutation,
  280. Codegen.UpdateProductVariantsMutationVariables
  281. >(UPDATE_PRODUCT_VARIANTS, {
  282. input: [
  283. {
  284. id: 'T_1',
  285. assetIds: ['T_1'],
  286. featuredAssetId: 'T_1',
  287. facetValueIds: ['T_1', 'T_2'],
  288. },
  289. ],
  290. });
  291. const { product } = await adminClient.query<
  292. Codegen.GetProductWithVariantsQuery,
  293. Codegen.GetProductWithVariantsQueryVariables
  294. >(GET_PRODUCT_WITH_VARIANTS, {
  295. id: 'T_1',
  296. });
  297. originalProduct = product!;
  298. originalFirstVariant = product!.variants.find(v => v.id === 'T_1')!;
  299. });
  300. it('duplicate product without variants', async () => {
  301. const { duplicateEntity } = await adminClient.query<
  302. Codegen.DuplicateEntityMutation,
  303. Codegen.DuplicateEntityMutationVariables
  304. >(DUPLICATE_ENTITY, {
  305. input: {
  306. entityName: 'Product',
  307. entityId: 'T_1',
  308. duplicatorInput: {
  309. code: 'product-duplicator',
  310. arguments: [
  311. {
  312. name: 'includeVariants',
  313. value: 'false',
  314. },
  315. ],
  316. },
  317. },
  318. });
  319. duplicateEntityGuard.assertSuccess(duplicateEntity);
  320. newProduct1Id = duplicateEntity.newEntityId;
  321. expect(newProduct1Id).toBe('T_2');
  322. });
  323. it('new product has no variants', async () => {
  324. const { product } = await adminClient.query<
  325. Codegen.GetProductWithVariantsQuery,
  326. Codegen.GetProductWithVariantsQueryVariables
  327. >(GET_PRODUCT_WITH_VARIANTS, {
  328. id: newProduct1Id,
  329. });
  330. expect(product?.variants.length).toBe(0);
  331. });
  332. it('is initially disabled', async () => {
  333. const { product } = await adminClient.query<
  334. Codegen.GetProductWithVariantsQuery,
  335. Codegen.GetProductWithVariantsQueryVariables
  336. >(GET_PRODUCT_WITH_VARIANTS, {
  337. id: newProduct1Id,
  338. });
  339. expect(product?.enabled).toBe(false);
  340. });
  341. it('assets are duplicated', async () => {
  342. const { product } = await adminClient.query<
  343. Codegen.GetProductWithVariantsQuery,
  344. Codegen.GetProductWithVariantsQueryVariables
  345. >(GET_PRODUCT_WITH_VARIANTS, {
  346. id: newProduct1Id,
  347. });
  348. expect(product?.featuredAsset).toEqual(originalProduct.featuredAsset);
  349. expect(product?.assets.length).toBe(1);
  350. expect(product?.assets).toEqual(originalProduct.assets);
  351. });
  352. it('facet values are duplicated', async () => {
  353. const { product } = await adminClient.query<
  354. Codegen.GetProductWithVariantsQuery,
  355. Codegen.GetProductWithVariantsQueryVariables
  356. >(GET_PRODUCT_WITH_VARIANTS, {
  357. id: newProduct1Id,
  358. });
  359. expect(product?.facetValues).toEqual(originalProduct.facetValues);
  360. expect(product?.facetValues.map(fv => fv.name).sort()).toEqual(['computers', 'electronics']);
  361. });
  362. it('duplicate product with variants', async () => {
  363. const { duplicateEntity } = await adminClient.query<
  364. Codegen.DuplicateEntityMutation,
  365. Codegen.DuplicateEntityMutationVariables
  366. >(DUPLICATE_ENTITY, {
  367. input: {
  368. entityName: 'Product',
  369. entityId: 'T_1',
  370. duplicatorInput: {
  371. code: 'product-duplicator',
  372. arguments: [
  373. {
  374. name: 'includeVariants',
  375. value: 'true',
  376. },
  377. ],
  378. },
  379. },
  380. });
  381. duplicateEntityGuard.assertSuccess(duplicateEntity);
  382. newProduct2Id = duplicateEntity.newEntityId;
  383. expect(newProduct2Id).toBe('T_3');
  384. });
  385. it('new product has variants', async () => {
  386. const { product } = await adminClient.query<
  387. Codegen.GetProductWithVariantsQuery,
  388. Codegen.GetProductWithVariantsQueryVariables
  389. >(GET_PRODUCT_WITH_VARIANTS, {
  390. id: newProduct2Id,
  391. });
  392. expect(product?.variants.length).toBe(4);
  393. expect(product?.variants.length).toBe(originalProduct.variants.length);
  394. expect(product?.variants.map(v => v.name).sort()).toEqual(
  395. originalProduct.variants.map(v => v.name).sort(),
  396. );
  397. });
  398. it('product name is suffixed', async () => {
  399. const { product } = await adminClient.query<
  400. Codegen.GetProductWithVariantsQuery,
  401. Codegen.GetProductWithVariantsQueryVariables
  402. >(GET_PRODUCT_WITH_VARIANTS, {
  403. id: newProduct2Id,
  404. });
  405. expect(product?.name).toBe('Laptop (copy)');
  406. });
  407. it('variant SKUs are suffixed', async () => {
  408. const { product } = await adminClient.query<
  409. Codegen.GetProductWithVariantsQuery,
  410. Codegen.GetProductWithVariantsQueryVariables
  411. >(GET_PRODUCT_WITH_VARIANTS, {
  412. id: newProduct2Id,
  413. });
  414. expect(product?.variants.map(v => v.sku).sort()).toEqual([
  415. 'L2201308-copy',
  416. 'L2201316-copy',
  417. 'L2201508-copy',
  418. 'L2201516-copy',
  419. ]);
  420. });
  421. it('variant assets are preserved', async () => {
  422. const { product } = await adminClient.query<
  423. Codegen.GetProductWithVariantsQuery,
  424. Codegen.GetProductWithVariantsQueryVariables
  425. >(GET_PRODUCT_WITH_VARIANTS, {
  426. id: newProduct2Id,
  427. });
  428. expect(product?.variants.find(v => v.name === originalFirstVariant.name)?.assets).toEqual(
  429. originalFirstVariant.assets,
  430. );
  431. expect(
  432. product?.variants.find(v => v.name === originalFirstVariant.name)?.featuredAsset,
  433. ).toEqual(originalFirstVariant.featuredAsset);
  434. });
  435. it('variant facet values are preserved', async () => {
  436. const { product } = await adminClient.query<
  437. Codegen.GetProductWithVariantsQuery,
  438. Codegen.GetProductWithVariantsQueryVariables
  439. >(GET_PRODUCT_WITH_VARIANTS, {
  440. id: newProduct2Id,
  441. });
  442. expect(
  443. product?.variants.find(v => v.name === originalFirstVariant.name)?.facetValues.length,
  444. ).toBe(2);
  445. expect(
  446. product?.variants.find(v => v.name === originalFirstVariant.name)?.facetValues,
  447. ).toEqual(originalFirstVariant.facetValues);
  448. });
  449. it('variant stock levels are preserved', async () => {
  450. const { product } = await adminClient.query<
  451. Codegen.GetProductWithVariantsQuery,
  452. Codegen.GetProductWithVariantsQueryVariables
  453. >(GET_PRODUCT_WITH_VARIANTS, {
  454. id: newProduct2Id,
  455. });
  456. expect(product?.variants.find(v => v.name === originalFirstVariant.name)?.stockOnHand).toBe(
  457. 100,
  458. );
  459. });
  460. it('variant prices are duplicated', async () => {
  461. const { duplicateEntity } = await adminClient.query<
  462. Codegen.DuplicateEntityMutation,
  463. Codegen.DuplicateEntityMutationVariables
  464. >(DUPLICATE_ENTITY, {
  465. input: {
  466. entityName: 'Product',
  467. entityId: 'T_1',
  468. duplicatorInput: {
  469. code: 'product-duplicator',
  470. arguments: [
  471. {
  472. name: 'includeVariants',
  473. value: 'true',
  474. },
  475. ],
  476. },
  477. },
  478. });
  479. const { product } = await adminClient.query<
  480. Codegen.GetProductWithVariantsQuery,
  481. Codegen.GetProductWithVariantsQueryVariables
  482. >(GET_PRODUCT_WITH_VARIANTS, {
  483. id: duplicateEntity.newEntityId,
  484. });
  485. duplicateEntityGuard.assertSuccess(duplicateEntity);
  486. const variant = product?.variants.find(v => v.sku.startsWith(originalFirstVariant.sku));
  487. expect(variant).not.toBeUndefined();
  488. expect(originalFirstVariant.price).toBeGreaterThan(0);
  489. expect(variant!.price).toBe(originalFirstVariant.price);
  490. });
  491. it('variant prices are duplicated on a channel specific basis', async () => {
  492. const { createChannel } = await adminClient.query<
  493. Codegen.CreateChannelMutation,
  494. Codegen.CreateChannelMutationVariables
  495. >(CREATE_CHANNEL, {
  496. input: {
  497. code: 'second-channel',
  498. token: 'second-channel',
  499. defaultLanguageCode: LanguageCode.en,
  500. currencyCode: Codegen.CurrencyCode.USD,
  501. pricesIncludeTax: false,
  502. defaultShippingZoneId: 'T_1',
  503. defaultTaxZoneId: 'T_1',
  504. },
  505. });
  506. await adminClient.query<
  507. Codegen.AssignProductsToChannelMutation,
  508. Codegen.AssignProductsToChannelMutationVariables
  509. >(ASSIGN_PRODUCT_TO_CHANNEL, {
  510. input: {
  511. channelId: createChannel.id,
  512. productIds: ['T_1'],
  513. },
  514. });
  515. const { product } = await adminClient.query<
  516. Codegen.GetProductWithVariantsQuery,
  517. Codegen.GetProductWithVariantsQueryVariables
  518. >(GET_PRODUCT_WITH_VARIANTS, {
  519. id: 'T_1',
  520. });
  521. const productVariant = product!.variants[0];
  522. adminClient.setChannelToken('second-channel');
  523. await adminClient.query<
  524. Codegen.UpdateProductVariantsMutation,
  525. Codegen.UpdateProductVariantsMutationVariables
  526. >(UPDATE_PRODUCT_VARIANTS, {
  527. input: {
  528. id: productVariant.id,
  529. price: productVariant.price + 150,
  530. },
  531. });
  532. adminClient.setChannelToken('e2e-default-channel');
  533. const { duplicateEntity } = await adminClient.query<
  534. Codegen.DuplicateEntityMutation,
  535. Codegen.DuplicateEntityMutationVariables
  536. >(DUPLICATE_ENTITY, {
  537. input: {
  538. entityName: 'Product',
  539. entityId: 'T_1',
  540. duplicatorInput: {
  541. code: 'product-duplicator',
  542. arguments: [
  543. {
  544. name: 'includeVariants',
  545. value: 'true',
  546. },
  547. ],
  548. },
  549. },
  550. });
  551. const { product: productWithVariantChannelNull } = await adminClient.query<
  552. Codegen.GetProductWithVariantsQuery,
  553. Codegen.GetProductWithVariantsQueryVariables
  554. >(GET_PRODUCT_WITH_VARIANTS, {
  555. id: duplicateEntity.newEntityId,
  556. });
  557. const productVariantChannelNull = productWithVariantChannelNull!.variants.find(v =>
  558. v.sku.startsWith(productVariant.sku),
  559. );
  560. expect(productVariantChannelNull!.price).toEqual(productVariant.price);
  561. adminClient.setChannelToken('second-channel');
  562. const { duplicateEntity: duplicateEntitySecondChannel } = await adminClient.query<
  563. Codegen.DuplicateEntityMutation,
  564. Codegen.DuplicateEntityMutationVariables
  565. >(DUPLICATE_ENTITY, {
  566. input: {
  567. entityName: 'Product',
  568. entityId: 'T_1',
  569. duplicatorInput: {
  570. code: 'product-duplicator',
  571. arguments: [
  572. {
  573. name: 'includeVariants',
  574. value: 'true',
  575. },
  576. ],
  577. },
  578. },
  579. });
  580. const { product: productWithVariantChannel2 } = await adminClient.query<
  581. Codegen.GetProductWithVariantsQuery,
  582. Codegen.GetProductWithVariantsQueryVariables
  583. >(GET_PRODUCT_WITH_VARIANTS, {
  584. id: duplicateEntitySecondChannel.newEntityId,
  585. });
  586. const productVariantChannel2 = productWithVariantChannel2!.variants.find(v =>
  587. v.sku.startsWith(productVariant.sku),
  588. );
  589. expect(productVariantChannel2!.price).toEqual(productVariant.price + 150);
  590. });
  591. it('tax categories are duplicated', async () => {
  592. // update existing variant with a non 1 first tax category
  593. // bc tax category defaults to the first available
  594. const { product } = await adminClient.query<
  595. Codegen.GetProductWithVariantsQuery,
  596. Codegen.GetProductWithVariantsQueryVariables
  597. >(GET_PRODUCT_WITH_VARIANTS, {
  598. id: 'T_1',
  599. });
  600. const { updateProductVariants } = await adminClient.query<
  601. Codegen.UpdateProductVariantsMutation,
  602. Codegen.UpdateProductVariantsMutationVariables
  603. >(UPDATE_PRODUCT_VARIANTS, {
  604. input: [{ id: product!.variants[0].id, taxCategoryId: 'T_2' }],
  605. });
  606. const { duplicateEntity } = await adminClient.query<
  607. Codegen.DuplicateEntityMutation,
  608. Codegen.DuplicateEntityMutationVariables
  609. >(DUPLICATE_ENTITY, {
  610. input: {
  611. entityName: 'Product',
  612. entityId: 'T_1',
  613. duplicatorInput: {
  614. code: 'product-duplicator',
  615. arguments: [
  616. {
  617. name: 'includeVariants',
  618. value: 'true',
  619. },
  620. ],
  621. },
  622. },
  623. });
  624. const { product: productReloaded } = await adminClient.query<
  625. Codegen.GetProductWithVariantsQuery,
  626. Codegen.GetProductWithVariantsQueryVariables
  627. >(GET_PRODUCT_WITH_VARIANTS, {
  628. id: duplicateEntity.newEntityId,
  629. });
  630. const variant = productReloaded?.variants.find(v =>
  631. v.sku.startsWith(product!.variants[0].sku),
  632. );
  633. expect(variant).not.toBeUndefined();
  634. expect(variant!.taxCategory.id).toEqual('T_2');
  635. });
  636. });
  637. describe('Collection duplicator', () => {
  638. let testCollection: Codegen.CreateCollectionMutation['createCollection'];
  639. let duplicatedCollectionId: string;
  640. beforeAll(async () => {
  641. await adminClient.asSuperAdmin();
  642. const { createCollection } = await adminClient.query<
  643. Codegen.CreateCollectionMutation,
  644. Codegen.CreateCollectionMutationVariables
  645. >(CREATE_COLLECTION, {
  646. input: {
  647. parentId: 'T_2',
  648. assetIds: ['T_1'],
  649. featuredAssetId: 'T_1',
  650. isPrivate: false,
  651. inheritFilters: false,
  652. translations: [
  653. {
  654. languageCode: LanguageCode.en,
  655. name: 'Test Collection',
  656. description: 'Test Collection description',
  657. slug: 'test-collection',
  658. },
  659. ],
  660. filters: [
  661. {
  662. code: variantIdCollectionFilter.code,
  663. arguments: [
  664. {
  665. name: 'variantIds',
  666. value: '["T_1"]',
  667. },
  668. {
  669. name: 'combineWithAnd',
  670. value: 'true',
  671. },
  672. ],
  673. },
  674. ],
  675. },
  676. });
  677. testCollection = createCollection;
  678. });
  679. it('duplicate collection', async () => {
  680. const { duplicateEntity } = await adminClient.query<
  681. Codegen.DuplicateEntityMutation,
  682. Codegen.DuplicateEntityMutationVariables
  683. >(DUPLICATE_ENTITY, {
  684. input: {
  685. entityName: 'Collection',
  686. entityId: testCollection.id,
  687. duplicatorInput: {
  688. code: 'collection-duplicator',
  689. arguments: [],
  690. },
  691. },
  692. });
  693. duplicateEntityGuard.assertSuccess(duplicateEntity);
  694. expect(duplicateEntity.newEntityId).toBeDefined();
  695. duplicatedCollectionId = duplicateEntity.newEntityId;
  696. });
  697. it('collection name is suffixed', async () => {
  698. const { collection } = await adminClient.query<
  699. Codegen.GetCollectionQuery,
  700. Codegen.GetCollectionQueryVariables
  701. >(GET_COLLECTION, {
  702. id: duplicatedCollectionId,
  703. });
  704. expect(collection?.name).toBe('Test Collection (copy)');
  705. });
  706. it('is initially private', async () => {
  707. const { collection } = await adminClient.query<
  708. Codegen.GetCollectionQuery,
  709. Codegen.GetCollectionQueryVariables
  710. >(GET_COLLECTION, {
  711. id: duplicatedCollectionId,
  712. });
  713. expect(collection?.isPrivate).toBe(true);
  714. });
  715. it('assets are duplicated', async () => {
  716. const { collection } = await adminClient.query<
  717. Codegen.GetCollectionQuery,
  718. Codegen.GetCollectionQueryVariables
  719. >(GET_COLLECTION, {
  720. id: duplicatedCollectionId,
  721. });
  722. expect(collection?.featuredAsset).toEqual(testCollection.featuredAsset);
  723. expect(collection?.assets.length).toBe(1);
  724. expect(collection?.assets).toEqual(testCollection.assets);
  725. });
  726. it('parentId matches', async () => {
  727. const { collection } = await adminClient.query<
  728. Codegen.GetCollectionQuery,
  729. Codegen.GetCollectionQueryVariables
  730. >(GET_COLLECTION, {
  731. id: duplicatedCollectionId,
  732. });
  733. expect(collection?.parent?.id).toBe(testCollection.parent?.id);
  734. });
  735. it('filters are duplicated', async () => {
  736. const { collection } = await adminClient.query<
  737. Codegen.GetCollectionQuery,
  738. Codegen.GetCollectionQueryVariables
  739. >(GET_COLLECTION, {
  740. id: duplicatedCollectionId,
  741. });
  742. expect(collection?.filters).toEqual(testCollection.filters);
  743. });
  744. });
  745. describe('Facet duplicator', () => {
  746. let newFacetId: string;
  747. it('duplicate facet', async () => {
  748. const { duplicateEntity } = await adminClient.query<
  749. Codegen.DuplicateEntityMutation,
  750. Codegen.DuplicateEntityMutationVariables
  751. >(DUPLICATE_ENTITY, {
  752. input: {
  753. entityName: 'Facet',
  754. entityId: 'T_1',
  755. duplicatorInput: {
  756. code: 'facet-duplicator',
  757. arguments: [
  758. {
  759. name: 'includeFacetValues',
  760. value: 'true',
  761. },
  762. ],
  763. },
  764. },
  765. });
  766. duplicateEntityGuard.assertSuccess(duplicateEntity);
  767. expect(duplicateEntity.newEntityId).toBe('T_2');
  768. newFacetId = duplicateEntity.newEntityId;
  769. });
  770. it('facet name is suffixed', async () => {
  771. const { facet } = await adminClient.query<
  772. Codegen.GetFacetWithValuesQuery,
  773. Codegen.GetFacetWithValuesQueryVariables
  774. >(GET_FACET_WITH_VALUES, {
  775. id: newFacetId,
  776. });
  777. expect(facet?.name).toBe('category (copy)');
  778. });
  779. it('is initially private', async () => {
  780. const { facet } = await adminClient.query<
  781. Codegen.GetFacetWithValuesQuery,
  782. Codegen.GetFacetWithValuesQueryVariables
  783. >(GET_FACET_WITH_VALUES, {
  784. id: newFacetId,
  785. });
  786. expect(facet?.isPrivate).toBe(true);
  787. });
  788. it('facet values are duplicated', async () => {
  789. const { facet } = await adminClient.query<
  790. Codegen.GetFacetWithValuesQuery,
  791. Codegen.GetFacetWithValuesQueryVariables
  792. >(GET_FACET_WITH_VALUES, {
  793. id: newFacetId,
  794. });
  795. expect(facet?.values.map(v => v.name).sort()).toEqual([
  796. 'computers (copy)',
  797. 'electronics (copy)',
  798. ]);
  799. });
  800. });
  801. describe('Promotion duplicator', () => {
  802. let testPromotion: Codegen.PromotionFragment;
  803. let duplicatedPromotionId: string;
  804. const promotionGuard: ErrorResultGuard<{ id: string }> = createErrorResultGuard(
  805. result => !!result.id,
  806. );
  807. beforeAll(async () => {
  808. await adminClient.asSuperAdmin();
  809. const { createPromotion } = await adminClient.query<
  810. Codegen.CreatePromotionMutation,
  811. Codegen.CreatePromotionMutationVariables
  812. >(CREATE_PROMOTION, {
  813. input: {
  814. enabled: true,
  815. couponCode: 'TEST',
  816. perCustomerUsageLimit: 1,
  817. usageLimit: 100,
  818. startsAt: new Date().toISOString(),
  819. endsAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30).toISOString(),
  820. translations: [
  821. {
  822. name: 'Test Promotion',
  823. description: 'Test Promotion description',
  824. languageCode: LanguageCode.en,
  825. },
  826. ],
  827. conditions: [
  828. {
  829. code: minimumOrderAmount.code,
  830. arguments: [
  831. {
  832. name: 'amount',
  833. value: '1000',
  834. },
  835. {
  836. name: 'taxInclusive',
  837. value: 'true',
  838. },
  839. ],
  840. },
  841. ],
  842. actions: [
  843. {
  844. code: freeShipping.code,
  845. arguments: [],
  846. },
  847. ],
  848. },
  849. });
  850. promotionGuard.assertSuccess(createPromotion);
  851. testPromotion = createPromotion;
  852. });
  853. it('duplicate promotion', async () => {
  854. const { duplicateEntity } = await adminClient.query<
  855. Codegen.DuplicateEntityMutation,
  856. Codegen.DuplicateEntityMutationVariables
  857. >(DUPLICATE_ENTITY, {
  858. input: {
  859. entityName: 'Promotion',
  860. entityId: testPromotion.id,
  861. duplicatorInput: {
  862. code: 'promotion-duplicator',
  863. arguments: [],
  864. },
  865. },
  866. });
  867. duplicateEntityGuard.assertSuccess(duplicateEntity);
  868. expect(testPromotion.id).toBe('T_1');
  869. expect(duplicateEntity.newEntityId).toBe('T_2');
  870. duplicatedPromotionId = duplicateEntity.newEntityId;
  871. });
  872. it('promotion name is suffixed', async () => {
  873. const { promotion } = await adminClient.query<
  874. Codegen.GetPromotionQuery,
  875. Codegen.GetPromotionQueryVariables
  876. >(GET_PROMOTION, {
  877. id: duplicatedPromotionId,
  878. });
  879. expect(promotion?.name).toBe('Test Promotion (copy)');
  880. });
  881. it('is initially disabled', async () => {
  882. const { promotion } = await adminClient.query<
  883. Codegen.GetPromotionQuery,
  884. Codegen.GetPromotionQueryVariables
  885. >(GET_PROMOTION, {
  886. id: duplicatedPromotionId,
  887. });
  888. expect(promotion?.enabled).toBe(false);
  889. });
  890. it('properties are duplicated', async () => {
  891. const { promotion } = await adminClient.query<
  892. Codegen.GetPromotionQuery,
  893. Codegen.GetPromotionQueryVariables
  894. >(GET_PROMOTION, {
  895. id: duplicatedPromotionId,
  896. });
  897. expect(promotion?.startsAt).toBe(testPromotion.startsAt);
  898. expect(promotion?.endsAt).toBe(testPromotion.endsAt);
  899. expect(promotion?.couponCode).toBe(testPromotion.couponCode);
  900. expect(promotion?.perCustomerUsageLimit).toBe(testPromotion.perCustomerUsageLimit);
  901. expect(promotion?.usageLimit).toBe(testPromotion.usageLimit);
  902. });
  903. it('conditions are duplicated', async () => {
  904. const { promotion } = await adminClient.query<
  905. Codegen.GetPromotionQuery,
  906. Codegen.GetPromotionQueryVariables
  907. >(GET_PROMOTION, {
  908. id: duplicatedPromotionId,
  909. });
  910. expect(promotion?.conditions).toEqual(testPromotion.conditions);
  911. });
  912. it('actions are duplicated', async () => {
  913. const { promotion } = await adminClient.query<
  914. Codegen.GetPromotionQuery,
  915. Codegen.GetPromotionQueryVariables
  916. >(GET_PROMOTION, {
  917. id: duplicatedPromotionId,
  918. });
  919. expect(promotion?.actions).toEqual(testPromotion.actions);
  920. });
  921. });
  922. });
  923. });
  924. const GET_ENTITY_DUPLICATORS = gql`
  925. query GetEntityDuplicators {
  926. entityDuplicators {
  927. code
  928. description
  929. requiresPermission
  930. forEntities
  931. args {
  932. name
  933. type
  934. defaultValue
  935. }
  936. }
  937. }
  938. `;
  939. const DUPLICATE_ENTITY = gql`
  940. mutation DuplicateEntity($input: DuplicateEntityInput!) {
  941. duplicateEntity(input: $input) {
  942. ... on DuplicateEntitySuccess {
  943. newEntityId
  944. }
  945. ... on DuplicateEntityError {
  946. message
  947. duplicationError
  948. }
  949. }
  950. }
  951. `;