shop-auth.e2e-spec.ts 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040
  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. expect(sendEmailFn).toHaveBeenCalled();
  572. expect(sendEmailFn.mock.calls[0][0] instanceof IdentifierChangeEvent).toBe(true);
  573. });
  574. it('can login with new email address after verification', async () => {
  575. await shopClient.asUserWithCredentials(NEW_EMAIL_ADDRESS, PASSWORD);
  576. const { activeCustomer } = await shopClient.query<GetActiveCustomer.Query>(GET_ACTIVE_CUSTOMER);
  577. expect(activeCustomer!.id).toBe(customer.id);
  578. expect(activeCustomer!.emailAddress).toBe(NEW_EMAIL_ADDRESS);
  579. });
  580. it('cannot login with old email address after verification', async () => {
  581. const result = await shopClient.asUserWithCredentials(customer.emailAddress, PASSWORD);
  582. expect(result.errorCode).toBe(ErrorCode.INVALID_CREDENTIALS_ERROR);
  583. });
  584. it('customer history for email update', async () => {
  585. const result = await adminClient.query<GetCustomerHistory.Query, GetCustomerHistory.Variables>(
  586. GET_CUSTOMER_HISTORY,
  587. {
  588. id: customer.id,
  589. options: {
  590. skip: 5,
  591. },
  592. },
  593. );
  594. expect(result.customer?.history.items.map(pick(['type', 'data']))).toEqual([
  595. {
  596. type: HistoryEntryType.CUSTOMER_EMAIL_UPDATE_REQUESTED,
  597. data: {
  598. newEmailAddress: 'new@address.com',
  599. oldEmailAddress: 'hayden.zieme12@hotmail.com',
  600. },
  601. },
  602. {
  603. type: HistoryEntryType.CUSTOMER_EMAIL_UPDATE_VERIFIED,
  604. data: {
  605. newEmailAddress: 'new@address.com',
  606. oldEmailAddress: 'hayden.zieme12@hotmail.com',
  607. },
  608. },
  609. ]);
  610. });
  611. });
  612. async function assertRequestAllowed<V>(operation: DocumentNode, variables?: V) {
  613. try {
  614. const status = await shopClient.queryStatus(operation, variables);
  615. expect(status).toBe(200);
  616. } catch (e) {
  617. const errorCode = getErrorCode(e);
  618. if (!errorCode) {
  619. fail(`Unexpected failure: ${e}`);
  620. } else {
  621. fail(`Operation should be allowed, got status ${getErrorCode(e)}`);
  622. }
  623. }
  624. }
  625. async function assertRequestForbidden<V>(operation: DocumentNode, variables: V) {
  626. try {
  627. const status = await shopClient.query(operation, variables);
  628. fail(`Should have thrown`);
  629. } catch (e) {
  630. expect(getErrorCode(e)).toBe('FORBIDDEN');
  631. }
  632. }
  633. function getErrorCode(err: any): string {
  634. return err.response.errors[0].extensions.code;
  635. }
  636. async function createAdministratorWithPermissions(
  637. code: string,
  638. permissions: Permission[],
  639. ): Promise<{ identifier: string; password: string }> {
  640. const roleResult = await shopClient.query<CreateRole.Mutation, CreateRole.Variables>(CREATE_ROLE, {
  641. input: {
  642. code,
  643. description: '',
  644. permissions,
  645. },
  646. });
  647. const role = roleResult.createRole;
  648. const identifier = `${code}@${Math.random().toString(16).substr(2, 8)}`;
  649. const password = `test`;
  650. const adminResult = await shopClient.query<
  651. CreateAdministrator.Mutation,
  652. CreateAdministrator.Variables
  653. >(CREATE_ADMINISTRATOR, {
  654. input: {
  655. emailAddress: identifier,
  656. firstName: code,
  657. lastName: 'Admin',
  658. password,
  659. roleIds: [role.id],
  660. },
  661. });
  662. const admin = adminResult.createAdministrator;
  663. return {
  664. identifier,
  665. password,
  666. };
  667. }
  668. /**
  669. * A "sleep" function which allows the sendEmailFn time to get called.
  670. */
  671. function waitForSendEmailFn() {
  672. return new Promise(resolve => setTimeout(resolve, 10));
  673. }
  674. });
  675. describe('Expiring tokens', () => {
  676. const { server, adminClient, shopClient } = createTestEnvironment(
  677. mergeConfig(testConfig, {
  678. plugins: [TestEmailPlugin as any],
  679. authOptions: {
  680. verificationTokenDuration: '1ms',
  681. },
  682. }),
  683. );
  684. beforeAll(async () => {
  685. await server.init({
  686. initialData,
  687. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  688. customerCount: 1,
  689. });
  690. await adminClient.asSuperAdmin();
  691. }, TEST_SETUP_TIMEOUT_MS);
  692. beforeEach(() => {
  693. sendEmailFn = jest.fn();
  694. });
  695. afterAll(async () => {
  696. await server.destroy();
  697. });
  698. it('attempting to verify after token has expired throws', async () => {
  699. const verificationTokenPromise = getVerificationTokenPromise();
  700. const input: RegisterCustomerInput = {
  701. firstName: 'Barry',
  702. lastName: 'Wallace',
  703. emailAddress: 'barry.wallace@test.com',
  704. };
  705. const { registerCustomerAccount } = await shopClient.query<Register.Mutation, Register.Variables>(
  706. REGISTER_ACCOUNT,
  707. {
  708. input,
  709. },
  710. );
  711. successErrorGuard.assertSuccess(registerCustomerAccount);
  712. const verificationToken = await verificationTokenPromise;
  713. expect(registerCustomerAccount.success).toBe(true);
  714. expect(sendEmailFn).toHaveBeenCalledTimes(1);
  715. expect(verificationToken).toBeDefined();
  716. await new Promise(resolve => setTimeout(resolve, 3));
  717. const { verifyCustomerAccount } = await shopClient.query<Verify.Mutation, Verify.Variables>(
  718. VERIFY_EMAIL,
  719. {
  720. password: 'test',
  721. token: verificationToken,
  722. },
  723. );
  724. currentUserErrorGuard.assertErrorResult(verifyCustomerAccount);
  725. expect(verifyCustomerAccount.message).toBe(
  726. `Verification token has expired. Use refreshCustomerVerification to send a new token.`,
  727. );
  728. expect(verifyCustomerAccount.errorCode).toBe(ErrorCode.VERIFICATION_TOKEN_EXPIRED_ERROR);
  729. });
  730. it('attempting to reset password after token has expired returns error result', async () => {
  731. const { customer } = await adminClient.query<GetCustomer.Query, GetCustomer.Variables>(GET_CUSTOMER, {
  732. id: 'T_1',
  733. });
  734. const passwordResetTokenPromise = getPasswordResetTokenPromise();
  735. const { requestPasswordReset } = await shopClient.query<
  736. RequestPasswordReset.Mutation,
  737. RequestPasswordReset.Variables
  738. >(REQUEST_PASSWORD_RESET, {
  739. identifier: customer!.emailAddress,
  740. });
  741. successErrorGuard.assertSuccess(requestPasswordReset);
  742. const passwordResetToken = await passwordResetTokenPromise;
  743. expect(requestPasswordReset.success).toBe(true);
  744. expect(sendEmailFn).toHaveBeenCalledTimes(1);
  745. expect(passwordResetToken).toBeDefined();
  746. await new Promise(resolve => setTimeout(resolve, 3));
  747. const { resetPassword } = await shopClient.query<ResetPassword.Mutation, ResetPassword.Variables>(
  748. RESET_PASSWORD,
  749. {
  750. password: 'test',
  751. token: passwordResetToken,
  752. },
  753. );
  754. currentUserErrorGuard.assertErrorResult(resetPassword);
  755. expect(resetPassword.message).toBe(`Password reset token has expired`);
  756. expect(resetPassword.errorCode).toBe(ErrorCode.PASSWORD_RESET_TOKEN_EXPIRED_ERROR);
  757. });
  758. });
  759. describe('Registration without email verification', () => {
  760. const { server, shopClient } = createTestEnvironment(
  761. mergeConfig(testConfig, {
  762. plugins: [TestEmailPlugin as any],
  763. authOptions: {
  764. requireVerification: false,
  765. },
  766. }),
  767. );
  768. const userEmailAddress = 'glen.beardsley@test.com';
  769. beforeAll(async () => {
  770. await server.init({
  771. initialData,
  772. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  773. customerCount: 1,
  774. });
  775. }, TEST_SETUP_TIMEOUT_MS);
  776. beforeEach(() => {
  777. sendEmailFn = jest.fn();
  778. });
  779. afterAll(async () => {
  780. await server.destroy();
  781. });
  782. it('Returns error result if no password is provided', async () => {
  783. const input: RegisterCustomerInput = {
  784. firstName: 'Glen',
  785. lastName: 'Beardsley',
  786. emailAddress: userEmailAddress,
  787. };
  788. const { registerCustomerAccount } = await shopClient.query<Register.Mutation, Register.Variables>(
  789. REGISTER_ACCOUNT,
  790. {
  791. input,
  792. },
  793. );
  794. successErrorGuard.assertErrorResult(registerCustomerAccount);
  795. expect(registerCustomerAccount.message).toBe('A password must be provided.');
  796. expect(registerCustomerAccount.errorCode).toBe(ErrorCode.MISSING_PASSWORD_ERROR);
  797. });
  798. it('register a new account with password', async () => {
  799. const input: RegisterCustomerInput = {
  800. firstName: 'Glen',
  801. lastName: 'Beardsley',
  802. emailAddress: userEmailAddress,
  803. password: 'test',
  804. };
  805. const { registerCustomerAccount } = await shopClient.query<Register.Mutation, Register.Variables>(
  806. REGISTER_ACCOUNT,
  807. {
  808. input,
  809. },
  810. );
  811. successErrorGuard.assertSuccess(registerCustomerAccount);
  812. expect(registerCustomerAccount.success).toBe(true);
  813. expect(sendEmailFn).not.toHaveBeenCalled();
  814. });
  815. it('can login after registering', async () => {
  816. await shopClient.asUserWithCredentials(userEmailAddress, 'test');
  817. const result = await shopClient.query(
  818. gql`
  819. query GetMe {
  820. me {
  821. identifier
  822. }
  823. }
  824. `,
  825. );
  826. expect(result.me.identifier).toBe(userEmailAddress);
  827. });
  828. });
  829. describe('Updating email address without email verification', () => {
  830. const { server, adminClient, shopClient } = createTestEnvironment(
  831. mergeConfig(testConfig, {
  832. plugins: [TestEmailPlugin as any],
  833. authOptions: {
  834. requireVerification: false,
  835. },
  836. }),
  837. );
  838. let customer: GetCustomer.Customer;
  839. const NEW_EMAIL_ADDRESS = 'new@address.com';
  840. beforeAll(async () => {
  841. await server.init({
  842. initialData,
  843. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  844. customerCount: 1,
  845. });
  846. await adminClient.asSuperAdmin();
  847. const result = await adminClient.query<GetCustomer.Query, GetCustomer.Variables>(GET_CUSTOMER, {
  848. id: 'T_1',
  849. });
  850. customer = result.customer!;
  851. }, TEST_SETUP_TIMEOUT_MS);
  852. beforeEach(() => {
  853. sendEmailFn = jest.fn();
  854. });
  855. afterAll(async () => {
  856. await server.destroy();
  857. });
  858. it('updates email address', async () => {
  859. await shopClient.asUserWithCredentials(customer.emailAddress, 'test');
  860. const { requestUpdateCustomerEmailAddress } = await shopClient.query<
  861. RequestUpdateEmailAddress.Mutation,
  862. RequestUpdateEmailAddress.Variables
  863. >(REQUEST_UPDATE_EMAIL_ADDRESS, {
  864. password: 'test',
  865. newEmailAddress: NEW_EMAIL_ADDRESS,
  866. });
  867. successErrorGuard.assertSuccess(requestUpdateCustomerEmailAddress);
  868. expect(requestUpdateCustomerEmailAddress.success).toBe(true);
  869. expect(sendEmailFn).toHaveBeenCalledTimes(1);
  870. expect(sendEmailFn.mock.calls[0][0] instanceof IdentifierChangeEvent).toBe(true);
  871. const { activeCustomer } = await shopClient.query<GetActiveCustomer.Query>(GET_ACTIVE_CUSTOMER);
  872. expect(activeCustomer!.emailAddress).toBe(NEW_EMAIL_ADDRESS);
  873. });
  874. });
  875. function getVerificationTokenPromise(): Promise<string> {
  876. return new Promise<any>(resolve => {
  877. sendEmailFn.mockImplementation((event: AccountRegistrationEvent) => {
  878. resolve(event.user.getNativeAuthenticationMethod().verificationToken);
  879. });
  880. });
  881. }
  882. function getPasswordResetTokenPromise(): Promise<string> {
  883. return new Promise<any>(resolve => {
  884. sendEmailFn.mockImplementation((event: PasswordResetEvent) => {
  885. resolve(event.user.getNativeAuthenticationMethod().passwordResetToken);
  886. });
  887. });
  888. }
  889. function getEmailUpdateTokenPromise(): Promise<{
  890. identifierChangeToken: string | null;
  891. pendingIdentifier: string | null;
  892. }> {
  893. return new Promise(resolve => {
  894. sendEmailFn.mockImplementation((event: IdentifierChangeRequestEvent) => {
  895. resolve(
  896. pick(event.user.getNativeAuthenticationMethod(), [
  897. 'identifierChangeToken',
  898. 'pendingIdentifier',
  899. ]),
  900. );
  901. });
  902. });
  903. }