shop-auth.e2e-spec.ts 33 KB

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