shop-auth.e2e-spec.ts 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044
  1. /* tslint:disable:no-non-null-assertion */
  2. import { OnModuleInit } from '@nestjs/common';
  3. import { ErrorCode, RegisterCustomerInput } from '@vendure/common/lib/generated-shop-types';
  4. import { pick } from '@vendure/common/lib/pick';
  5. import {
  6. AccountRegistrationEvent,
  7. EventBus,
  8. EventBusModule,
  9. IdentifierChangeEvent,
  10. IdentifierChangeRequestEvent,
  11. mergeConfig,
  12. PasswordResetEvent,
  13. VendurePlugin,
  14. } from '@vendure/core';
  15. import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
  16. import { DocumentNode } from 'graphql';
  17. import gql from 'graphql-tag';
  18. import path from 'path';
  19. import { initialData } from '../../../e2e-common/e2e-initial-data';
  20. import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
  21. import {
  22. CreateAdministrator,
  23. CreateRole,
  24. GetCustomer,
  25. GetCustomerHistory,
  26. GetCustomerList,
  27. HistoryEntryType,
  28. Permission,
  29. } from './graphql/generated-e2e-admin-types';
  30. import {
  31. CurrentUserShopFragment,
  32. GetActiveCustomer,
  33. RefreshToken,
  34. Register,
  35. RequestPasswordReset,
  36. RequestUpdateEmailAddress,
  37. ResetPassword,
  38. UpdateEmailAddress,
  39. Verify,
  40. } from './graphql/generated-e2e-shop-types';
  41. import {
  42. CREATE_ADMINISTRATOR,
  43. CREATE_ROLE,
  44. GET_CUSTOMER,
  45. GET_CUSTOMER_HISTORY,
  46. GET_CUSTOMER_LIST,
  47. } from './graphql/shared-definitions';
  48. import {
  49. GET_ACTIVE_CUSTOMER,
  50. REFRESH_TOKEN,
  51. REGISTER_ACCOUNT,
  52. REQUEST_PASSWORD_RESET,
  53. REQUEST_UPDATE_EMAIL_ADDRESS,
  54. RESET_PASSWORD,
  55. UPDATE_EMAIL_ADDRESS,
  56. VERIFY_EMAIL,
  57. } from './graphql/shop-definitions';
  58. import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
  59. let sendEmailFn: jest.Mock;
  60. /**
  61. * This mock plugin simulates an EmailPlugin which would send emails
  62. * on the registration & password reset events.
  63. */
  64. @VendurePlugin({
  65. imports: [EventBusModule],
  66. })
  67. class TestEmailPlugin implements OnModuleInit {
  68. constructor(private eventBus: EventBus) {}
  69. onModuleInit() {
  70. this.eventBus.ofType(AccountRegistrationEvent).subscribe(event => {
  71. sendEmailFn(event);
  72. });
  73. this.eventBus.ofType(PasswordResetEvent).subscribe(event => {
  74. sendEmailFn(event);
  75. });
  76. this.eventBus.ofType(IdentifierChangeRequestEvent).subscribe(event => {
  77. sendEmailFn(event);
  78. });
  79. this.eventBus.ofType(IdentifierChangeEvent).subscribe(event => {
  80. sendEmailFn(event);
  81. });
  82. }
  83. }
  84. const successErrorGuard: ErrorResultGuard<{ success: boolean }> = createErrorResultGuard(
  85. input => input.success != null,
  86. );
  87. const currentUserErrorGuard: ErrorResultGuard<CurrentUserShopFragment> = createErrorResultGuard(
  88. input => input.identifier != null,
  89. );
  90. describe('Shop auth & accounts', () => {
  91. const { server, adminClient, shopClient } = createTestEnvironment(
  92. mergeConfig(testConfig(), {
  93. plugins: [TestEmailPlugin as any],
  94. }),
  95. );
  96. beforeAll(async () => {
  97. await server.init({
  98. initialData,
  99. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  100. customerCount: 2,
  101. });
  102. await adminClient.asSuperAdmin();
  103. }, TEST_SETUP_TIMEOUT_MS);
  104. afterAll(async () => {
  105. await server.destroy();
  106. });
  107. describe('customer account creation with deferred password', () => {
  108. const password = 'password';
  109. const emailAddress = 'test1@test.com';
  110. let verificationToken: string;
  111. let newCustomerId: string;
  112. beforeEach(() => {
  113. sendEmailFn = jest.fn();
  114. });
  115. it('does not return error result on email address conflict', async () => {
  116. // To prevent account enumeration attacks
  117. const { customers } = await adminClient.query<GetCustomerList.Query>(GET_CUSTOMER_LIST);
  118. const input: RegisterCustomerInput = {
  119. firstName: 'Duplicate',
  120. lastName: 'Person',
  121. phoneNumber: '123456',
  122. emailAddress: customers.items[0].emailAddress,
  123. };
  124. const { registerCustomerAccount } = await shopClient.query<Register.Mutation, Register.Variables>(
  125. REGISTER_ACCOUNT,
  126. {
  127. input,
  128. },
  129. );
  130. successErrorGuard.assertSuccess(registerCustomerAccount);
  131. });
  132. it('register a new account without password', async () => {
  133. const verificationTokenPromise = getVerificationTokenPromise();
  134. const input: RegisterCustomerInput = {
  135. firstName: 'Sean',
  136. lastName: 'Tester',
  137. phoneNumber: '123456',
  138. emailAddress,
  139. };
  140. const { registerCustomerAccount } = await shopClient.query<Register.Mutation, Register.Variables>(
  141. REGISTER_ACCOUNT,
  142. {
  143. input,
  144. },
  145. );
  146. successErrorGuard.assertSuccess(registerCustomerAccount);
  147. verificationToken = await verificationTokenPromise;
  148. expect(registerCustomerAccount.success).toBe(true);
  149. expect(sendEmailFn).toHaveBeenCalled();
  150. expect(verificationToken).toBeDefined();
  151. const { customers } = await adminClient.query<GetCustomerList.Query, GetCustomerList.Variables>(
  152. GET_CUSTOMER_LIST,
  153. {
  154. options: {
  155. filter: {
  156. emailAddress: {
  157. eq: emailAddress,
  158. },
  159. },
  160. },
  161. },
  162. );
  163. expect(
  164. pick(customers.items[0], ['firstName', 'lastName', 'emailAddress', 'phoneNumber']),
  165. ).toEqual(input);
  166. });
  167. it('issues a new token if attempting to register a second time', async () => {
  168. const sendEmail = new Promise<string>(resolve => {
  169. sendEmailFn.mockImplementation((event: AccountRegistrationEvent) => {
  170. resolve(event.user.getNativeAuthenticationMethod().verificationToken!);
  171. });
  172. });
  173. const input: RegisterCustomerInput = {
  174. firstName: 'Sean',
  175. lastName: 'Tester',
  176. emailAddress,
  177. };
  178. const { registerCustomerAccount } = await shopClient.query<Register.Mutation, Register.Variables>(
  179. REGISTER_ACCOUNT,
  180. {
  181. input,
  182. },
  183. );
  184. successErrorGuard.assertSuccess(registerCustomerAccount);
  185. const newVerificationToken = await sendEmail;
  186. expect(registerCustomerAccount.success).toBe(true);
  187. expect(sendEmailFn).toHaveBeenCalled();
  188. expect(newVerificationToken).not.toBe(verificationToken);
  189. verificationToken = newVerificationToken;
  190. });
  191. it('refreshCustomerVerification issues a new token', async () => {
  192. const sendEmail = new Promise<string>(resolve => {
  193. sendEmailFn.mockImplementation((event: AccountRegistrationEvent) => {
  194. resolve(event.user.getNativeAuthenticationMethod().verificationToken!);
  195. });
  196. });
  197. const { refreshCustomerVerification } = await shopClient.query<
  198. RefreshToken.Mutation,
  199. RefreshToken.Variables
  200. >(REFRESH_TOKEN, { emailAddress });
  201. successErrorGuard.assertSuccess(refreshCustomerVerification);
  202. const newVerificationToken = await sendEmail;
  203. expect(refreshCustomerVerification.success).toBe(true);
  204. expect(sendEmailFn).toHaveBeenCalled();
  205. expect(newVerificationToken).not.toBe(verificationToken);
  206. verificationToken = newVerificationToken;
  207. });
  208. it('refreshCustomerVerification does nothing with an unrecognized emailAddress', async () => {
  209. const { refreshCustomerVerification } = await shopClient.query<
  210. RefreshToken.Mutation,
  211. RefreshToken.Variables
  212. >(REFRESH_TOKEN, {
  213. emailAddress: 'never-been-registered@test.com',
  214. });
  215. successErrorGuard.assertSuccess(refreshCustomerVerification);
  216. await waitForSendEmailFn();
  217. expect(refreshCustomerVerification.success).toBe(true);
  218. expect(sendEmailFn).not.toHaveBeenCalled();
  219. });
  220. it('login fails before verification', async () => {
  221. const result = await shopClient.asUserWithCredentials(emailAddress, '');
  222. expect(result.errorCode).toBe(ErrorCode.INVALID_CREDENTIALS_ERROR);
  223. });
  224. it('verification fails with wrong token', async () => {
  225. const { verifyCustomerAccount } = await shopClient.query<Verify.Mutation, Verify.Variables>(
  226. VERIFY_EMAIL,
  227. {
  228. password,
  229. token: 'bad-token',
  230. },
  231. );
  232. currentUserErrorGuard.assertErrorResult(verifyCustomerAccount);
  233. expect(verifyCustomerAccount.message).toBe(`Verification token not recognized`);
  234. expect(verifyCustomerAccount.errorCode).toBe(ErrorCode.VERIFICATION_TOKEN_INVALID_ERROR);
  235. });
  236. it('verification fails with no password', async () => {
  237. const { verifyCustomerAccount } = await shopClient.query<Verify.Mutation, Verify.Variables>(
  238. VERIFY_EMAIL,
  239. {
  240. token: verificationToken,
  241. },
  242. );
  243. currentUserErrorGuard.assertErrorResult(verifyCustomerAccount);
  244. expect(verifyCustomerAccount.message).toBe(`A password must be provided.`);
  245. expect(verifyCustomerAccount.errorCode).toBe(ErrorCode.MISSING_PASSWORD_ERROR);
  246. });
  247. it('verification succeeds with password and correct token', async () => {
  248. const { verifyCustomerAccount } = await shopClient.query<Verify.Mutation, Verify.Variables>(
  249. VERIFY_EMAIL,
  250. {
  251. password,
  252. token: verificationToken,
  253. },
  254. );
  255. currentUserErrorGuard.assertSuccess(verifyCustomerAccount);
  256. expect(verifyCustomerAccount.identifier).toBe('test1@test.com');
  257. const { activeCustomer } = await shopClient.query<GetActiveCustomer.Query>(GET_ACTIVE_CUSTOMER);
  258. newCustomerId = activeCustomer!.id;
  259. });
  260. it('registration silently fails if attempting to register an email already verified', async () => {
  261. const input: RegisterCustomerInput = {
  262. firstName: 'Dodgy',
  263. lastName: 'Hacker',
  264. emailAddress,
  265. };
  266. const { registerCustomerAccount } = await shopClient.query<Register.Mutation, Register.Variables>(
  267. REGISTER_ACCOUNT,
  268. {
  269. input,
  270. },
  271. );
  272. successErrorGuard.assertSuccess(registerCustomerAccount);
  273. await waitForSendEmailFn();
  274. expect(registerCustomerAccount.success).toBe(true);
  275. expect(sendEmailFn).not.toHaveBeenCalled();
  276. });
  277. it('verification fails if attempted a second time', async () => {
  278. const { verifyCustomerAccount } = await shopClient.query<Verify.Mutation, Verify.Variables>(
  279. VERIFY_EMAIL,
  280. {
  281. password,
  282. token: verificationToken,
  283. },
  284. );
  285. currentUserErrorGuard.assertErrorResult(verifyCustomerAccount);
  286. expect(verifyCustomerAccount.message).toBe(`Verification token not recognized`);
  287. expect(verifyCustomerAccount.errorCode).toBe(ErrorCode.VERIFICATION_TOKEN_INVALID_ERROR);
  288. });
  289. it('customer history contains entries for registration & verification', async () => {
  290. const { customer } = await adminClient.query<
  291. GetCustomerHistory.Query,
  292. GetCustomerHistory.Variables
  293. >(GET_CUSTOMER_HISTORY, {
  294. id: newCustomerId,
  295. });
  296. expect(customer?.history.items.map(pick(['type', 'data']))).toEqual([
  297. {
  298. type: HistoryEntryType.CUSTOMER_REGISTERED,
  299. data: {
  300. strategy: 'native',
  301. },
  302. },
  303. {
  304. // second entry because we register twice above
  305. type: HistoryEntryType.CUSTOMER_REGISTERED,
  306. data: {
  307. strategy: 'native',
  308. },
  309. },
  310. {
  311. type: HistoryEntryType.CUSTOMER_VERIFIED,
  312. data: {
  313. strategy: 'native',
  314. },
  315. },
  316. ]);
  317. });
  318. });
  319. describe('customer account creation with up-front password', () => {
  320. const password = 'password';
  321. const emailAddress = 'test2@test.com';
  322. let verificationToken: string;
  323. it('register a new account with password', async () => {
  324. const verificationTokenPromise = getVerificationTokenPromise();
  325. const input: RegisterCustomerInput = {
  326. firstName: 'Lu',
  327. lastName: 'Tester',
  328. phoneNumber: '443324',
  329. emailAddress,
  330. password,
  331. };
  332. const { registerCustomerAccount } = await shopClient.query<Register.Mutation, Register.Variables>(
  333. REGISTER_ACCOUNT,
  334. {
  335. input,
  336. },
  337. );
  338. successErrorGuard.assertSuccess(registerCustomerAccount);
  339. verificationToken = await verificationTokenPromise;
  340. expect(registerCustomerAccount.success).toBe(true);
  341. expect(sendEmailFn).toHaveBeenCalled();
  342. expect(verificationToken).toBeDefined();
  343. const { customers } = await adminClient.query<GetCustomerList.Query, GetCustomerList.Variables>(
  344. GET_CUSTOMER_LIST,
  345. {
  346. options: {
  347. filter: {
  348. emailAddress: {
  349. eq: emailAddress,
  350. },
  351. },
  352. },
  353. },
  354. );
  355. expect(
  356. pick(customers.items[0], ['firstName', 'lastName', 'emailAddress', 'phoneNumber']),
  357. ).toEqual(pick(input, ['firstName', 'lastName', 'emailAddress', 'phoneNumber']));
  358. });
  359. it('login fails before verification', async () => {
  360. const result = await shopClient.asUserWithCredentials(emailAddress, password);
  361. expect(result.errorCode).toBe(ErrorCode.NOT_VERIFIED_ERROR);
  362. expect(result.message).toBe('Please verify this email address before logging in');
  363. });
  364. it('verification fails with password', async () => {
  365. const { verifyCustomerAccount } = await shopClient.query<Verify.Mutation, Verify.Variables>(
  366. VERIFY_EMAIL,
  367. {
  368. token: verificationToken,
  369. password: 'new password',
  370. },
  371. );
  372. currentUserErrorGuard.assertErrorResult(verifyCustomerAccount);
  373. expect(verifyCustomerAccount.message).toBe(`A password has already been set during registration`);
  374. expect(verifyCustomerAccount.errorCode).toBe(ErrorCode.PASSWORD_ALREADY_SET_ERROR);
  375. });
  376. it('verification succeeds with no password and correct token', async () => {
  377. const { verifyCustomerAccount } = await shopClient.query<Verify.Mutation, Verify.Variables>(
  378. VERIFY_EMAIL,
  379. {
  380. token: verificationToken,
  381. },
  382. );
  383. currentUserErrorGuard.assertSuccess(verifyCustomerAccount);
  384. expect(verifyCustomerAccount.identifier).toBe('test2@test.com');
  385. const { activeCustomer } = await shopClient.query<GetActiveCustomer.Query>(GET_ACTIVE_CUSTOMER);
  386. });
  387. });
  388. describe('password reset', () => {
  389. let passwordResetToken: string;
  390. let customer: GetCustomer.Customer;
  391. beforeAll(async () => {
  392. const result = await adminClient.query<GetCustomer.Query, GetCustomer.Variables>(GET_CUSTOMER, {
  393. id: 'T_1',
  394. });
  395. customer = result.customer!;
  396. });
  397. beforeEach(() => {
  398. sendEmailFn = jest.fn();
  399. });
  400. it('requestPasswordReset silently fails with invalid identifier', async () => {
  401. const { requestPasswordReset } = await shopClient.query<
  402. RequestPasswordReset.Mutation,
  403. RequestPasswordReset.Variables
  404. >(REQUEST_PASSWORD_RESET, {
  405. identifier: 'invalid-identifier',
  406. });
  407. successErrorGuard.assertSuccess(requestPasswordReset);
  408. await waitForSendEmailFn();
  409. expect(requestPasswordReset.success).toBe(true);
  410. expect(sendEmailFn).not.toHaveBeenCalled();
  411. expect(passwordResetToken).not.toBeDefined();
  412. });
  413. it('requestPasswordReset sends reset token', async () => {
  414. const passwordResetTokenPromise = getPasswordResetTokenPromise();
  415. const { requestPasswordReset } = await shopClient.query<
  416. RequestPasswordReset.Mutation,
  417. RequestPasswordReset.Variables
  418. >(REQUEST_PASSWORD_RESET, {
  419. identifier: customer.emailAddress,
  420. });
  421. successErrorGuard.assertSuccess(requestPasswordReset);
  422. passwordResetToken = await passwordResetTokenPromise;
  423. expect(requestPasswordReset.success).toBe(true);
  424. expect(sendEmailFn).toHaveBeenCalled();
  425. expect(passwordResetToken).toBeDefined();
  426. });
  427. it('resetPassword returns error result with wrong token', async () => {
  428. const { resetPassword } = await shopClient.query<ResetPassword.Mutation, ResetPassword.Variables>(
  429. RESET_PASSWORD,
  430. {
  431. password: 'newPassword',
  432. token: 'bad-token',
  433. },
  434. );
  435. currentUserErrorGuard.assertErrorResult(resetPassword);
  436. expect(resetPassword.message).toBe(`Password reset token not recognized`);
  437. expect(resetPassword.errorCode).toBe(ErrorCode.PASSWORD_RESET_TOKEN_INVALID_ERROR);
  438. });
  439. it('resetPassword works with valid token', async () => {
  440. const { resetPassword } = await shopClient.query<ResetPassword.Mutation, ResetPassword.Variables>(
  441. RESET_PASSWORD,
  442. {
  443. token: passwordResetToken,
  444. password: 'newPassword',
  445. },
  446. );
  447. currentUserErrorGuard.assertSuccess(resetPassword);
  448. expect(resetPassword.identifier).toBe(customer.emailAddress);
  449. const loginResult = await shopClient.asUserWithCredentials(customer.emailAddress, 'newPassword');
  450. expect(loginResult.identifier).toBe(customer.emailAddress);
  451. });
  452. it('customer history for password reset', async () => {
  453. const result = await adminClient.query<GetCustomerHistory.Query, GetCustomerHistory.Variables>(
  454. GET_CUSTOMER_HISTORY,
  455. {
  456. id: customer.id,
  457. options: {
  458. // skip CUSTOMER_ADDRESS_CREATED entry
  459. skip: 3,
  460. },
  461. },
  462. );
  463. expect(result.customer?.history.items.map(pick(['type', 'data']))).toEqual([
  464. {
  465. type: HistoryEntryType.CUSTOMER_PASSWORD_RESET_REQUESTED,
  466. data: {},
  467. },
  468. {
  469. type: HistoryEntryType.CUSTOMER_PASSWORD_RESET_VERIFIED,
  470. data: {},
  471. },
  472. ]);
  473. });
  474. });
  475. describe('updating emailAddress', () => {
  476. let emailUpdateToken: string;
  477. let customer: GetCustomer.Customer;
  478. const NEW_EMAIL_ADDRESS = 'new@address.com';
  479. const PASSWORD = 'newPassword';
  480. beforeAll(async () => {
  481. const result = await adminClient.query<GetCustomer.Query, GetCustomer.Variables>(GET_CUSTOMER, {
  482. id: 'T_1',
  483. });
  484. customer = result.customer!;
  485. });
  486. beforeEach(() => {
  487. sendEmailFn = jest.fn();
  488. });
  489. it('throws if not logged in', async () => {
  490. try {
  491. await shopClient.asAnonymousUser();
  492. await shopClient.query<
  493. RequestUpdateEmailAddress.Mutation,
  494. RequestUpdateEmailAddress.Variables
  495. >(REQUEST_UPDATE_EMAIL_ADDRESS, {
  496. password: PASSWORD,
  497. newEmailAddress: NEW_EMAIL_ADDRESS,
  498. });
  499. fail('should have thrown');
  500. } catch (err) {
  501. expect(getErrorCode(err)).toBe('FORBIDDEN');
  502. }
  503. });
  504. it('return error result if password is incorrect', async () => {
  505. await shopClient.asUserWithCredentials(customer.emailAddress, PASSWORD);
  506. const { requestUpdateCustomerEmailAddress } = await shopClient.query<
  507. RequestUpdateEmailAddress.Mutation,
  508. RequestUpdateEmailAddress.Variables
  509. >(REQUEST_UPDATE_EMAIL_ADDRESS, {
  510. password: 'bad password',
  511. newEmailAddress: NEW_EMAIL_ADDRESS,
  512. });
  513. successErrorGuard.assertErrorResult(requestUpdateCustomerEmailAddress);
  514. expect(requestUpdateCustomerEmailAddress.message).toBe('The provided credentials are invalid');
  515. expect(requestUpdateCustomerEmailAddress.errorCode).toBe(ErrorCode.INVALID_CREDENTIALS_ERROR);
  516. });
  517. it('return error result email address already in use', async () => {
  518. await shopClient.asUserWithCredentials(customer.emailAddress, PASSWORD);
  519. const result = await adminClient.query<GetCustomer.Query, GetCustomer.Variables>(GET_CUSTOMER, {
  520. id: 'T_2',
  521. });
  522. const otherCustomer = result.customer!;
  523. const { requestUpdateCustomerEmailAddress } = await shopClient.query<
  524. RequestUpdateEmailAddress.Mutation,
  525. RequestUpdateEmailAddress.Variables
  526. >(REQUEST_UPDATE_EMAIL_ADDRESS, {
  527. password: PASSWORD,
  528. newEmailAddress: otherCustomer.emailAddress,
  529. });
  530. successErrorGuard.assertErrorResult(requestUpdateCustomerEmailAddress);
  531. expect(requestUpdateCustomerEmailAddress.message).toBe('The email address is not available.');
  532. expect(requestUpdateCustomerEmailAddress.errorCode).toBe(ErrorCode.EMAIL_ADDRESS_CONFLICT_ERROR);
  533. });
  534. it('triggers event with token', async () => {
  535. await shopClient.asUserWithCredentials(customer.emailAddress, PASSWORD);
  536. const emailUpdateTokenPromise = getEmailUpdateTokenPromise();
  537. await shopClient.query<RequestUpdateEmailAddress.Mutation, RequestUpdateEmailAddress.Variables>(
  538. REQUEST_UPDATE_EMAIL_ADDRESS,
  539. {
  540. password: PASSWORD,
  541. newEmailAddress: NEW_EMAIL_ADDRESS,
  542. },
  543. );
  544. const { identifierChangeToken, pendingIdentifier } = await emailUpdateTokenPromise;
  545. emailUpdateToken = identifierChangeToken!;
  546. expect(pendingIdentifier).toBe(NEW_EMAIL_ADDRESS);
  547. expect(emailUpdateToken).toBeTruthy();
  548. });
  549. it('cannot login with new email address before verification', async () => {
  550. const result = await shopClient.asUserWithCredentials(NEW_EMAIL_ADDRESS, PASSWORD);
  551. expect(result.errorCode).toBe(ErrorCode.INVALID_CREDENTIALS_ERROR);
  552. });
  553. it('return error result for bad token', async () => {
  554. const { updateCustomerEmailAddress } = await shopClient.query<
  555. UpdateEmailAddress.Mutation,
  556. UpdateEmailAddress.Variables
  557. >(UPDATE_EMAIL_ADDRESS, { token: 'bad token' });
  558. successErrorGuard.assertErrorResult(updateCustomerEmailAddress);
  559. expect(updateCustomerEmailAddress.message).toBe('Identifier change token not recognized');
  560. expect(updateCustomerEmailAddress.errorCode).toBe(
  561. ErrorCode.IDENTIFIER_CHANGE_TOKEN_INVALID_ERROR,
  562. );
  563. });
  564. it('verify the new email address', async () => {
  565. const { updateCustomerEmailAddress } = await shopClient.query<
  566. UpdateEmailAddress.Mutation,
  567. UpdateEmailAddress.Variables
  568. >(UPDATE_EMAIL_ADDRESS, { token: emailUpdateToken });
  569. successErrorGuard.assertSuccess(updateCustomerEmailAddress);
  570. expect(updateCustomerEmailAddress.success).toBe(true);
  571. // Allow for occasional race condition where the event does not
  572. // publish before the assertions are made.
  573. await new Promise(resolve => setTimeout(resolve, 10));
  574. expect(sendEmailFn).toHaveBeenCalled();
  575. expect(sendEmailFn.mock.calls[0][0] instanceof IdentifierChangeEvent).toBe(true);
  576. });
  577. it('can login with new email address after verification', async () => {
  578. await shopClient.asUserWithCredentials(NEW_EMAIL_ADDRESS, PASSWORD);
  579. const { activeCustomer } = await shopClient.query<GetActiveCustomer.Query>(GET_ACTIVE_CUSTOMER);
  580. expect(activeCustomer!.id).toBe(customer.id);
  581. expect(activeCustomer!.emailAddress).toBe(NEW_EMAIL_ADDRESS);
  582. });
  583. it('cannot login with old email address after verification', async () => {
  584. const result = await shopClient.asUserWithCredentials(customer.emailAddress, PASSWORD);
  585. expect(result.errorCode).toBe(ErrorCode.INVALID_CREDENTIALS_ERROR);
  586. });
  587. it('customer history for email update', async () => {
  588. const result = await adminClient.query<GetCustomerHistory.Query, GetCustomerHistory.Variables>(
  589. GET_CUSTOMER_HISTORY,
  590. {
  591. id: customer.id,
  592. options: {
  593. skip: 5,
  594. },
  595. },
  596. );
  597. expect(result.customer?.history.items.map(pick(['type', 'data']))).toEqual([
  598. {
  599. type: HistoryEntryType.CUSTOMER_EMAIL_UPDATE_REQUESTED,
  600. data: {
  601. newEmailAddress: 'new@address.com',
  602. oldEmailAddress: 'hayden.zieme12@hotmail.com',
  603. },
  604. },
  605. {
  606. type: HistoryEntryType.CUSTOMER_EMAIL_UPDATE_VERIFIED,
  607. data: {
  608. newEmailAddress: 'new@address.com',
  609. oldEmailAddress: 'hayden.zieme12@hotmail.com',
  610. },
  611. },
  612. ]);
  613. });
  614. });
  615. async function assertRequestAllowed<V>(operation: DocumentNode, variables?: V) {
  616. try {
  617. const status = await shopClient.queryStatus(operation, variables);
  618. expect(status).toBe(200);
  619. } catch (e) {
  620. const errorCode = getErrorCode(e);
  621. if (!errorCode) {
  622. fail(`Unexpected failure: ${e}`);
  623. } else {
  624. fail(`Operation should be allowed, got status ${getErrorCode(e)}`);
  625. }
  626. }
  627. }
  628. async function assertRequestForbidden<V>(operation: DocumentNode, variables: V) {
  629. try {
  630. const status = await shopClient.query(operation, variables);
  631. fail(`Should have thrown`);
  632. } catch (e) {
  633. expect(getErrorCode(e)).toBe('FORBIDDEN');
  634. }
  635. }
  636. function getErrorCode(err: any): string {
  637. return err.response.errors[0].extensions.code;
  638. }
  639. async function createAdministratorWithPermissions(
  640. code: string,
  641. permissions: Permission[],
  642. ): Promise<{ identifier: string; password: string }> {
  643. const roleResult = await shopClient.query<CreateRole.Mutation, CreateRole.Variables>(CREATE_ROLE, {
  644. input: {
  645. code,
  646. description: '',
  647. permissions,
  648. },
  649. });
  650. const role = roleResult.createRole;
  651. const identifier = `${code}@${Math.random().toString(16).substr(2, 8)}`;
  652. const password = `test`;
  653. const adminResult = await shopClient.query<
  654. CreateAdministrator.Mutation,
  655. CreateAdministrator.Variables
  656. >(CREATE_ADMINISTRATOR, {
  657. input: {
  658. emailAddress: identifier,
  659. firstName: code,
  660. lastName: 'Admin',
  661. password,
  662. roleIds: [role.id],
  663. },
  664. });
  665. const admin = adminResult.createAdministrator;
  666. return {
  667. identifier,
  668. password,
  669. };
  670. }
  671. /**
  672. * A "sleep" function which allows the sendEmailFn time to get called.
  673. */
  674. function waitForSendEmailFn() {
  675. return new Promise(resolve => setTimeout(resolve, 10));
  676. }
  677. });
  678. describe('Expiring tokens', () => {
  679. const { server, adminClient, shopClient } = createTestEnvironment(
  680. mergeConfig(testConfig(), {
  681. plugins: [TestEmailPlugin as any],
  682. authOptions: {
  683. verificationTokenDuration: '1ms',
  684. },
  685. }),
  686. );
  687. beforeAll(async () => {
  688. await server.init({
  689. initialData,
  690. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  691. customerCount: 1,
  692. });
  693. await adminClient.asSuperAdmin();
  694. }, TEST_SETUP_TIMEOUT_MS);
  695. beforeEach(() => {
  696. sendEmailFn = jest.fn();
  697. });
  698. afterAll(async () => {
  699. await server.destroy();
  700. });
  701. it('attempting to verify after token has expired throws', async () => {
  702. const verificationTokenPromise = getVerificationTokenPromise();
  703. const input: RegisterCustomerInput = {
  704. firstName: 'Barry',
  705. lastName: 'Wallace',
  706. emailAddress: 'barry.wallace@test.com',
  707. };
  708. const { registerCustomerAccount } = await shopClient.query<Register.Mutation, Register.Variables>(
  709. REGISTER_ACCOUNT,
  710. {
  711. input,
  712. },
  713. );
  714. successErrorGuard.assertSuccess(registerCustomerAccount);
  715. const verificationToken = await verificationTokenPromise;
  716. expect(registerCustomerAccount.success).toBe(true);
  717. expect(sendEmailFn).toHaveBeenCalledTimes(1);
  718. expect(verificationToken).toBeDefined();
  719. await new Promise(resolve => setTimeout(resolve, 3));
  720. const { verifyCustomerAccount } = await shopClient.query<Verify.Mutation, Verify.Variables>(
  721. VERIFY_EMAIL,
  722. {
  723. password: 'test',
  724. token: verificationToken,
  725. },
  726. );
  727. currentUserErrorGuard.assertErrorResult(verifyCustomerAccount);
  728. expect(verifyCustomerAccount.message).toBe(
  729. `Verification token has expired. Use refreshCustomerVerification to send a new token.`,
  730. );
  731. expect(verifyCustomerAccount.errorCode).toBe(ErrorCode.VERIFICATION_TOKEN_EXPIRED_ERROR);
  732. });
  733. it('attempting to reset password after token has expired returns error result', async () => {
  734. const { customer } = await adminClient.query<GetCustomer.Query, GetCustomer.Variables>(GET_CUSTOMER, {
  735. id: 'T_1',
  736. });
  737. const passwordResetTokenPromise = getPasswordResetTokenPromise();
  738. const { requestPasswordReset } = await shopClient.query<
  739. RequestPasswordReset.Mutation,
  740. RequestPasswordReset.Variables
  741. >(REQUEST_PASSWORD_RESET, {
  742. identifier: customer!.emailAddress,
  743. });
  744. successErrorGuard.assertSuccess(requestPasswordReset);
  745. const passwordResetToken = await passwordResetTokenPromise;
  746. expect(requestPasswordReset.success).toBe(true);
  747. expect(sendEmailFn).toHaveBeenCalledTimes(1);
  748. expect(passwordResetToken).toBeDefined();
  749. await new Promise(resolve => setTimeout(resolve, 3));
  750. const { resetPassword } = await shopClient.query<ResetPassword.Mutation, ResetPassword.Variables>(
  751. RESET_PASSWORD,
  752. {
  753. password: 'test',
  754. token: passwordResetToken,
  755. },
  756. );
  757. currentUserErrorGuard.assertErrorResult(resetPassword);
  758. expect(resetPassword.message).toBe(`Password reset token has expired`);
  759. expect(resetPassword.errorCode).toBe(ErrorCode.PASSWORD_RESET_TOKEN_EXPIRED_ERROR);
  760. });
  761. });
  762. describe('Registration without email verification', () => {
  763. const { server, shopClient } = createTestEnvironment(
  764. mergeConfig(testConfig(), {
  765. plugins: [TestEmailPlugin as any],
  766. authOptions: {
  767. requireVerification: false,
  768. },
  769. }),
  770. );
  771. const userEmailAddress = 'glen.beardsley@test.com';
  772. beforeAll(async () => {
  773. await server.init({
  774. initialData,
  775. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  776. customerCount: 1,
  777. });
  778. }, TEST_SETUP_TIMEOUT_MS);
  779. beforeEach(() => {
  780. sendEmailFn = jest.fn();
  781. });
  782. afterAll(async () => {
  783. await server.destroy();
  784. });
  785. it('Returns error result if no password is provided', async () => {
  786. const input: RegisterCustomerInput = {
  787. firstName: 'Glen',
  788. lastName: 'Beardsley',
  789. emailAddress: userEmailAddress,
  790. };
  791. const { registerCustomerAccount } = await shopClient.query<Register.Mutation, Register.Variables>(
  792. REGISTER_ACCOUNT,
  793. {
  794. input,
  795. },
  796. );
  797. successErrorGuard.assertErrorResult(registerCustomerAccount);
  798. expect(registerCustomerAccount.message).toBe('A password must be provided.');
  799. expect(registerCustomerAccount.errorCode).toBe(ErrorCode.MISSING_PASSWORD_ERROR);
  800. });
  801. it('register a new account with password', async () => {
  802. const input: RegisterCustomerInput = {
  803. firstName: 'Glen',
  804. lastName: 'Beardsley',
  805. emailAddress: userEmailAddress,
  806. password: 'test',
  807. };
  808. const { registerCustomerAccount } = await shopClient.query<Register.Mutation, Register.Variables>(
  809. REGISTER_ACCOUNT,
  810. {
  811. input,
  812. },
  813. );
  814. successErrorGuard.assertSuccess(registerCustomerAccount);
  815. expect(registerCustomerAccount.success).toBe(true);
  816. expect(sendEmailFn).not.toHaveBeenCalled();
  817. });
  818. it('can login after registering', async () => {
  819. await shopClient.asUserWithCredentials(userEmailAddress, 'test');
  820. const result = await shopClient.query(
  821. gql`
  822. query GetMe {
  823. me {
  824. identifier
  825. }
  826. }
  827. `,
  828. );
  829. expect(result.me.identifier).toBe(userEmailAddress);
  830. });
  831. });
  832. describe('Updating email address without email verification', () => {
  833. const { server, adminClient, shopClient } = createTestEnvironment(
  834. mergeConfig(testConfig(), {
  835. plugins: [TestEmailPlugin as any],
  836. authOptions: {
  837. requireVerification: false,
  838. },
  839. }),
  840. );
  841. let customer: GetCustomer.Customer;
  842. const NEW_EMAIL_ADDRESS = 'new@address.com';
  843. beforeAll(async () => {
  844. await server.init({
  845. initialData,
  846. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  847. customerCount: 1,
  848. });
  849. await adminClient.asSuperAdmin();
  850. const result = await adminClient.query<GetCustomer.Query, GetCustomer.Variables>(GET_CUSTOMER, {
  851. id: 'T_1',
  852. });
  853. customer = result.customer!;
  854. }, TEST_SETUP_TIMEOUT_MS);
  855. beforeEach(() => {
  856. sendEmailFn = jest.fn();
  857. });
  858. afterAll(async () => {
  859. await server.destroy();
  860. });
  861. it('updates email address', async () => {
  862. await shopClient.asUserWithCredentials(customer.emailAddress, 'test');
  863. const { requestUpdateCustomerEmailAddress } = await shopClient.query<
  864. RequestUpdateEmailAddress.Mutation,
  865. RequestUpdateEmailAddress.Variables
  866. >(REQUEST_UPDATE_EMAIL_ADDRESS, {
  867. password: 'test',
  868. newEmailAddress: NEW_EMAIL_ADDRESS,
  869. });
  870. successErrorGuard.assertSuccess(requestUpdateCustomerEmailAddress);
  871. expect(requestUpdateCustomerEmailAddress.success).toBe(true);
  872. expect(sendEmailFn).toHaveBeenCalledTimes(1);
  873. expect(sendEmailFn.mock.calls[0][0] instanceof IdentifierChangeEvent).toBe(true);
  874. const { activeCustomer } = await shopClient.query<GetActiveCustomer.Query>(GET_ACTIVE_CUSTOMER);
  875. expect(activeCustomer!.emailAddress).toBe(NEW_EMAIL_ADDRESS);
  876. });
  877. });
  878. function getVerificationTokenPromise(): Promise<string> {
  879. return new Promise<any>(resolve => {
  880. sendEmailFn.mockImplementation((event: AccountRegistrationEvent) => {
  881. resolve(event.user.getNativeAuthenticationMethod().verificationToken);
  882. });
  883. });
  884. }
  885. function getPasswordResetTokenPromise(): Promise<string> {
  886. return new Promise<any>(resolve => {
  887. sendEmailFn.mockImplementation((event: PasswordResetEvent) => {
  888. resolve(event.user.getNativeAuthenticationMethod().passwordResetToken);
  889. });
  890. });
  891. }
  892. function getEmailUpdateTokenPromise(): Promise<{
  893. identifierChangeToken: string | null;
  894. pendingIdentifier: string | null;
  895. }> {
  896. return new Promise(resolve => {
  897. sendEmailFn.mockImplementation((event: IdentifierChangeRequestEvent) => {
  898. resolve(
  899. pick(event.user.getNativeAuthenticationMethod(), [
  900. 'identifierChangeToken',
  901. 'pendingIdentifier',
  902. ]),
  903. );
  904. });
  905. });
  906. }