product-channel.e2e-spec.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. /* eslint-disable @typescript-eslint/no-non-null-assertion */
  2. import {
  3. createErrorResultGuard,
  4. createTestEnvironment,
  5. E2E_DEFAULT_CHANNEL_TOKEN,
  6. ErrorResultGuard,
  7. } from '@vendure/testing';
  8. import path from 'path';
  9. import { afterAll, beforeAll, describe, expect, it } from 'vitest';
  10. import { initialData } from '../../../e2e-common/e2e-initial-data';
  11. import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
  12. import {
  13. AssignProductsToChannelDocument,
  14. AssignProductVariantsToChannelDocument,
  15. ChannelFragment,
  16. CreateAdministratorDocument,
  17. CreateChannelDocument,
  18. CreateProductDocument,
  19. CreateProductMutation,
  20. CreateProductVariantsDocument,
  21. CreateRoleDocument,
  22. CreateRoleMutation,
  23. CurrencyCode,
  24. GetChannelsDocument,
  25. GetProductVariantListDocument,
  26. GetProductWithVariantsDocument,
  27. GetProductWithVariantsQuery,
  28. LanguageCode,
  29. Permission,
  30. ProductVariantFragment,
  31. RemoveProductsFromChannelDocument,
  32. RemoveProductVariantsFromChannelDocument,
  33. UpdateChannelDocument,
  34. UpdateProductDocument,
  35. UpdateProductVariantsDocument,
  36. } from './graphql/generated-e2e-admin-types';
  37. import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
  38. describe('ChannelAware Products and ProductVariants', () => {
  39. const { server, adminClient, shopClient } = createTestEnvironment(testConfig());
  40. const SECOND_CHANNEL_TOKEN = 'second_channel_token';
  41. const THIRD_CHANNEL_TOKEN = 'third_channel_token';
  42. let secondChannelAdminRole: CreateRoleMutation['createRole'];
  43. beforeAll(async () => {
  44. await server.init({
  45. initialData,
  46. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  47. customerCount: 1,
  48. });
  49. await adminClient.asSuperAdmin();
  50. await adminClient.query(CreateChannelDocument, {
  51. input: {
  52. code: 'second-channel',
  53. token: SECOND_CHANNEL_TOKEN,
  54. defaultLanguageCode: LanguageCode.en,
  55. currencyCode: CurrencyCode.GBP,
  56. pricesIncludeTax: true,
  57. defaultShippingZoneId: 'T_1',
  58. defaultTaxZoneId: 'T_1',
  59. },
  60. });
  61. await adminClient.query(CreateChannelDocument, {
  62. input: {
  63. code: 'third-channel',
  64. token: THIRD_CHANNEL_TOKEN,
  65. defaultLanguageCode: LanguageCode.en,
  66. currencyCode: CurrencyCode.EUR,
  67. pricesIncludeTax: true,
  68. defaultShippingZoneId: 'T_1',
  69. defaultTaxZoneId: 'T_1',
  70. },
  71. });
  72. const { createRole } = await adminClient.query(CreateRoleDocument, {
  73. input: {
  74. description: 'second channel admin',
  75. code: 'second-channel-admin',
  76. channelIds: ['T_2'],
  77. permissions: [
  78. Permission.ReadCatalog,
  79. Permission.ReadSettings,
  80. Permission.ReadAdministrator,
  81. Permission.CreateAdministrator,
  82. Permission.UpdateAdministrator,
  83. ],
  84. },
  85. });
  86. secondChannelAdminRole = createRole;
  87. await adminClient.query(CreateAdministratorDocument, {
  88. input: {
  89. firstName: 'Admin',
  90. lastName: 'Two',
  91. emailAddress: 'admin2@test.com',
  92. password: 'test',
  93. roleIds: [secondChannelAdminRole.id],
  94. },
  95. });
  96. }, TEST_SETUP_TIMEOUT_MS);
  97. afterAll(async () => {
  98. await server.destroy();
  99. });
  100. describe('assigning Product to Channels', () => {
  101. let product1: NonNullable<GetProductWithVariantsQuery['product']>;
  102. beforeAll(async () => {
  103. await adminClient.asSuperAdmin();
  104. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  105. const { product } = await adminClient.query(GetProductWithVariantsDocument, {
  106. id: 'T_1',
  107. });
  108. product1 = product!;
  109. });
  110. it(
  111. 'throws if attempting to assign Product to channel to which the admin has no access',
  112. assertThrowsWithMessage(async () => {
  113. await adminClient.asUserWithCredentials('admin2@test.com', 'test');
  114. await adminClient.query(AssignProductsToChannelDocument, {
  115. input: {
  116. channelId: 'T_3',
  117. productIds: [product1.id],
  118. },
  119. });
  120. }, 'You are not currently authorized to perform this action'),
  121. );
  122. it('assigns Product to Channel and applies price factor', async () => {
  123. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  124. const PRICE_FACTOR = 0.5;
  125. await adminClient.asSuperAdmin();
  126. const { assignProductsToChannel } = await adminClient.query(AssignProductsToChannelDocument, {
  127. input: {
  128. channelId: 'T_2',
  129. productIds: [product1.id],
  130. priceFactor: PRICE_FACTOR,
  131. },
  132. });
  133. expect(assignProductsToChannel[0].channels.map(c => c.id).sort()).toEqual(['T_1', 'T_2']);
  134. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  135. const { product } = await adminClient.query(GetProductWithVariantsDocument, {
  136. id: product1.id,
  137. });
  138. expect(product!.variants.map(v => v.price)).toEqual(
  139. product1.variants.map(v => Math.round(v.price * PRICE_FACTOR)),
  140. );
  141. // Second Channel is configured to include taxes in price, so they should be the same.
  142. expect(product!.variants.map(v => v.priceWithTax)).toEqual(
  143. product1.variants.map(v => Math.round(v.priceWithTax * PRICE_FACTOR)),
  144. );
  145. // Second Channel has the default currency of GBP, so the prices should be the same.
  146. expect(product!.variants.map(v => v.currencyCode)).toEqual(['GBP', 'GBP', 'GBP', 'GBP']);
  147. });
  148. it('ProductVariant.channels includes all Channels from default Channel', async () => {
  149. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  150. const { product } = await adminClient.query(GetProductWithVariantsDocument, {
  151. id: product1.id,
  152. });
  153. expect(product?.variants[0].channels.map(c => c.id)).toEqual(['T_1', 'T_2']);
  154. });
  155. it('ProductVariant.channels includes only current Channel from non-default Channel', async () => {
  156. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  157. const { product } = await adminClient.query(GetProductWithVariantsDocument, {
  158. id: product1.id,
  159. });
  160. expect(product?.variants[0].channels.map(c => c.id)).toEqual(['T_2']);
  161. });
  162. it('does not assign Product to same channel twice', async () => {
  163. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  164. const { assignProductsToChannel } = await adminClient.query(AssignProductsToChannelDocument, {
  165. input: {
  166. channelId: 'T_2',
  167. productIds: [product1.id],
  168. },
  169. });
  170. expect(assignProductsToChannel[0].channels.map(c => c.id).sort()).toEqual(['T_1', 'T_2']);
  171. });
  172. it(
  173. 'throws if attempting to remove Product from default Channel',
  174. assertThrowsWithMessage(async () => {
  175. await adminClient.query(RemoveProductsFromChannelDocument, {
  176. input: {
  177. productIds: [product1.id],
  178. channelId: 'T_1',
  179. },
  180. });
  181. }, 'Items cannot be removed from the default Channel'),
  182. );
  183. it('removes Product from Channel', async () => {
  184. await adminClient.asSuperAdmin();
  185. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  186. const { removeProductsFromChannel } = await adminClient.query(RemoveProductsFromChannelDocument, {
  187. input: {
  188. productIds: [product1.id],
  189. channelId: 'T_2',
  190. },
  191. });
  192. expect(removeProductsFromChannel[0].channels.map(c => c.id)).toEqual(['T_1']);
  193. });
  194. });
  195. describe('assigning ProductVariant to Channels', () => {
  196. let product1: NonNullable<GetProductWithVariantsQuery['product']>;
  197. beforeAll(async () => {
  198. await adminClient.asSuperAdmin();
  199. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  200. const { product } = await adminClient.query(GetProductWithVariantsDocument, {
  201. id: 'T_2',
  202. });
  203. product1 = product!;
  204. });
  205. it(
  206. 'throws if attempting to assign ProductVariant to channel to which the admin has no access',
  207. assertThrowsWithMessage(async () => {
  208. await adminClient.asUserWithCredentials('admin2@test.com', 'test');
  209. await adminClient.query(AssignProductVariantsToChannelDocument, {
  210. input: {
  211. channelId: 'T_3',
  212. productVariantIds: [product1.variants[0].id],
  213. },
  214. });
  215. }, 'You are not currently authorized to perform this action'),
  216. );
  217. it('assigns ProductVariant to Channel and applies price factor', async () => {
  218. const PRICE_FACTOR = 0.5;
  219. await adminClient.asSuperAdmin();
  220. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  221. const { assignProductVariantsToChannel } = await adminClient.query(
  222. AssignProductVariantsToChannelDocument,
  223. {
  224. input: {
  225. channelId: 'T_3',
  226. productVariantIds: [product1.variants[0].id],
  227. priceFactor: PRICE_FACTOR,
  228. },
  229. },
  230. );
  231. expect(assignProductVariantsToChannel[0].channels.map(c => c.id).sort()).toEqual(['T_1', 'T_3']);
  232. adminClient.setChannelToken(THIRD_CHANNEL_TOKEN);
  233. const { product } = await adminClient.query(GetProductWithVariantsDocument, {
  234. id: product1.id,
  235. });
  236. expect(product!.channels.map(c => c.id).sort()).toEqual(['T_3']);
  237. // Third Channel is configured to include taxes in price, so they should be the same.
  238. expect(product!.variants.map(v => v.priceWithTax)).toEqual([
  239. Math.round(product1.variants[0].priceWithTax * PRICE_FACTOR),
  240. ]);
  241. // Third Channel has the default currency EUR
  242. expect(product!.variants.map(v => v.currencyCode)).toEqual(['EUR']);
  243. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  244. const { product: check } = await adminClient.query(GetProductWithVariantsDocument, {
  245. id: product1.id,
  246. });
  247. // from the default channel, all channels are visible
  248. expect(check?.channels.map(c => c.id).sort()).toEqual(['T_1', 'T_3']);
  249. expect(check?.variants[0].channels.map(c => c.id).sort()).toEqual(['T_1', 'T_3']);
  250. expect(check?.variants[1].channels.map(c => c.id).sort()).toEqual(['T_1']);
  251. });
  252. it('does not assign ProductVariant to same channel twice', async () => {
  253. await adminClient.asSuperAdmin();
  254. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  255. const { assignProductVariantsToChannel } = await adminClient.query(
  256. AssignProductVariantsToChannelDocument,
  257. {
  258. input: {
  259. channelId: 'T_3',
  260. productVariantIds: [product1.variants[0].id],
  261. },
  262. },
  263. );
  264. expect(assignProductVariantsToChannel[0].channels.map(c => c.id).sort()).toEqual(['T_1', 'T_3']);
  265. });
  266. it(
  267. 'throws if attempting to remove ProductVariant from default Channel',
  268. assertThrowsWithMessage(async () => {
  269. await adminClient.query(RemoveProductVariantsFromChannelDocument, {
  270. input: {
  271. productVariantIds: [product1.variants[0].id],
  272. channelId: 'T_1',
  273. },
  274. });
  275. }, 'Items cannot be removed from the default Channel'),
  276. );
  277. it('removes ProductVariant but not Product from Channel', async () => {
  278. await adminClient.asSuperAdmin();
  279. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  280. const { assignProductVariantsToChannel } = await adminClient.query(
  281. AssignProductVariantsToChannelDocument,
  282. {
  283. input: {
  284. channelId: 'T_3',
  285. productVariantIds: [product1.variants[1].id],
  286. },
  287. },
  288. );
  289. expect(assignProductVariantsToChannel[0].channels.map(c => c.id).sort()).toEqual(['T_1', 'T_3']);
  290. const { removeProductVariantsFromChannel } = await adminClient.query(
  291. RemoveProductVariantsFromChannelDocument,
  292. {
  293. input: {
  294. productVariantIds: [product1.variants[1].id],
  295. channelId: 'T_3',
  296. },
  297. },
  298. );
  299. expect(removeProductVariantsFromChannel[0].channels.map(c => c.id)).toEqual(['T_1']);
  300. const { product } = await adminClient.query(GetProductWithVariantsDocument, {
  301. id: product1.id,
  302. });
  303. expect(product!.channels.map(c => c.id).sort()).toEqual(['T_1', 'T_3']);
  304. });
  305. it('removes ProductVariant and Product from Channel', async () => {
  306. await adminClient.asSuperAdmin();
  307. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  308. const { removeProductVariantsFromChannel } = await adminClient.query(
  309. RemoveProductVariantsFromChannelDocument,
  310. {
  311. input: {
  312. productVariantIds: [product1.variants[0].id],
  313. channelId: 'T_3',
  314. },
  315. },
  316. );
  317. expect(removeProductVariantsFromChannel[0].channels.map(c => c.id)).toEqual(['T_1']);
  318. const { product } = await adminClient.query(GetProductWithVariantsDocument, {
  319. id: product1.id,
  320. });
  321. expect(product!.channels.map(c => c.id).sort()).toEqual(['T_1']);
  322. });
  323. });
  324. describe('creating Product in sub-channel', () => {
  325. let createdProduct: CreateProductMutation['createProduct'];
  326. let createdVariant: ProductVariantFragment;
  327. it('creates a Product in sub-channel', async () => {
  328. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  329. const { createProduct } = await adminClient.query(CreateProductDocument, {
  330. input: {
  331. translations: [
  332. {
  333. languageCode: LanguageCode.en,
  334. name: 'Channel Product',
  335. slug: 'channel-product',
  336. description: 'Channel product',
  337. },
  338. ],
  339. },
  340. });
  341. const { createProductVariants } = await adminClient.query(CreateProductVariantsDocument, {
  342. input: [
  343. {
  344. productId: createProduct.id,
  345. sku: 'PV1',
  346. optionIds: [],
  347. translations: [{ languageCode: LanguageCode.en, name: 'Variant 1' }],
  348. },
  349. ],
  350. });
  351. createdProduct = createProduct;
  352. createdVariant = createProductVariants[0]!;
  353. // from sub-channel, only that channel is visible
  354. expect(createdProduct.channels.map(c => c.id).sort()).toEqual(['T_2']);
  355. expect(createdVariant.channels.map(c => c.id).sort()).toEqual(['T_2']);
  356. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  357. const { product } = await adminClient.query(GetProductWithVariantsDocument, {
  358. id: createProduct.id,
  359. });
  360. // from the default channel, all channels are visible
  361. expect(product?.channels.map(c => c.id).sort()).toEqual(['T_1', 'T_2']);
  362. expect(product?.variants[0].channels.map(c => c.id).sort()).toEqual(['T_1', 'T_2']);
  363. });
  364. });
  365. describe('updating Product in sub-channel', () => {
  366. it(
  367. 'throws if attempting to update a Product which is not assigned to that Channel',
  368. assertThrowsWithMessage(async () => {
  369. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  370. await adminClient.query(UpdateProductDocument, {
  371. input: {
  372. id: 'T_2',
  373. translations: [{ languageCode: LanguageCode.en, name: 'xyz' }],
  374. },
  375. });
  376. }, 'No Product with the id "2" could be found'),
  377. );
  378. });
  379. describe('updating channel defaultCurrencyCode', () => {
  380. let secondChannelId: string;
  381. const channelGuard: ErrorResultGuard<ChannelFragment> = createErrorResultGuard(input => !!input.id);
  382. beforeAll(async () => {
  383. const { channels } = await adminClient.query(GetChannelsDocument);
  384. secondChannelId = channels.items.find(c => c.token === SECOND_CHANNEL_TOKEN)!.id;
  385. });
  386. it('updates variant prices from old default to new', async () => {
  387. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  388. const { productVariants } = await adminClient.query(GetProductVariantListDocument, {});
  389. expect(productVariants.items.map(i => i.currencyCode)).toEqual(['GBP']);
  390. const { updateChannel } = await adminClient.query(UpdateChannelDocument, {
  391. input: {
  392. id: secondChannelId,
  393. availableCurrencyCodes: [CurrencyCode.MYR, CurrencyCode.GBP, CurrencyCode.EUR],
  394. defaultCurrencyCode: CurrencyCode.MYR,
  395. },
  396. });
  397. channelGuard.assertSuccess(updateChannel);
  398. expect(updateChannel.defaultCurrencyCode).toBe(CurrencyCode.MYR);
  399. const { productVariants: variantsAfter } = await adminClient.query(
  400. GetProductVariantListDocument,
  401. {},
  402. );
  403. expect(variantsAfter.items.map(i => i.currencyCode)).toEqual(['MYR']);
  404. });
  405. it('does not change prices in other currencies', async () => {
  406. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  407. const { productVariants } = await adminClient.query(GetProductVariantListDocument, {});
  408. const { updateProductVariants } = await adminClient.query(UpdateProductVariantsDocument, {
  409. input: productVariants.items.map(i => ({
  410. id: i.id,
  411. prices: [
  412. { price: 100, currencyCode: CurrencyCode.GBP },
  413. { price: 200, currencyCode: CurrencyCode.MYR },
  414. { price: 300, currencyCode: CurrencyCode.EUR },
  415. ],
  416. })),
  417. });
  418. expect(updateProductVariants[0]?.prices.sort((a, b) => a.price - b.price)).toEqual([
  419. { currencyCode: 'GBP', price: 100 },
  420. { currencyCode: 'MYR', price: 200 },
  421. { currencyCode: 'EUR', price: 300 },
  422. ]);
  423. expect(updateProductVariants[0]?.currencyCode).toBe('MYR');
  424. await adminClient.query(UpdateChannelDocument, {
  425. input: {
  426. id: secondChannelId,
  427. availableCurrencyCodes: [
  428. CurrencyCode.MYR,
  429. CurrencyCode.GBP,
  430. CurrencyCode.EUR,
  431. CurrencyCode.AUD,
  432. ],
  433. defaultCurrencyCode: CurrencyCode.AUD,
  434. },
  435. });
  436. const { productVariants: after } = await adminClient.query(GetProductVariantListDocument, {});
  437. expect(after.items.map(i => i.currencyCode)).toEqual(['AUD']);
  438. expect(after.items[0]?.prices.sort((a, b) => a.price - b.price)).toEqual([
  439. { currencyCode: 'GBP', price: 100 },
  440. { currencyCode: 'AUD', price: 200 },
  441. { currencyCode: 'EUR', price: 300 },
  442. ]);
  443. });
  444. // https://github.com/vendure-ecommerce/vendure/issues/2391
  445. it('does not duplicate an existing price', async () => {
  446. await adminClient.query(UpdateChannelDocument, {
  447. input: {
  448. id: secondChannelId,
  449. defaultCurrencyCode: CurrencyCode.GBP,
  450. },
  451. });
  452. const { productVariants: after } = await adminClient.query(GetProductVariantListDocument, {});
  453. expect(after.items.map(i => i.currencyCode)).toEqual(['GBP']);
  454. expect(after.items[0]?.prices.sort((a, b) => a.price - b.price)).toEqual([
  455. { currencyCode: 'GBP', price: 100 },
  456. { currencyCode: 'AUD', price: 200 },
  457. { currencyCode: 'EUR', price: 300 },
  458. ]);
  459. });
  460. });
  461. });