shop-auth.e2e-spec.ts 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782
  1. /* tslint:disable:no-non-null-assertion */
  2. import { OnModuleInit } from '@nestjs/common';
  3. import { RegisterCustomerInput } from '@vendure/common/lib/generated-shop-types';
  4. import { pick } from '@vendure/common/lib/pick';
  5. import { DocumentNode } from 'graphql';
  6. import gql from 'graphql-tag';
  7. import path from 'path';
  8. import { EventBus } from '../src/event-bus/event-bus';
  9. import { EventBusModule } from '../src/event-bus/event-bus.module';
  10. import { AccountRegistrationEvent } from '../src/event-bus/events/account-registration-event';
  11. import { IdentifierChangeEvent } from '../src/event-bus/events/identifier-change-event';
  12. import { IdentifierChangeRequestEvent } from '../src/event-bus/events/identifier-change-request-event';
  13. import { PasswordResetEvent } from '../src/event-bus/events/password-reset-event';
  14. import { VendurePlugin } from '../src/plugin/vendure-plugin';
  15. import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
  16. import {
  17. CreateAdministrator,
  18. CreateRole,
  19. GetCustomer,
  20. Permission,
  21. } from './graphql/generated-e2e-admin-types';
  22. import {
  23. GetActiveCustomer,
  24. RefreshToken,
  25. Register,
  26. RequestPasswordReset,
  27. RequestUpdateEmailAddress,
  28. ResetPassword,
  29. UpdateEmailAddress,
  30. Verify,
  31. } from './graphql/generated-e2e-shop-types';
  32. import { CREATE_ADMINISTRATOR, CREATE_ROLE, GET_CUSTOMER } from './graphql/shared-definitions';
  33. import {
  34. GET_ACTIVE_CUSTOMER,
  35. REFRESH_TOKEN,
  36. REGISTER_ACCOUNT,
  37. REQUEST_PASSWORD_RESET,
  38. REQUEST_UPDATE_EMAIL_ADDRESS,
  39. RESET_PASSWORD,
  40. UPDATE_EMAIL_ADDRESS,
  41. VERIFY_EMAIL,
  42. } from './graphql/shop-definitions';
  43. import { TestAdminClient, TestShopClient } from './test-client';
  44. import { TestServer } from './test-server';
  45. import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
  46. let sendEmailFn: jest.Mock;
  47. describe('Shop auth & accounts', () => {
  48. const shopClient = new TestShopClient();
  49. const adminClient = new TestAdminClient();
  50. const server = new TestServer();
  51. beforeAll(async () => {
  52. const token = await server.init(
  53. {
  54. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  55. customerCount: 2,
  56. },
  57. {
  58. plugins: [TestEmailPlugin],
  59. },
  60. );
  61. await shopClient.init();
  62. await adminClient.init();
  63. }, TEST_SETUP_TIMEOUT_MS);
  64. afterAll(async () => {
  65. await server.destroy();
  66. });
  67. describe('customer account creation', () => {
  68. const password = 'password';
  69. const emailAddress = 'test1@test.com';
  70. let verificationToken: string;
  71. beforeEach(() => {
  72. sendEmailFn = jest.fn();
  73. });
  74. it(
  75. 'errors if a password is provided',
  76. assertThrowsWithMessage(async () => {
  77. const input: RegisterCustomerInput = {
  78. firstName: 'Sofia',
  79. lastName: 'Green',
  80. emailAddress: 'sofia.green@test.com',
  81. password: 'test',
  82. };
  83. const result = await shopClient.query<Register.Mutation, Register.Variables>(
  84. REGISTER_ACCOUNT,
  85. { input },
  86. );
  87. }, 'Do not provide a password when `authOptions.requireVerification` is set to "true"'),
  88. );
  89. it('register a new account', async () => {
  90. const verificationTokenPromise = getVerificationTokenPromise();
  91. const input: RegisterCustomerInput = {
  92. firstName: 'Sean',
  93. lastName: 'Tester',
  94. emailAddress,
  95. };
  96. const result = await shopClient.query<Register.Mutation, Register.Variables>(REGISTER_ACCOUNT, {
  97. input,
  98. });
  99. verificationToken = await verificationTokenPromise;
  100. expect(result.registerCustomerAccount).toBe(true);
  101. expect(sendEmailFn).toHaveBeenCalled();
  102. expect(verificationToken).toBeDefined();
  103. });
  104. it('issues a new token if attempting to register a second time', async () => {
  105. const sendEmail = new Promise<string>(resolve => {
  106. sendEmailFn.mockImplementation((event: AccountRegistrationEvent) => {
  107. resolve(event.user.verificationToken!);
  108. });
  109. });
  110. const input: RegisterCustomerInput = {
  111. firstName: 'Sean',
  112. lastName: 'Tester',
  113. emailAddress,
  114. };
  115. const result = await shopClient.query<Register.Mutation, Register.Variables>(REGISTER_ACCOUNT, {
  116. input,
  117. });
  118. const newVerificationToken = await sendEmail;
  119. expect(result.registerCustomerAccount).toBe(true);
  120. expect(sendEmailFn).toHaveBeenCalled();
  121. expect(newVerificationToken).not.toBe(verificationToken);
  122. verificationToken = newVerificationToken;
  123. });
  124. it('refreshCustomerVerification issues a new token', async () => {
  125. const sendEmail = new Promise<string>(resolve => {
  126. sendEmailFn.mockImplementation((event: AccountRegistrationEvent) => {
  127. resolve(event.user.verificationToken!);
  128. });
  129. });
  130. const result = await shopClient.query<RefreshToken.Mutation, RefreshToken.Variables>(
  131. REFRESH_TOKEN,
  132. { emailAddress },
  133. );
  134. const newVerificationToken = await sendEmail;
  135. expect(result.refreshCustomerVerification).toBe(true);
  136. expect(sendEmailFn).toHaveBeenCalled();
  137. expect(newVerificationToken).not.toBe(verificationToken);
  138. verificationToken = newVerificationToken;
  139. });
  140. it('refreshCustomerVerification does nothing with an unrecognized emailAddress', async () => {
  141. const result = await shopClient.query<RefreshToken.Mutation, RefreshToken.Variables>(
  142. REFRESH_TOKEN,
  143. {
  144. emailAddress: 'never-been-registered@test.com',
  145. },
  146. );
  147. await waitForSendEmailFn();
  148. expect(result.refreshCustomerVerification).toBe(true);
  149. expect(sendEmailFn).not.toHaveBeenCalled();
  150. });
  151. it('login fails before verification', async () => {
  152. try {
  153. await shopClient.asUserWithCredentials(emailAddress, '');
  154. fail('should have thrown');
  155. } catch (err) {
  156. expect(getErrorCode(err)).toBe('UNAUTHORIZED');
  157. }
  158. });
  159. it(
  160. 'verification fails with wrong token',
  161. assertThrowsWithMessage(
  162. () =>
  163. shopClient.query<Verify.Mutation, Verify.Variables>(VERIFY_EMAIL, {
  164. password,
  165. token: 'bad-token',
  166. }),
  167. `Verification token not recognized`,
  168. ),
  169. );
  170. it('verification succeeds with correct token', async () => {
  171. const result = await shopClient.query<Verify.Mutation, Verify.Variables>(VERIFY_EMAIL, {
  172. password,
  173. token: verificationToken,
  174. });
  175. expect(result.verifyCustomerAccount.user.identifier).toBe('test1@test.com');
  176. });
  177. it('registration silently fails if attempting to register an email already verified', async () => {
  178. const input: RegisterCustomerInput = {
  179. firstName: 'Dodgy',
  180. lastName: 'Hacker',
  181. emailAddress,
  182. };
  183. const result = await shopClient.query<Register.Mutation, Register.Variables>(REGISTER_ACCOUNT, {
  184. input,
  185. });
  186. await waitForSendEmailFn();
  187. expect(result.registerCustomerAccount).toBe(true);
  188. expect(sendEmailFn).not.toHaveBeenCalled();
  189. });
  190. it(
  191. 'verification fails if attempted a second time',
  192. assertThrowsWithMessage(
  193. () =>
  194. shopClient.query<Verify.Mutation, Verify.Variables>(VERIFY_EMAIL, {
  195. password,
  196. token: verificationToken,
  197. }),
  198. `Verification token not recognized`,
  199. ),
  200. );
  201. });
  202. describe('password reset', () => {
  203. let passwordResetToken: string;
  204. let customer: GetCustomer.Customer;
  205. beforeAll(async () => {
  206. const result = await adminClient.query<GetCustomer.Query, GetCustomer.Variables>(GET_CUSTOMER, {
  207. id: 'T_1',
  208. });
  209. customer = result.customer!;
  210. });
  211. beforeEach(() => {
  212. sendEmailFn = jest.fn();
  213. });
  214. it('requestPasswordReset silently fails with invalid identifier', async () => {
  215. const result = await shopClient.query<
  216. RequestPasswordReset.Mutation,
  217. RequestPasswordReset.Variables
  218. >(REQUEST_PASSWORD_RESET, {
  219. identifier: 'invalid-identifier',
  220. });
  221. await waitForSendEmailFn();
  222. expect(result.requestPasswordReset).toBe(true);
  223. expect(sendEmailFn).not.toHaveBeenCalled();
  224. expect(passwordResetToken).not.toBeDefined();
  225. });
  226. it('requestPasswordReset sends reset token', async () => {
  227. const passwordResetTokenPromise = getPasswordResetTokenPromise();
  228. const result = await shopClient.query<
  229. RequestPasswordReset.Mutation,
  230. RequestPasswordReset.Variables
  231. >(REQUEST_PASSWORD_RESET, {
  232. identifier: customer.emailAddress,
  233. });
  234. passwordResetToken = await passwordResetTokenPromise;
  235. expect(result.requestPasswordReset).toBe(true);
  236. expect(sendEmailFn).toHaveBeenCalled();
  237. expect(passwordResetToken).toBeDefined();
  238. });
  239. it(
  240. 'resetPassword fails with wrong token',
  241. assertThrowsWithMessage(
  242. () =>
  243. shopClient.query<ResetPassword.Mutation, ResetPassword.Variables>(RESET_PASSWORD, {
  244. password: 'newPassword',
  245. token: 'bad-token',
  246. }),
  247. `Password reset token not recognized`,
  248. ),
  249. );
  250. it('resetPassword works with valid token', async () => {
  251. const result = await shopClient.query<ResetPassword.Mutation, ResetPassword.Variables>(
  252. RESET_PASSWORD,
  253. {
  254. token: passwordResetToken,
  255. password: 'newPassword',
  256. },
  257. );
  258. const loginResult = await shopClient.asUserWithCredentials(customer.emailAddress, 'newPassword');
  259. expect(loginResult.user.identifier).toBe(customer.emailAddress);
  260. });
  261. });
  262. describe('updating emailAddress', () => {
  263. let emailUpdateToken: string;
  264. let customer: GetCustomer.Customer;
  265. const NEW_EMAIL_ADDRESS = 'new@address.com';
  266. const PASSWORD = 'newPassword';
  267. beforeAll(async () => {
  268. const result = await adminClient.query<GetCustomer.Query, GetCustomer.Variables>(GET_CUSTOMER, {
  269. id: 'T_1',
  270. });
  271. customer = result.customer!;
  272. });
  273. beforeEach(() => {
  274. sendEmailFn = jest.fn();
  275. });
  276. it('throws if not logged in', async () => {
  277. try {
  278. await shopClient.asAnonymousUser();
  279. await shopClient.query<
  280. RequestUpdateEmailAddress.Mutation,
  281. RequestUpdateEmailAddress.Variables
  282. >(REQUEST_UPDATE_EMAIL_ADDRESS, {
  283. password: PASSWORD,
  284. newEmailAddress: NEW_EMAIL_ADDRESS,
  285. });
  286. fail('should have thrown');
  287. } catch (err) {
  288. expect(getErrorCode(err)).toBe('FORBIDDEN');
  289. }
  290. });
  291. it('throws if password is incorrect', async () => {
  292. try {
  293. await shopClient.asUserWithCredentials(customer.emailAddress, PASSWORD);
  294. await shopClient.query<
  295. RequestUpdateEmailAddress.Mutation,
  296. RequestUpdateEmailAddress.Variables
  297. >(REQUEST_UPDATE_EMAIL_ADDRESS, {
  298. password: 'bad password',
  299. newEmailAddress: NEW_EMAIL_ADDRESS,
  300. });
  301. fail('should have thrown');
  302. } catch (err) {
  303. expect(getErrorCode(err)).toBe('UNAUTHORIZED');
  304. }
  305. });
  306. it(
  307. 'throws if email address already in use',
  308. assertThrowsWithMessage(async () => {
  309. await shopClient.asUserWithCredentials(customer.emailAddress, PASSWORD);
  310. const result = await adminClient.query<GetCustomer.Query, GetCustomer.Variables>(
  311. GET_CUSTOMER,
  312. { id: 'T_2' },
  313. );
  314. const otherCustomer = result.customer!;
  315. await shopClient.query<
  316. RequestUpdateEmailAddress.Mutation,
  317. RequestUpdateEmailAddress.Variables
  318. >(REQUEST_UPDATE_EMAIL_ADDRESS, {
  319. password: PASSWORD,
  320. newEmailAddress: otherCustomer.emailAddress,
  321. });
  322. }, 'This email address is not available'),
  323. );
  324. it('triggers event with token', async () => {
  325. await shopClient.asUserWithCredentials(customer.emailAddress, PASSWORD);
  326. const emailUpdateTokenPromise = getEmailUpdateTokenPromise();
  327. await shopClient.query<RequestUpdateEmailAddress.Mutation, RequestUpdateEmailAddress.Variables>(
  328. REQUEST_UPDATE_EMAIL_ADDRESS,
  329. {
  330. password: PASSWORD,
  331. newEmailAddress: NEW_EMAIL_ADDRESS,
  332. },
  333. );
  334. const { identifierChangeToken, pendingIdentifier } = await emailUpdateTokenPromise;
  335. emailUpdateToken = identifierChangeToken!;
  336. expect(pendingIdentifier).toBe(NEW_EMAIL_ADDRESS);
  337. expect(emailUpdateToken).toBeTruthy();
  338. });
  339. it('cannot login with new email address before verification', async () => {
  340. try {
  341. await shopClient.asUserWithCredentials(NEW_EMAIL_ADDRESS, PASSWORD);
  342. fail('should have thrown');
  343. } catch (err) {
  344. expect(getErrorCode(err)).toBe('UNAUTHORIZED');
  345. }
  346. });
  347. it(
  348. 'throws with bad token',
  349. assertThrowsWithMessage(async () => {
  350. await shopClient.query<UpdateEmailAddress.Mutation, UpdateEmailAddress.Variables>(
  351. UPDATE_EMAIL_ADDRESS,
  352. { token: 'bad token' },
  353. );
  354. }, 'Identifier change token not recognized'),
  355. );
  356. it('verify the new email address', async () => {
  357. const result = await shopClient.query<UpdateEmailAddress.Mutation, UpdateEmailAddress.Variables>(
  358. UPDATE_EMAIL_ADDRESS,
  359. { token: emailUpdateToken },
  360. );
  361. expect(result.updateCustomerEmailAddress).toBe(true);
  362. expect(sendEmailFn).toHaveBeenCalled();
  363. expect(sendEmailFn.mock.calls[0][0] instanceof IdentifierChangeEvent).toBe(true);
  364. });
  365. it('can login with new email address after verification', async () => {
  366. await shopClient.asUserWithCredentials(NEW_EMAIL_ADDRESS, PASSWORD);
  367. const { activeCustomer } = await shopClient.query<GetActiveCustomer.Query>(GET_ACTIVE_CUSTOMER);
  368. expect(activeCustomer!.id).toBe(customer.id);
  369. expect(activeCustomer!.emailAddress).toBe(NEW_EMAIL_ADDRESS);
  370. });
  371. it('cannot login with old email address after verification', async () => {
  372. try {
  373. await shopClient.asUserWithCredentials(customer.emailAddress, PASSWORD);
  374. fail('should have thrown');
  375. } catch (err) {
  376. expect(getErrorCode(err)).toBe('UNAUTHORIZED');
  377. }
  378. });
  379. });
  380. async function assertRequestAllowed<V>(operation: DocumentNode, variables?: V) {
  381. try {
  382. const status = await shopClient.queryStatus(operation, variables);
  383. expect(status).toBe(200);
  384. } catch (e) {
  385. const errorCode = getErrorCode(e);
  386. if (!errorCode) {
  387. fail(`Unexpected failure: ${e}`);
  388. } else {
  389. fail(`Operation should be allowed, got status ${getErrorCode(e)}`);
  390. }
  391. }
  392. }
  393. async function assertRequestForbidden<V>(operation: DocumentNode, variables: V) {
  394. try {
  395. const status = await shopClient.query(operation, variables);
  396. fail(`Should have thrown`);
  397. } catch (e) {
  398. expect(getErrorCode(e)).toBe('FORBIDDEN');
  399. }
  400. }
  401. function getErrorCode(err: any): string {
  402. return err.response.errors[0].extensions.code;
  403. }
  404. async function createAdministratorWithPermissions(
  405. code: string,
  406. permissions: Permission[],
  407. ): Promise<{ identifier: string; password: string }> {
  408. const roleResult = await shopClient.query<CreateRole.Mutation, CreateRole.Variables>(CREATE_ROLE, {
  409. input: {
  410. code,
  411. description: '',
  412. permissions,
  413. },
  414. });
  415. const role = roleResult.createRole;
  416. const identifier = `${code}@${Math.random()
  417. .toString(16)
  418. .substr(2, 8)}`;
  419. const password = `test`;
  420. const adminResult = await shopClient.query<
  421. CreateAdministrator.Mutation,
  422. CreateAdministrator.Variables
  423. >(CREATE_ADMINISTRATOR, {
  424. input: {
  425. emailAddress: identifier,
  426. firstName: code,
  427. lastName: 'Admin',
  428. password,
  429. roleIds: [role.id],
  430. },
  431. });
  432. const admin = adminResult.createAdministrator;
  433. return {
  434. identifier,
  435. password,
  436. };
  437. }
  438. /**
  439. * A "sleep" function which allows the sendEmailFn time to get called.
  440. */
  441. function waitForSendEmailFn() {
  442. return new Promise(resolve => setTimeout(resolve, 10));
  443. }
  444. });
  445. describe('Expiring tokens', () => {
  446. const shopClient = new TestShopClient();
  447. const adminClient = new TestAdminClient();
  448. const server = new TestServer();
  449. beforeAll(async () => {
  450. const token = await server.init(
  451. {
  452. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  453. customerCount: 1,
  454. },
  455. {
  456. plugins: [TestEmailPlugin],
  457. authOptions: {
  458. verificationTokenDuration: '1ms',
  459. },
  460. },
  461. );
  462. await shopClient.init();
  463. await adminClient.init();
  464. }, TEST_SETUP_TIMEOUT_MS);
  465. beforeEach(() => {
  466. sendEmailFn = jest.fn();
  467. });
  468. afterAll(async () => {
  469. await server.destroy();
  470. });
  471. it(
  472. 'attempting to verify after token has expired throws',
  473. assertThrowsWithMessage(async () => {
  474. const verificationTokenPromise = getVerificationTokenPromise();
  475. const input: RegisterCustomerInput = {
  476. firstName: 'Barry',
  477. lastName: 'Wallace',
  478. emailAddress: 'barry.wallace@test.com',
  479. };
  480. const result = await shopClient.query<Register.Mutation, Register.Variables>(REGISTER_ACCOUNT, {
  481. input,
  482. });
  483. const verificationToken = await verificationTokenPromise;
  484. expect(result.registerCustomerAccount).toBe(true);
  485. expect(sendEmailFn).toHaveBeenCalledTimes(1);
  486. expect(verificationToken).toBeDefined();
  487. await new Promise(resolve => setTimeout(resolve, 3));
  488. return shopClient.query(VERIFY_EMAIL, {
  489. password: 'test',
  490. token: verificationToken,
  491. });
  492. }, `Verification token has expired. Use refreshCustomerVerification to send a new token.`),
  493. );
  494. it(
  495. 'attempting to reset password after token has expired throws',
  496. assertThrowsWithMessage(async () => {
  497. const { customer } = await adminClient.query<GetCustomer.Query, GetCustomer.Variables>(
  498. GET_CUSTOMER,
  499. { id: 'T_1' },
  500. );
  501. const passwordResetTokenPromise = getPasswordResetTokenPromise();
  502. const result = await shopClient.query<
  503. RequestPasswordReset.Mutation,
  504. RequestPasswordReset.Variables
  505. >(REQUEST_PASSWORD_RESET, {
  506. identifier: customer!.emailAddress,
  507. });
  508. const passwordResetToken = await passwordResetTokenPromise;
  509. expect(result.requestPasswordReset).toBe(true);
  510. expect(sendEmailFn).toHaveBeenCalledTimes(1);
  511. expect(passwordResetToken).toBeDefined();
  512. await new Promise(resolve => setTimeout(resolve, 3));
  513. return shopClient.query<ResetPassword.Mutation, ResetPassword.Variables>(RESET_PASSWORD, {
  514. password: 'test',
  515. token: passwordResetToken,
  516. });
  517. }, `Password reset token has expired.`),
  518. );
  519. });
  520. describe('Registration without email verification', () => {
  521. const shopClient = new TestShopClient();
  522. const server = new TestServer();
  523. const userEmailAddress = 'glen.beardsley@test.com';
  524. beforeAll(async () => {
  525. const token = await server.init(
  526. {
  527. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  528. customerCount: 1,
  529. },
  530. {
  531. plugins: [TestEmailPlugin],
  532. authOptions: {
  533. requireVerification: false,
  534. },
  535. },
  536. );
  537. await shopClient.init();
  538. }, TEST_SETUP_TIMEOUT_MS);
  539. beforeEach(() => {
  540. sendEmailFn = jest.fn();
  541. });
  542. afterAll(async () => {
  543. await server.destroy();
  544. });
  545. it(
  546. 'errors if no password is provided',
  547. assertThrowsWithMessage(async () => {
  548. const input: RegisterCustomerInput = {
  549. firstName: 'Glen',
  550. lastName: 'Beardsley',
  551. emailAddress: userEmailAddress,
  552. };
  553. const result = await shopClient.query<Register.Mutation, Register.Variables>(REGISTER_ACCOUNT, {
  554. input,
  555. });
  556. }, 'A password must be provided when `authOptions.requireVerification` is set to "false"'),
  557. );
  558. it('register a new account with password', async () => {
  559. const input: RegisterCustomerInput = {
  560. firstName: 'Glen',
  561. lastName: 'Beardsley',
  562. emailAddress: userEmailAddress,
  563. password: 'test',
  564. };
  565. const result = await shopClient.query<Register.Mutation, Register.Variables>(REGISTER_ACCOUNT, {
  566. input,
  567. });
  568. expect(result.registerCustomerAccount).toBe(true);
  569. expect(sendEmailFn).not.toHaveBeenCalled();
  570. });
  571. it('can login after registering', async () => {
  572. await shopClient.asUserWithCredentials(userEmailAddress, 'test');
  573. const result = await shopClient.query(
  574. gql`
  575. query GetMe {
  576. me {
  577. identifier
  578. }
  579. }
  580. `,
  581. );
  582. expect(result.me.identifier).toBe(userEmailAddress);
  583. });
  584. });
  585. describe('Updating email address without email verification', () => {
  586. const shopClient = new TestShopClient();
  587. const adminClient = new TestAdminClient();
  588. const server = new TestServer();
  589. let customer: GetCustomer.Customer;
  590. const NEW_EMAIL_ADDRESS = 'new@address.com';
  591. beforeAll(async () => {
  592. const token = await server.init(
  593. {
  594. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  595. customerCount: 1,
  596. },
  597. {
  598. plugins: [TestEmailPlugin],
  599. authOptions: {
  600. requireVerification: false,
  601. },
  602. },
  603. );
  604. await shopClient.init();
  605. await adminClient.init();
  606. }, TEST_SETUP_TIMEOUT_MS);
  607. beforeAll(async () => {
  608. const result = await adminClient.query<GetCustomer.Query, GetCustomer.Variables>(GET_CUSTOMER, {
  609. id: 'T_1',
  610. });
  611. customer = result.customer!;
  612. });
  613. beforeEach(() => {
  614. sendEmailFn = jest.fn();
  615. });
  616. afterAll(async () => {
  617. await server.destroy();
  618. });
  619. it('updates email address', async () => {
  620. await shopClient.asUserWithCredentials(customer.emailAddress, 'test');
  621. const { requestUpdateCustomerEmailAddress } = await shopClient.query<
  622. RequestUpdateEmailAddress.Mutation,
  623. RequestUpdateEmailAddress.Variables
  624. >(REQUEST_UPDATE_EMAIL_ADDRESS, {
  625. password: 'test',
  626. newEmailAddress: NEW_EMAIL_ADDRESS,
  627. });
  628. expect(requestUpdateCustomerEmailAddress).toBe(true);
  629. expect(sendEmailFn).toHaveBeenCalledTimes(1);
  630. expect(sendEmailFn.mock.calls[0][0] instanceof IdentifierChangeEvent).toBe(true);
  631. const { activeCustomer } = await shopClient.query<GetActiveCustomer.Query>(GET_ACTIVE_CUSTOMER);
  632. expect(activeCustomer!.emailAddress).toBe(NEW_EMAIL_ADDRESS);
  633. });
  634. });
  635. /**
  636. * This mock plugin simulates an EmailPlugin which would send emails
  637. * on the registration & password reset events.
  638. */
  639. @VendurePlugin({
  640. imports: [EventBusModule],
  641. })
  642. class TestEmailPlugin implements OnModuleInit {
  643. constructor(private eventBus: EventBus) {}
  644. onModuleInit() {
  645. this.eventBus.subscribe(AccountRegistrationEvent, event => {
  646. sendEmailFn(event);
  647. });
  648. this.eventBus.subscribe(PasswordResetEvent, event => {
  649. sendEmailFn(event);
  650. });
  651. this.eventBus.subscribe(IdentifierChangeRequestEvent, event => {
  652. sendEmailFn(event);
  653. });
  654. this.eventBus.subscribe(IdentifierChangeEvent, event => {
  655. sendEmailFn(event);
  656. });
  657. }
  658. }
  659. function getVerificationTokenPromise(): Promise<string> {
  660. return new Promise<any>(resolve => {
  661. sendEmailFn.mockImplementation((event: AccountRegistrationEvent) => {
  662. resolve(event.user.verificationToken);
  663. });
  664. });
  665. }
  666. function getPasswordResetTokenPromise(): Promise<string> {
  667. return new Promise<any>(resolve => {
  668. sendEmailFn.mockImplementation((event: PasswordResetEvent) => {
  669. resolve(event.user.passwordResetToken);
  670. });
  671. });
  672. }
  673. function getEmailUpdateTokenPromise(): Promise<{
  674. identifierChangeToken: string | null;
  675. pendingIdentifier: string | null;
  676. }> {
  677. return new Promise(resolve => {
  678. sendEmailFn.mockImplementation((event: IdentifierChangeRequestEvent) => {
  679. resolve(pick(event.user, ['identifierChangeToken', 'pendingIdentifier']));
  680. });
  681. });
  682. }