shop-auth.e2e-spec.ts 27 KB

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