shop-auth.e2e-spec.ts 26 KB

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