promotion.e2e-spec.ts 15 KB

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