shop-auth.e2e-spec.ts 50 KB

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