promotion.e2e-spec.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. import { pick } from '@vendure/common/lib/pick';
  2. import { PromotionAction, PromotionCondition, PromotionOrderAction } from '@vendure/core';
  3. import {
  4. createErrorResultGuard,
  5. createTestEnvironment,
  6. E2E_DEFAULT_CHANNEL_TOKEN,
  7. ErrorResultGuard,
  8. } from '@vendure/testing';
  9. import gql from 'graphql-tag';
  10. import path from 'path';
  11. import { initialData } from '../../../e2e-common/e2e-initial-data';
  12. import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
  13. import { PROMOTION_FRAGMENT } from './graphql/fragments';
  14. import {
  15. AssignPromotionToChannel,
  16. ChannelFragment,
  17. CreateChannel,
  18. CreatePromotion,
  19. CurrencyCode,
  20. DeletePromotion,
  21. DeletionResult,
  22. ErrorCode,
  23. GetAdjustmentOperations,
  24. GetPromotion,
  25. GetPromotionList,
  26. LanguageCode,
  27. Promotion,
  28. PromotionFragment,
  29. RemovePromotionFromChannel,
  30. UpdatePromotion,
  31. } from './graphql/generated-e2e-admin-types';
  32. import {
  33. ASSIGN_PROMOTIONS_TO_CHANNEL,
  34. CREATE_CHANNEL,
  35. CREATE_PROMOTION,
  36. DELETE_PROMOTION,
  37. REMOVE_PROMOTIONS_FROM_CHANNEL,
  38. } from './graphql/shared-definitions';
  39. import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
  40. // tslint:disable:no-non-null-assertion
  41. describe('Promotion resolver', () => {
  42. const promoCondition = generateTestCondition('promo_condition');
  43. const promoCondition2 = generateTestCondition('promo_condition2');
  44. const promoAction = generateTestAction('promo_action');
  45. const { server, adminClient, shopClient } = createTestEnvironment({
  46. ...testConfig(),
  47. promotionOptions: {
  48. promotionConditions: [promoCondition, promoCondition2],
  49. promotionActions: [promoAction],
  50. },
  51. });
  52. const snapshotProps: Array<keyof Promotion.Fragment> = [
  53. 'name',
  54. 'actions',
  55. 'conditions',
  56. 'enabled',
  57. 'couponCode',
  58. 'startsAt',
  59. 'endsAt',
  60. ];
  61. let promotion: Promotion.Fragment;
  62. const promotionGuard: ErrorResultGuard<PromotionFragment> = createErrorResultGuard(
  63. input => !!input.couponCode,
  64. );
  65. beforeAll(async () => {
  66. await server.init({
  67. initialData,
  68. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  69. customerCount: 1,
  70. });
  71. await adminClient.asSuperAdmin();
  72. }, TEST_SETUP_TIMEOUT_MS);
  73. afterAll(async () => {
  74. await server.destroy();
  75. });
  76. it('createPromotion', async () => {
  77. const { createPromotion } = await adminClient.query<
  78. CreatePromotion.Mutation,
  79. CreatePromotion.Variables
  80. >(CREATE_PROMOTION, {
  81. input: {
  82. name: 'test promotion',
  83. enabled: true,
  84. couponCode: 'TEST123',
  85. startsAt: new Date('2019-10-30T00:00:00.000Z'),
  86. endsAt: new Date('2019-12-01T00:00:00.000Z'),
  87. conditions: [
  88. {
  89. code: promoCondition.code,
  90. arguments: [{ name: 'arg', value: '500' }],
  91. },
  92. ],
  93. actions: [
  94. {
  95. code: promoAction.code,
  96. arguments: [
  97. {
  98. name: 'facetValueIds',
  99. value: '["T_1"]',
  100. },
  101. ],
  102. },
  103. ],
  104. },
  105. });
  106. promotionGuard.assertSuccess(createPromotion);
  107. promotion = createPromotion;
  108. expect(pick(promotion, snapshotProps)).toMatchSnapshot();
  109. });
  110. it('createPromotion return error result with empty conditions and no couponCode', async () => {
  111. const { createPromotion } = await adminClient.query<
  112. CreatePromotion.Mutation,
  113. CreatePromotion.Variables
  114. >(CREATE_PROMOTION, {
  115. input: {
  116. name: 'bad promotion',
  117. enabled: true,
  118. conditions: [],
  119. actions: [
  120. {
  121. code: promoAction.code,
  122. arguments: [
  123. {
  124. name: 'facetValueIds',
  125. value: '["T_1"]',
  126. },
  127. ],
  128. },
  129. ],
  130. },
  131. });
  132. promotionGuard.assertErrorResult(createPromotion);
  133. expect(createPromotion.message).toBe(
  134. 'A Promotion must have either at least one condition or a coupon code set',
  135. );
  136. expect(createPromotion.errorCode).toBe(ErrorCode.MISSING_CONDITIONS_ERROR);
  137. });
  138. it('updatePromotion', async () => {
  139. const { updatePromotion } = await adminClient.query<
  140. UpdatePromotion.Mutation,
  141. UpdatePromotion.Variables
  142. >(UPDATE_PROMOTION, {
  143. input: {
  144. id: promotion.id,
  145. couponCode: 'TEST1235',
  146. startsAt: new Date('2019-05-30T22:00:00.000Z'),
  147. endsAt: new Date('2019-06-01T22:00:00.000Z'),
  148. conditions: [
  149. {
  150. code: promoCondition.code,
  151. arguments: [{ name: 'arg', value: '90' }],
  152. },
  153. {
  154. code: promoCondition2.code,
  155. arguments: [{ name: 'arg', value: '10' }],
  156. },
  157. ],
  158. },
  159. });
  160. promotionGuard.assertSuccess(updatePromotion);
  161. expect(pick(updatePromotion, snapshotProps)).toMatchSnapshot();
  162. });
  163. it('updatePromotion return error result with empty conditions and no couponCode', async () => {
  164. const { updatePromotion } = await adminClient.query<
  165. UpdatePromotion.Mutation,
  166. UpdatePromotion.Variables
  167. >(UPDATE_PROMOTION, {
  168. input: {
  169. id: promotion.id,
  170. couponCode: '',
  171. conditions: [],
  172. },
  173. });
  174. promotionGuard.assertErrorResult(updatePromotion);
  175. expect(updatePromotion.message).toBe(
  176. 'A Promotion must have either at least one condition or a coupon code set',
  177. );
  178. expect(updatePromotion.errorCode).toBe(ErrorCode.MISSING_CONDITIONS_ERROR);
  179. });
  180. it('promotion', async () => {
  181. const result = await adminClient.query<GetPromotion.Query, GetPromotion.Variables>(GET_PROMOTION, {
  182. id: promotion.id,
  183. });
  184. expect(result.promotion!.name).toBe(promotion.name);
  185. });
  186. it('promotions', async () => {
  187. const result = await adminClient.query<GetPromotionList.Query, GetPromotionList.Variables>(
  188. GET_PROMOTION_LIST,
  189. {},
  190. );
  191. expect(result.promotions.totalItems).toBe(1);
  192. expect(result.promotions.items[0].name).toBe('test promotion');
  193. });
  194. it('adjustmentOperations', async () => {
  195. const result = await adminClient.query<
  196. GetAdjustmentOperations.Query,
  197. GetAdjustmentOperations.Variables
  198. >(GET_ADJUSTMENT_OPERATIONS);
  199. expect(result.promotionActions).toMatchSnapshot();
  200. expect(result.promotionConditions).toMatchSnapshot();
  201. });
  202. describe('channels', () => {
  203. const SECOND_CHANNEL_TOKEN = 'SECOND_CHANNEL_TOKEN';
  204. let secondChannel: ChannelFragment;
  205. beforeAll(async () => {
  206. const { createChannel } = await adminClient.query<
  207. CreateChannel.Mutation,
  208. CreateChannel.Variables
  209. >(CREATE_CHANNEL, {
  210. input: {
  211. code: 'second-channel',
  212. token: SECOND_CHANNEL_TOKEN,
  213. defaultLanguageCode: LanguageCode.en,
  214. pricesIncludeTax: true,
  215. currencyCode: CurrencyCode.EUR,
  216. defaultTaxZoneId: 'T_1',
  217. defaultShippingZoneId: 'T_2',
  218. },
  219. });
  220. secondChannel = createChannel as any;
  221. });
  222. it('does not list Promotions not in active channel', async () => {
  223. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  224. const { promotions } = await adminClient.query<GetPromotionList.Query>(GET_PROMOTION_LIST);
  225. expect(promotions.totalItems).toBe(0);
  226. expect(promotions.items).toEqual([]);
  227. });
  228. it('does not return Promotion not in active channel', async () => {
  229. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  230. const { promotion: result } = await adminClient.query<GetPromotion.Query, GetPromotion.Variables>(
  231. GET_PROMOTION,
  232. {
  233. id: promotion.id,
  234. },
  235. );
  236. expect(result).toBeNull();
  237. });
  238. it('assignPromotionsToChannel', async () => {
  239. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  240. const { assignPromotionsToChannel } = await adminClient.query<
  241. AssignPromotionToChannel.Mutation,
  242. AssignPromotionToChannel.Variables
  243. >(ASSIGN_PROMOTIONS_TO_CHANNEL, {
  244. input: {
  245. channelId: secondChannel.id,
  246. promotionIds: [promotion.id],
  247. },
  248. });
  249. expect(assignPromotionsToChannel).toEqual([{ id: promotion.id, name: promotion.name }]);
  250. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  251. const { promotion: result } = await adminClient.query<GetPromotion.Query, GetPromotion.Variables>(
  252. GET_PROMOTION,
  253. {
  254. id: promotion.id,
  255. },
  256. );
  257. expect(result?.id).toBe(promotion.id);
  258. const { promotions } = await adminClient.query<GetPromotionList.Query>(GET_PROMOTION_LIST);
  259. expect(promotions.totalItems).toBe(1);
  260. expect(promotions.items.map(pick(['id']))).toEqual([{ id: promotion.id }]);
  261. });
  262. it('removePromotionsFromChannel', async () => {
  263. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  264. const { removePromotionsFromChannel } = await adminClient.query<
  265. RemovePromotionFromChannel.Mutation,
  266. RemovePromotionFromChannel.Variables
  267. >(REMOVE_PROMOTIONS_FROM_CHANNEL, {
  268. input: {
  269. channelId: secondChannel.id,
  270. promotionIds: [promotion.id],
  271. },
  272. });
  273. expect(removePromotionsFromChannel).toEqual([{ id: promotion.id, name: promotion.name }]);
  274. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  275. const { promotion: result } = await adminClient.query<GetPromotion.Query, GetPromotion.Variables>(
  276. GET_PROMOTION,
  277. {
  278. id: promotion.id,
  279. },
  280. );
  281. expect(result).toBeNull();
  282. const { promotions } = await adminClient.query<GetPromotionList.Query>(GET_PROMOTION_LIST);
  283. expect(promotions.totalItems).toBe(0);
  284. });
  285. });
  286. describe('deletion', () => {
  287. let allPromotions: GetPromotionList.Items[];
  288. let promotionToDelete: GetPromotionList.Items;
  289. beforeAll(async () => {
  290. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  291. const result = await adminClient.query<GetPromotionList.Query>(GET_PROMOTION_LIST);
  292. allPromotions = result.promotions.items;
  293. });
  294. it('deletes a promotion', async () => {
  295. promotionToDelete = allPromotions[0];
  296. const result = await adminClient.query<DeletePromotion.Mutation, DeletePromotion.Variables>(
  297. DELETE_PROMOTION,
  298. { id: promotionToDelete.id },
  299. );
  300. expect(result.deletePromotion).toEqual({ result: DeletionResult.DELETED });
  301. });
  302. it('cannot get a deleted promotion', async () => {
  303. const result = await adminClient.query<GetPromotion.Query, GetPromotion.Variables>(
  304. GET_PROMOTION,
  305. {
  306. id: promotionToDelete.id,
  307. },
  308. );
  309. expect(result.promotion).toBe(null);
  310. });
  311. it('deleted promotion omitted from list', async () => {
  312. const result = await adminClient.query<GetPromotionList.Query>(GET_PROMOTION_LIST);
  313. expect(result.promotions.items.length).toBe(allPromotions.length - 1);
  314. expect(result.promotions.items.map(c => c.id).includes(promotionToDelete.id)).toBe(false);
  315. });
  316. it(
  317. 'updatePromotion throws for deleted promotion',
  318. assertThrowsWithMessage(
  319. () =>
  320. adminClient.query<UpdatePromotion.Mutation, UpdatePromotion.Variables>(UPDATE_PROMOTION, {
  321. input: {
  322. id: promotionToDelete.id,
  323. enabled: false,
  324. },
  325. }),
  326. `No Promotion with the id '1' could be found`,
  327. ),
  328. );
  329. });
  330. });
  331. function generateTestCondition(code: string): PromotionCondition<any> {
  332. return new PromotionCondition({
  333. code,
  334. description: [{ languageCode: LanguageCode.en, value: `description for ${code}` }],
  335. args: { arg: { type: 'int' } },
  336. check: (order, args) => true,
  337. });
  338. }
  339. function generateTestAction(code: string): PromotionAction<any> {
  340. return new PromotionOrderAction({
  341. code,
  342. description: [{ languageCode: LanguageCode.en, value: `description for ${code}` }],
  343. args: { facetValueIds: { type: 'ID', list: true } },
  344. execute: (order, args) => {
  345. return 42;
  346. },
  347. });
  348. }
  349. export const GET_PROMOTION_LIST = gql`
  350. query GetPromotionList($options: PromotionListOptions) {
  351. promotions(options: $options) {
  352. items {
  353. ...Promotion
  354. }
  355. totalItems
  356. }
  357. }
  358. ${PROMOTION_FRAGMENT}
  359. `;
  360. export const GET_PROMOTION = gql`
  361. query GetPromotion($id: ID!) {
  362. promotion(id: $id) {
  363. ...Promotion
  364. }
  365. }
  366. ${PROMOTION_FRAGMENT}
  367. `;
  368. export const UPDATE_PROMOTION = gql`
  369. mutation UpdatePromotion($input: UpdatePromotionInput!) {
  370. updatePromotion(input: $input) {
  371. ...Promotion
  372. ... on ErrorResult {
  373. errorCode
  374. message
  375. }
  376. }
  377. }
  378. ${PROMOTION_FRAGMENT}
  379. `;
  380. export const CONFIGURABLE_DEF_FRAGMENT = gql`
  381. fragment ConfigurableOperationDef on ConfigurableOperationDefinition {
  382. args {
  383. name
  384. type
  385. ui
  386. }
  387. code
  388. description
  389. }
  390. `;
  391. export const GET_ADJUSTMENT_OPERATIONS = gql`
  392. query GetAdjustmentOperations {
  393. promotionActions {
  394. ...ConfigurableOperationDef
  395. }
  396. promotionConditions {
  397. ...ConfigurableOperationDef
  398. }
  399. }
  400. ${CONFIGURABLE_DEF_FRAGMENT}
  401. `;