shop-auth.e2e-spec.ts 45 KB

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