promotion.e2e-spec.ts 15 KB

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