shop-auth.e2e-spec.ts 41 KB

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