channel.e2e-spec.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. /* eslint-disable @typescript-eslint/no-non-null-assertion */
  2. import {
  3. CurrencyCode,
  4. DeletionResult,
  5. ErrorCode,
  6. LanguageCode,
  7. Permission,
  8. } from '@vendure/common/lib/generated-types';
  9. import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
  10. import {
  11. createErrorResultGuard,
  12. createTestEnvironment,
  13. E2E_DEFAULT_CHANNEL_TOKEN,
  14. ErrorResultGuard,
  15. } from '@vendure/testing';
  16. import { ResultOf } from 'gql.tada';
  17. import path from 'path';
  18. import { afterAll, beforeAll, describe, expect, it } from 'vitest';
  19. import { initialData } from '../../../e2e-common/e2e-initial-data';
  20. import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
  21. import { channelFragment } from './graphql/fragments-admin';
  22. import { FragmentOf, graphql } from './graphql/graphql-admin';
  23. import {
  24. assignProductToChannelDocument,
  25. createAdministratorDocument,
  26. createChannelDocument,
  27. createRoleDocument,
  28. getChannelsDocument,
  29. getCustomerListDocument,
  30. getProductListDocument,
  31. getProductWithVariantsDocument,
  32. MeDocument,
  33. updateChannelDocument,
  34. } from './graphql/shared-definitions';
  35. import { getActiveOrderDocument } from './graphql/shop-definitions';
  36. import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
  37. describe('Channels', () => {
  38. const { server, adminClient, shopClient } = createTestEnvironment(testConfig());
  39. const SECOND_CHANNEL_TOKEN = 'second_channel_token';
  40. let secondChannelAdminRole: ResultOf<typeof createRoleDocument>['createRole'];
  41. let customerUser: ResultOf<typeof getCustomerListDocument>['customers']['items'][number];
  42. type ChannelFragment = FragmentOf<typeof channelFragment>;
  43. const channelGuard: ErrorResultGuard<ChannelFragment> = createErrorResultGuard(
  44. input => !!input.defaultLanguageCode,
  45. );
  46. beforeAll(async () => {
  47. await server.init({
  48. initialData,
  49. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  50. customerCount: 1,
  51. });
  52. await adminClient.asSuperAdmin();
  53. const { customers } = await adminClient.query(getCustomerListDocument, {
  54. options: { take: 1 },
  55. });
  56. customerUser = customers.items[0];
  57. }, TEST_SETUP_TIMEOUT_MS);
  58. afterAll(async () => {
  59. await server.destroy();
  60. });
  61. it('createChannel returns error result defaultLanguageCode not available', async () => {
  62. const { createChannel } = await adminClient.query(createChannelDocument, {
  63. input: {
  64. code: 'second-channel',
  65. token: SECOND_CHANNEL_TOKEN,
  66. defaultLanguageCode: LanguageCode.zh,
  67. currencyCode: CurrencyCode.GBP,
  68. pricesIncludeTax: true,
  69. defaultShippingZoneId: 'T_1',
  70. defaultTaxZoneId: 'T_1',
  71. },
  72. });
  73. channelGuard.assertErrorResult(createChannel);
  74. expect(createChannel.message).toBe(
  75. 'Language "zh" is not available. First enable it via GlobalSettings and try again',
  76. );
  77. expect(createChannel.errorCode).toBe(ErrorCode.LANGUAGE_NOT_AVAILABLE_ERROR);
  78. expect(createChannel.languageCode).toBe('zh');
  79. });
  80. it('create a new Channel', async () => {
  81. const { createChannel } = await adminClient.query(createChannelDocument, {
  82. input: {
  83. code: 'second-channel',
  84. token: SECOND_CHANNEL_TOKEN,
  85. defaultLanguageCode: LanguageCode.en,
  86. currencyCode: CurrencyCode.GBP,
  87. pricesIncludeTax: true,
  88. defaultShippingZoneId: 'T_1',
  89. defaultTaxZoneId: 'T_1',
  90. },
  91. });
  92. channelGuard.assertSuccess(createChannel);
  93. expect(createChannel).toEqual({
  94. id: 'T_2',
  95. code: 'second-channel',
  96. token: SECOND_CHANNEL_TOKEN,
  97. availableCurrencyCodes: ['GBP'],
  98. currencyCode: 'GBP',
  99. defaultCurrencyCode: 'GBP',
  100. defaultLanguageCode: 'en',
  101. defaultShippingZone: {
  102. id: 'T_1',
  103. },
  104. defaultTaxZone: {
  105. id: 'T_1',
  106. },
  107. pricesIncludeTax: true,
  108. });
  109. });
  110. // it('update currencyCode', async () => {
  111. // const { updateChannel } = await adminClient.query<
  112. // Codegen.UpdateChannelMutation,
  113. // Codegen.UpdateChannelMutationVariables
  114. // >(updateChannelDocument, {
  115. // input: {
  116. // id: 'T_1',
  117. // currencyCode: CurrencyCode.MYR,
  118. // },
  119. // });
  120. // channelGuard.assertSuccess(updateChannel);
  121. // expect(updateChannel.currencyCode).toBe('MYR');
  122. // });
  123. it('superadmin has all permissions on new channel', async () => {
  124. const { me } = await adminClient.query(MeDocument);
  125. expect(me!.channels.length).toBe(2);
  126. const secondChannelData = me!.channels.find(c => c.token === SECOND_CHANNEL_TOKEN);
  127. const nonOwnerPermissions = Object.values(Permission).filter(
  128. p => p !== Permission.Owner && p !== Permission.Public,
  129. );
  130. expect(secondChannelData!.permissions.sort()).toEqual(nonOwnerPermissions);
  131. });
  132. it('customer has Authenticated permission on new channel', async () => {
  133. await shopClient.asUserWithCredentials(customerUser.emailAddress, 'test');
  134. const { me } = await shopClient.query(MeDocument);
  135. expect(me!.channels.length).toBe(2);
  136. expect(me!.channels).toEqual([
  137. {
  138. code: DEFAULT_CHANNEL_CODE,
  139. permissions: ['Authenticated'],
  140. token: E2E_DEFAULT_CHANNEL_TOKEN,
  141. },
  142. {
  143. code: 'second-channel',
  144. permissions: ['Authenticated'],
  145. token: SECOND_CHANNEL_TOKEN,
  146. },
  147. ]);
  148. });
  149. it('createRole on second Channel', async () => {
  150. const { createRole } = await adminClient.query(createRoleDocument, {
  151. input: {
  152. description: 'second channel admin',
  153. code: 'second-channel-admin',
  154. channelIds: ['T_2'],
  155. permissions: [
  156. Permission.ReadCatalog,
  157. Permission.ReadSettings,
  158. Permission.ReadAdministrator,
  159. Permission.CreateAdministrator,
  160. Permission.UpdateAdministrator,
  161. ],
  162. },
  163. });
  164. expect(createRole.channels).toEqual([
  165. {
  166. id: 'T_2',
  167. code: 'second-channel',
  168. token: SECOND_CHANNEL_TOKEN,
  169. },
  170. ]);
  171. secondChannelAdminRole = createRole;
  172. });
  173. it('createAdministrator with second-channel-admin role', async () => {
  174. const { createAdministrator } = await adminClient.query(createAdministratorDocument, {
  175. input: {
  176. firstName: 'Admin',
  177. lastName: 'Two',
  178. emailAddress: 'admin2@test.com',
  179. password: 'test',
  180. roleIds: [secondChannelAdminRole.id],
  181. },
  182. });
  183. expect(createAdministrator.user.roles.map(r => r.description)).toEqual(['second channel admin']);
  184. });
  185. it(
  186. 'cannot create role on channel for which admin does not have CreateAdministrator permission',
  187. assertThrowsWithMessage(async () => {
  188. await adminClient.asUserWithCredentials('admin2@test.com', 'test');
  189. await adminClient.query(createRoleDocument, {
  190. input: {
  191. description: 'read default channel catalog',
  192. code: 'read default channel catalog',
  193. channelIds: ['T_1'],
  194. permissions: [Permission.ReadCatalog],
  195. },
  196. });
  197. }, 'You are not currently authorized to perform this action'),
  198. );
  199. it('can create role on channel for which admin has CreateAdministrator permission', async () => {
  200. const { createRole } = await adminClient.query(createRoleDocument, {
  201. input: {
  202. description: 'read second channel catalog',
  203. code: 'read-second-channel-catalog',
  204. channelIds: ['T_2'],
  205. permissions: [Permission.ReadCatalog],
  206. },
  207. });
  208. expect(createRole.channels).toEqual([
  209. {
  210. id: 'T_2',
  211. code: 'second-channel',
  212. token: SECOND_CHANNEL_TOKEN,
  213. },
  214. ]);
  215. });
  216. it('createRole with no channelId implicitly uses active channel', async () => {
  217. await adminClient.asSuperAdmin();
  218. const { createRole } = await adminClient.query(createRoleDocument, {
  219. input: {
  220. description: 'update second channel catalog',
  221. code: 'update-second-channel-catalog',
  222. permissions: [Permission.UpdateCatalog],
  223. },
  224. });
  225. expect(createRole.channels).toEqual([
  226. {
  227. id: 'T_2',
  228. code: 'second-channel',
  229. token: SECOND_CHANNEL_TOKEN,
  230. },
  231. ]);
  232. });
  233. describe('setting defaultLanguage', () => {
  234. it('returns error result if languageCode not in availableLanguages', async () => {
  235. await adminClient.asSuperAdmin();
  236. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  237. const { updateChannel } = await adminClient.query(updateChannelDocument, {
  238. input: {
  239. id: 'T_1',
  240. defaultLanguageCode: LanguageCode.zh,
  241. },
  242. });
  243. channelGuard.assertErrorResult(updateChannel);
  244. expect(updateChannel.message).toBe(
  245. 'Language "zh" is not available. First enable it via GlobalSettings and try again',
  246. );
  247. expect(updateChannel.errorCode).toBe(ErrorCode.LANGUAGE_NOT_AVAILABLE_ERROR);
  248. expect(updateChannel.languageCode).toBe('zh');
  249. });
  250. it('allows setting to an available language', async () => {
  251. await adminClient.asSuperAdmin();
  252. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  253. await adminClient.query(updateGlobalLanguagesDocument, {
  254. input: {
  255. availableLanguages: [LanguageCode.en, LanguageCode.zh],
  256. },
  257. });
  258. const { updateChannel } = await adminClient.query(updateChannelDocument, {
  259. input: {
  260. id: 'T_1',
  261. defaultLanguageCode: LanguageCode.zh,
  262. },
  263. });
  264. channelGuard.assertSuccess(updateChannel);
  265. expect(updateChannel.defaultLanguageCode).toBe(LanguageCode.zh);
  266. });
  267. });
  268. it('deleteChannel', async () => {
  269. const PROD_ID = 'T_1';
  270. await adminClient.asSuperAdmin();
  271. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  272. const { assignProductsToChannel } = await adminClient.query(assignProductToChannelDocument, {
  273. input: {
  274. channelId: 'T_2',
  275. productIds: [PROD_ID],
  276. },
  277. });
  278. expect(assignProductsToChannel[0].channels.map(c => c.id).sort()).toEqual(['T_1', 'T_2']);
  279. // create a Session on the Channel to be deleted to ensure it gets cleaned up
  280. shopClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  281. await shopClient.query(getActiveOrderDocument);
  282. const { deleteChannel } = await adminClient.query(deleteChannelDocument, {
  283. id: 'T_2',
  284. });
  285. expect(deleteChannel.result).toBe(DeletionResult.DELETED);
  286. const { channels } = await adminClient.query(getChannelsDocument);
  287. expect(channels.items.map(c => c.id).sort()).toEqual(['T_1']);
  288. const { product } = await adminClient.query(getProductWithVariantsDocument, {
  289. id: PROD_ID,
  290. });
  291. expect(product!.channels.map(c => c.id)).toEqual(['T_1']);
  292. });
  293. it('Fail to delete the default channel', async () => {
  294. await adminClient.asSuperAdmin();
  295. const defaultChannelId = (await adminClient.query(getChannelsDocument)).channels.items.find(
  296. channel => channel.code === DEFAULT_CHANNEL_CODE,
  297. )?.id;
  298. expect(defaultChannelId).not.toBeUndefined();
  299. const mutation = await adminClient.query(deleteChannelDocument, { id: defaultChannelId! });
  300. expect(mutation.deleteChannel).toEqual({
  301. result: DeletionResult.NOT_DELETED,
  302. message: 'The default Channel cannot be deleted',
  303. });
  304. });
  305. describe('currencyCode support', () => {
  306. beforeAll(async () => {
  307. await adminClient.asSuperAdmin();
  308. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  309. });
  310. it('initial currencyCode values', async () => {
  311. const { channel } = await adminClient.query(getChannelDocument, {
  312. id: 'T_1',
  313. });
  314. expect(channel?.defaultCurrencyCode).toBe('USD');
  315. expect(channel?.availableCurrencyCodes).toEqual(['USD']);
  316. });
  317. it('setting defaultCurrencyCode adds it to availableCurrencyCodes', async () => {
  318. const { updateChannel } = await adminClient.query(updateChannelDocument, {
  319. input: {
  320. id: 'T_1',
  321. defaultCurrencyCode: CurrencyCode.MYR,
  322. },
  323. });
  324. channelGuard.assertSuccess(updateChannel);
  325. expect(updateChannel.defaultCurrencyCode).toBe('MYR');
  326. expect(updateChannel.currencyCode).toBe('MYR');
  327. expect(updateChannel.availableCurrencyCodes).toEqual(['USD', 'MYR']);
  328. });
  329. it('setting defaultCurrencyCode adds it to availableCurrencyCodes 2', async () => {
  330. // As above, but this time we set the availableCurrencyCodes explicitly
  331. // to exclude the defaultCurrencyCode
  332. const { updateChannel } = await adminClient.query(updateChannelDocument, {
  333. input: {
  334. id: 'T_1',
  335. defaultCurrencyCode: CurrencyCode.AUD,
  336. availableCurrencyCodes: [CurrencyCode.GBP],
  337. },
  338. });
  339. channelGuard.assertSuccess(updateChannel);
  340. expect(updateChannel.defaultCurrencyCode).toBe('AUD');
  341. expect(updateChannel.currencyCode).toBe('AUD');
  342. expect(updateChannel.availableCurrencyCodes).toEqual(['GBP', 'AUD']);
  343. });
  344. it(
  345. 'cannot remove the defaultCurrencyCode from availableCurrencyCodes',
  346. assertThrowsWithMessage(async () => {
  347. await adminClient.query(updateChannelDocument, {
  348. input: {
  349. id: 'T_1',
  350. availableCurrencyCodes: [CurrencyCode.GBP],
  351. },
  352. });
  353. }, 'availableCurrencyCodes must include the defaultCurrencyCode (AUD)'),
  354. );
  355. it(
  356. 'specifying an unsupported currencyCode throws',
  357. assertThrowsWithMessage(async () => {
  358. await adminClient.query(
  359. getProductListDocument,
  360. {
  361. options: {
  362. take: 1,
  363. },
  364. },
  365. { currencyCode: 'JPY' },
  366. );
  367. }, 'The currency "JPY" is not available in the current Channel'),
  368. );
  369. });
  370. });
  371. const deleteChannelDocument = graphql(`
  372. mutation DeleteChannel($id: ID!) {
  373. deleteChannel(id: $id) {
  374. message
  375. result
  376. }
  377. }
  378. `);
  379. const getChannelDocument = graphql(`
  380. query GetChannel($id: ID!) {
  381. channel(id: $id) {
  382. id
  383. code
  384. token
  385. defaultCurrencyCode
  386. availableCurrencyCodes
  387. defaultLanguageCode
  388. availableLanguageCodes
  389. outOfStockThreshold
  390. pricesIncludeTax
  391. }
  392. }
  393. `);
  394. const updateGlobalLanguagesDocument = graphql(`
  395. mutation UpdateGlobalLanguages($input: UpdateGlobalSettingsInput!) {
  396. updateGlobalSettings(input: $input) {
  397. ... on GlobalSettings {
  398. id
  399. availableLanguages
  400. }
  401. }
  402. }
  403. `);