shop-auth.e2e-spec.ts 50 KB

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