duplicate-entity.e2e-spec.ts 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041
  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. // console.log('pv1', duplicateEntity.newEntityId, product!.variants, variant)
  488. expect(variant).not.toBeUndefined();
  489. expect(originalFirstVariant.price).toBeGreaterThan(0);
  490. expect(variant!.price).toBe(originalFirstVariant.price);
  491. });
  492. it('variant prices are duplicated on a channel specific basis', async () => {
  493. const { createChannel } = await adminClient.query<
  494. Codegen.CreateChannelMutation,
  495. Codegen.CreateChannelMutationVariables
  496. >(CREATE_CHANNEL, {
  497. input: {
  498. code: 'second-channel',
  499. token: 'second-channel',
  500. defaultLanguageCode: LanguageCode.en,
  501. currencyCode: Codegen.CurrencyCode.USD,
  502. pricesIncludeTax: false,
  503. defaultShippingZoneId: 'T_1',
  504. defaultTaxZoneId: 'T_1',
  505. },
  506. });
  507. await adminClient.query<
  508. Codegen.AssignProductsToChannelMutation,
  509. Codegen.AssignProductsToChannelMutationVariables
  510. >(ASSIGN_PRODUCT_TO_CHANNEL, {
  511. input: {
  512. channelId: createChannel.id,
  513. productIds: ['T_1'],
  514. },
  515. });
  516. const { product } = await adminClient.query<
  517. Codegen.GetProductWithVariantsQuery,
  518. Codegen.GetProductWithVariantsQueryVariables
  519. >(GET_PRODUCT_WITH_VARIANTS, {
  520. id: 'T_1',
  521. });
  522. const productVariant = product!.variants[0];
  523. adminClient.setChannelToken('second-channel');
  524. await adminClient.query<
  525. Codegen.UpdateProductVariantsMutation,
  526. Codegen.UpdateProductVariantsMutationVariables
  527. >(UPDATE_PRODUCT_VARIANTS, {
  528. input: {
  529. id: productVariant.id,
  530. price: productVariant.price + 150,
  531. },
  532. });
  533. adminClient.setChannelToken('e2e-default-channel');
  534. const { duplicateEntity } = await adminClient.query<
  535. Codegen.DuplicateEntityMutation,
  536. Codegen.DuplicateEntityMutationVariables
  537. >(DUPLICATE_ENTITY, {
  538. input: {
  539. entityName: 'Product',
  540. entityId: 'T_1',
  541. duplicatorInput: {
  542. code: 'product-duplicator',
  543. arguments: [
  544. {
  545. name: 'includeVariants',
  546. value: 'true',
  547. },
  548. ],
  549. },
  550. },
  551. });
  552. const { product: productWithVariantChannelNull } = await adminClient.query<
  553. Codegen.GetProductWithVariantsQuery,
  554. Codegen.GetProductWithVariantsQueryVariables
  555. >(GET_PRODUCT_WITH_VARIANTS, {
  556. id: duplicateEntity.newEntityId,
  557. });
  558. const productVariantChannelNull = productWithVariantChannelNull!.variants.find(v =>
  559. v.sku.startsWith(productVariant.sku),
  560. );
  561. expect(productVariantChannelNull!.price).toEqual(productVariant.price);
  562. adminClient.setChannelToken('second-channel');
  563. const { duplicateEntity: duplicateEntitySecondChannel } = await adminClient.query<
  564. Codegen.DuplicateEntityMutation,
  565. Codegen.DuplicateEntityMutationVariables
  566. >(DUPLICATE_ENTITY, {
  567. input: {
  568. entityName: 'Product',
  569. entityId: 'T_1',
  570. duplicatorInput: {
  571. code: 'product-duplicator',
  572. arguments: [
  573. {
  574. name: 'includeVariants',
  575. value: 'true',
  576. },
  577. ],
  578. },
  579. },
  580. });
  581. const { product: productWithVariantChannel2 } = await adminClient.query<
  582. Codegen.GetProductWithVariantsQuery,
  583. Codegen.GetProductWithVariantsQueryVariables
  584. >(GET_PRODUCT_WITH_VARIANTS, {
  585. id: duplicateEntitySecondChannel.newEntityId,
  586. });
  587. const productVariantChannel2 = productWithVariantChannel2!.variants.find(v =>
  588. v.sku.startsWith(productVariant.sku),
  589. );
  590. expect(productVariantChannel2!.price).toEqual(productVariant.price + 150);
  591. });
  592. });
  593. describe('Collection duplicator', () => {
  594. let testCollection: Codegen.CreateCollectionMutation['createCollection'];
  595. let duplicatedCollectionId: string;
  596. beforeAll(async () => {
  597. await adminClient.asSuperAdmin();
  598. const { createCollection } = await adminClient.query<
  599. Codegen.CreateCollectionMutation,
  600. Codegen.CreateCollectionMutationVariables
  601. >(CREATE_COLLECTION, {
  602. input: {
  603. parentId: 'T_2',
  604. assetIds: ['T_1'],
  605. featuredAssetId: 'T_1',
  606. isPrivate: false,
  607. inheritFilters: false,
  608. translations: [
  609. {
  610. languageCode: LanguageCode.en,
  611. name: 'Test Collection',
  612. description: 'Test Collection description',
  613. slug: 'test-collection',
  614. },
  615. ],
  616. filters: [
  617. {
  618. code: variantIdCollectionFilter.code,
  619. arguments: [
  620. {
  621. name: 'variantIds',
  622. value: '["T_1"]',
  623. },
  624. {
  625. name: 'combineWithAnd',
  626. value: 'true',
  627. },
  628. ],
  629. },
  630. ],
  631. },
  632. });
  633. testCollection = createCollection;
  634. });
  635. it('duplicate collection', async () => {
  636. const { duplicateEntity } = await adminClient.query<
  637. Codegen.DuplicateEntityMutation,
  638. Codegen.DuplicateEntityMutationVariables
  639. >(DUPLICATE_ENTITY, {
  640. input: {
  641. entityName: 'Collection',
  642. entityId: testCollection.id,
  643. duplicatorInput: {
  644. code: 'collection-duplicator',
  645. arguments: [],
  646. },
  647. },
  648. });
  649. duplicateEntityGuard.assertSuccess(duplicateEntity);
  650. expect(duplicateEntity.newEntityId).toBeDefined();
  651. duplicatedCollectionId = duplicateEntity.newEntityId;
  652. });
  653. it('collection name is suffixed', async () => {
  654. const { collection } = await adminClient.query<
  655. Codegen.GetCollectionQuery,
  656. Codegen.GetCollectionQueryVariables
  657. >(GET_COLLECTION, {
  658. id: duplicatedCollectionId,
  659. });
  660. expect(collection?.name).toBe('Test Collection (copy)');
  661. });
  662. it('is initially private', async () => {
  663. const { collection } = await adminClient.query<
  664. Codegen.GetCollectionQuery,
  665. Codegen.GetCollectionQueryVariables
  666. >(GET_COLLECTION, {
  667. id: duplicatedCollectionId,
  668. });
  669. expect(collection?.isPrivate).toBe(true);
  670. });
  671. it('assets are duplicated', async () => {
  672. const { collection } = await adminClient.query<
  673. Codegen.GetCollectionQuery,
  674. Codegen.GetCollectionQueryVariables
  675. >(GET_COLLECTION, {
  676. id: duplicatedCollectionId,
  677. });
  678. expect(collection?.featuredAsset).toEqual(testCollection.featuredAsset);
  679. expect(collection?.assets.length).toBe(1);
  680. expect(collection?.assets).toEqual(testCollection.assets);
  681. });
  682. it('parentId matches', async () => {
  683. const { collection } = await adminClient.query<
  684. Codegen.GetCollectionQuery,
  685. Codegen.GetCollectionQueryVariables
  686. >(GET_COLLECTION, {
  687. id: duplicatedCollectionId,
  688. });
  689. expect(collection?.parent?.id).toBe(testCollection.parent?.id);
  690. });
  691. it('filters are duplicated', async () => {
  692. const { collection } = await adminClient.query<
  693. Codegen.GetCollectionQuery,
  694. Codegen.GetCollectionQueryVariables
  695. >(GET_COLLECTION, {
  696. id: duplicatedCollectionId,
  697. });
  698. expect(collection?.filters).toEqual(testCollection.filters);
  699. });
  700. });
  701. describe('Facet duplicator', () => {
  702. let newFacetId: string;
  703. it('duplicate facet', async () => {
  704. const { duplicateEntity } = await adminClient.query<
  705. Codegen.DuplicateEntityMutation,
  706. Codegen.DuplicateEntityMutationVariables
  707. >(DUPLICATE_ENTITY, {
  708. input: {
  709. entityName: 'Facet',
  710. entityId: 'T_1',
  711. duplicatorInput: {
  712. code: 'facet-duplicator',
  713. arguments: [
  714. {
  715. name: 'includeFacetValues',
  716. value: 'true',
  717. },
  718. ],
  719. },
  720. },
  721. });
  722. duplicateEntityGuard.assertSuccess(duplicateEntity);
  723. expect(duplicateEntity.newEntityId).toBe('T_2');
  724. newFacetId = duplicateEntity.newEntityId;
  725. });
  726. it('facet name is suffixed', async () => {
  727. const { facet } = await adminClient.query<
  728. Codegen.GetFacetWithValuesQuery,
  729. Codegen.GetFacetWithValuesQueryVariables
  730. >(GET_FACET_WITH_VALUES, {
  731. id: newFacetId,
  732. });
  733. expect(facet?.name).toBe('category (copy)');
  734. });
  735. it('is initially private', async () => {
  736. const { facet } = await adminClient.query<
  737. Codegen.GetFacetWithValuesQuery,
  738. Codegen.GetFacetWithValuesQueryVariables
  739. >(GET_FACET_WITH_VALUES, {
  740. id: newFacetId,
  741. });
  742. expect(facet?.isPrivate).toBe(true);
  743. });
  744. it('facet values are duplicated', async () => {
  745. const { facet } = await adminClient.query<
  746. Codegen.GetFacetWithValuesQuery,
  747. Codegen.GetFacetWithValuesQueryVariables
  748. >(GET_FACET_WITH_VALUES, {
  749. id: newFacetId,
  750. });
  751. expect(facet?.values.map(v => v.name).sort()).toEqual([
  752. 'computers (copy)',
  753. 'electronics (copy)',
  754. ]);
  755. });
  756. });
  757. describe('Promotion duplicator', () => {
  758. let testPromotion: Codegen.PromotionFragment;
  759. let duplicatedPromotionId: string;
  760. const promotionGuard: ErrorResultGuard<{ id: string }> = createErrorResultGuard(
  761. result => !!result.id,
  762. );
  763. beforeAll(async () => {
  764. await adminClient.asSuperAdmin();
  765. const { createPromotion } = await adminClient.query<
  766. Codegen.CreatePromotionMutation,
  767. Codegen.CreatePromotionMutationVariables
  768. >(CREATE_PROMOTION, {
  769. input: {
  770. enabled: true,
  771. couponCode: 'TEST',
  772. perCustomerUsageLimit: 1,
  773. usageLimit: 100,
  774. startsAt: new Date().toISOString(),
  775. endsAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30).toISOString(),
  776. translations: [
  777. {
  778. name: 'Test Promotion',
  779. description: 'Test Promotion description',
  780. languageCode: LanguageCode.en,
  781. },
  782. ],
  783. conditions: [
  784. {
  785. code: minimumOrderAmount.code,
  786. arguments: [
  787. {
  788. name: 'amount',
  789. value: '1000',
  790. },
  791. {
  792. name: 'taxInclusive',
  793. value: 'true',
  794. },
  795. ],
  796. },
  797. ],
  798. actions: [
  799. {
  800. code: freeShipping.code,
  801. arguments: [],
  802. },
  803. ],
  804. },
  805. });
  806. promotionGuard.assertSuccess(createPromotion);
  807. testPromotion = createPromotion;
  808. });
  809. it('duplicate promotion', async () => {
  810. const { duplicateEntity } = await adminClient.query<
  811. Codegen.DuplicateEntityMutation,
  812. Codegen.DuplicateEntityMutationVariables
  813. >(DUPLICATE_ENTITY, {
  814. input: {
  815. entityName: 'Promotion',
  816. entityId: testPromotion.id,
  817. duplicatorInput: {
  818. code: 'promotion-duplicator',
  819. arguments: [],
  820. },
  821. },
  822. });
  823. duplicateEntityGuard.assertSuccess(duplicateEntity);
  824. expect(testPromotion.id).toBe('T_1');
  825. expect(duplicateEntity.newEntityId).toBe('T_2');
  826. duplicatedPromotionId = duplicateEntity.newEntityId;
  827. });
  828. it('promotion name is suffixed', async () => {
  829. const { promotion } = await adminClient.query<
  830. Codegen.GetPromotionQuery,
  831. Codegen.GetPromotionQueryVariables
  832. >(GET_PROMOTION, {
  833. id: duplicatedPromotionId,
  834. });
  835. expect(promotion?.name).toBe('Test Promotion (copy)');
  836. });
  837. it('is initially disabled', async () => {
  838. const { promotion } = await adminClient.query<
  839. Codegen.GetPromotionQuery,
  840. Codegen.GetPromotionQueryVariables
  841. >(GET_PROMOTION, {
  842. id: duplicatedPromotionId,
  843. });
  844. expect(promotion?.enabled).toBe(false);
  845. });
  846. it('properties are duplicated', async () => {
  847. const { promotion } = await adminClient.query<
  848. Codegen.GetPromotionQuery,
  849. Codegen.GetPromotionQueryVariables
  850. >(GET_PROMOTION, {
  851. id: duplicatedPromotionId,
  852. });
  853. expect(promotion?.startsAt).toBe(testPromotion.startsAt);
  854. expect(promotion?.endsAt).toBe(testPromotion.endsAt);
  855. expect(promotion?.couponCode).toBe(testPromotion.couponCode);
  856. expect(promotion?.perCustomerUsageLimit).toBe(testPromotion.perCustomerUsageLimit);
  857. expect(promotion?.usageLimit).toBe(testPromotion.usageLimit);
  858. });
  859. it('conditions are duplicated', async () => {
  860. const { promotion } = await adminClient.query<
  861. Codegen.GetPromotionQuery,
  862. Codegen.GetPromotionQueryVariables
  863. >(GET_PROMOTION, {
  864. id: duplicatedPromotionId,
  865. });
  866. expect(promotion?.conditions).toEqual(testPromotion.conditions);
  867. });
  868. it('actions are duplicated', async () => {
  869. const { promotion } = await adminClient.query<
  870. Codegen.GetPromotionQuery,
  871. Codegen.GetPromotionQueryVariables
  872. >(GET_PROMOTION, {
  873. id: duplicatedPromotionId,
  874. });
  875. expect(promotion?.actions).toEqual(testPromotion.actions);
  876. });
  877. });
  878. });
  879. });
  880. const GET_ENTITY_DUPLICATORS = gql`
  881. query GetEntityDuplicators {
  882. entityDuplicators {
  883. code
  884. description
  885. requiresPermission
  886. forEntities
  887. args {
  888. name
  889. type
  890. defaultValue
  891. }
  892. }
  893. }
  894. `;
  895. const DUPLICATE_ENTITY = gql`
  896. mutation DuplicateEntity($input: DuplicateEntityInput!) {
  897. duplicateEntity(input: $input) {
  898. ... on DuplicateEntitySuccess {
  899. newEntityId
  900. }
  901. ... on DuplicateEntityError {
  902. message
  903. duplicationError
  904. }
  905. }
  906. }
  907. `;