shop-auth.e2e-spec.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. /* tslint:disable:no-non-null-assertion */
  2. import { DocumentNode } from 'graphql';
  3. import gql from 'graphql-tag';
  4. import path from 'path';
  5. import {
  6. CREATE_ADMINISTRATOR,
  7. CREATE_ROLE,
  8. } from '../../admin-ui/src/app/data/definitions/administrator-definitions';
  9. import { GET_CUSTOMER } from '../../admin-ui/src/app/data/definitions/customer-definitions';
  10. import { RegisterCustomerInput } from '../../shared/generated-shop-types';
  11. import { CreateAdministrator, CreateRole, GetCustomer, Permission } from '../../shared/generated-types';
  12. import { NoopEmailGenerator } from '../src/config/email/noop-email-generator';
  13. import { defaultEmailTypes } from '../src/email/default-email-types';
  14. import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
  15. import { TestAdminClient, TestShopClient } from './test-client';
  16. import { TestServer } from './test-server';
  17. import { assertThrowsWithMessage } from './test-utils';
  18. let sendEmailFn: jest.Mock;
  19. const emailOptions = {
  20. emailTemplatePath: 'src/email/templates',
  21. emailTypes: defaultEmailTypes,
  22. generator: new NoopEmailGenerator(),
  23. transport: {
  24. type: 'testing' as 'testing',
  25. onSend: ctx => sendEmailFn(ctx),
  26. },
  27. };
  28. describe('Shop auth & accounts', () => {
  29. const shopClient = new TestShopClient();
  30. const adminClient = new TestAdminClient();
  31. const server = new TestServer();
  32. beforeAll(async () => {
  33. const token = await server.init(
  34. {
  35. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  36. customerCount: 1,
  37. },
  38. {
  39. emailOptions,
  40. },
  41. );
  42. await shopClient.init();
  43. await adminClient.init();
  44. }, TEST_SETUP_TIMEOUT_MS);
  45. afterAll(async () => {
  46. await server.destroy();
  47. });
  48. describe('customer account creation', () => {
  49. const password = 'password';
  50. const emailAddress = 'test1@test.com';
  51. let verificationToken: string;
  52. beforeEach(() => {
  53. sendEmailFn = jest.fn();
  54. });
  55. it(
  56. 'errors if a password is provided',
  57. assertThrowsWithMessage(async () => {
  58. const input: RegisterCustomerInput = {
  59. firstName: 'Sofia',
  60. lastName: 'Green',
  61. emailAddress: 'sofia.green@test.com',
  62. password: 'test',
  63. };
  64. const result = await shopClient.query(REGISTER_ACCOUNT, { input });
  65. }, 'Do not provide a password when `authOptions.requireVerification` is set to "true"'),
  66. );
  67. it('register a new account', async () => {
  68. const verificationTokenPromise = getVerificationTokenPromise();
  69. const input: RegisterCustomerInput = {
  70. firstName: 'Sean',
  71. lastName: 'Tester',
  72. emailAddress,
  73. };
  74. const result = await shopClient.query(REGISTER_ACCOUNT, { input });
  75. verificationToken = await verificationTokenPromise;
  76. expect(result.registerCustomerAccount).toBe(true);
  77. expect(sendEmailFn).toHaveBeenCalled();
  78. expect(verificationToken).toBeDefined();
  79. });
  80. it('issues a new token if attempting to register a second time', async () => {
  81. const sendEmail = new Promise<string>(resolve => {
  82. sendEmailFn.mockImplementation(ctx => {
  83. resolve(ctx.event.user.verificationToken);
  84. });
  85. });
  86. const input: RegisterCustomerInput = {
  87. firstName: 'Sean',
  88. lastName: 'Tester',
  89. emailAddress,
  90. };
  91. const result = await shopClient.query(REGISTER_ACCOUNT, { input });
  92. const newVerificationToken = await sendEmail;
  93. expect(result.registerCustomerAccount).toBe(true);
  94. expect(sendEmailFn).toHaveBeenCalled();
  95. expect(newVerificationToken).not.toBe(verificationToken);
  96. verificationToken = newVerificationToken;
  97. });
  98. it('refreshCustomerVerification issues a new token', async () => {
  99. const sendEmail = new Promise<string>(resolve => {
  100. sendEmailFn.mockImplementation(ctx => {
  101. resolve(ctx.event.user.verificationToken);
  102. });
  103. });
  104. const result = await shopClient.query(REFRESH_TOKEN, { emailAddress });
  105. const newVerificationToken = await sendEmail;
  106. expect(result.refreshCustomerVerification).toBe(true);
  107. expect(sendEmailFn).toHaveBeenCalled();
  108. expect(newVerificationToken).not.toBe(verificationToken);
  109. verificationToken = newVerificationToken;
  110. });
  111. it('refreshCustomerVerification does nothing with an unrecognized emailAddress', async () => {
  112. const result = await shopClient.query(REFRESH_TOKEN, {
  113. emailAddress: 'never-been-registered@test.com',
  114. });
  115. await waitForSendEmailFn();
  116. expect(result.refreshCustomerVerification).toBe(true);
  117. expect(sendEmailFn).not.toHaveBeenCalled();
  118. });
  119. it('login fails before verification', async () => {
  120. try {
  121. await shopClient.asUserWithCredentials(emailAddress, '');
  122. fail('should have thrown');
  123. } catch (err) {
  124. expect(getErrorCode(err)).toBe('UNAUTHORIZED');
  125. }
  126. });
  127. it(
  128. 'verification fails with wrong token',
  129. assertThrowsWithMessage(
  130. () =>
  131. shopClient.query(VERIFY_EMAIL, {
  132. password,
  133. token: 'bad-token',
  134. }),
  135. `Verification token not recognized`,
  136. ),
  137. );
  138. it('verification succeeds with correct token', async () => {
  139. const result = await shopClient.query(VERIFY_EMAIL, {
  140. password,
  141. token: verificationToken,
  142. });
  143. expect(result.verifyCustomerAccount.user.identifier).toBe('test1@test.com');
  144. });
  145. it('registration silently fails if attempting to register an email already verified', async () => {
  146. const input: RegisterCustomerInput = {
  147. firstName: 'Dodgy',
  148. lastName: 'Hacker',
  149. emailAddress,
  150. };
  151. const result = await shopClient.query(REGISTER_ACCOUNT, { input });
  152. await waitForSendEmailFn();
  153. expect(result.registerCustomerAccount).toBe(true);
  154. expect(sendEmailFn).not.toHaveBeenCalled();
  155. });
  156. it(
  157. 'verification fails if attempted a second time',
  158. assertThrowsWithMessage(
  159. () =>
  160. shopClient.query(VERIFY_EMAIL, {
  161. password,
  162. token: verificationToken,
  163. }),
  164. `Verification token not recognized`,
  165. ),
  166. );
  167. });
  168. describe('password reset', () => {
  169. let passwordResetToken: string;
  170. let customer: GetCustomer.Customer;
  171. beforeAll(async () => {
  172. const result = await adminClient.query<GetCustomer.Query, GetCustomer.Variables>(GET_CUSTOMER, {
  173. id: 'T_1',
  174. });
  175. customer = result.customer!;
  176. });
  177. beforeEach(() => {
  178. sendEmailFn = jest.fn();
  179. });
  180. it('requestPasswordReset silently fails with invalid identifier', async () => {
  181. const result = await shopClient.query(REQUEST_PASSWORD_RESET, {
  182. identifier: 'invalid-identifier',
  183. });
  184. await waitForSendEmailFn();
  185. expect(result.requestPasswordReset).toBe(true);
  186. expect(sendEmailFn).not.toHaveBeenCalled();
  187. expect(passwordResetToken).not.toBeDefined();
  188. });
  189. it('requestPasswordReset sends reset token', async () => {
  190. const passwordResetTokenPromise = getPasswordResetTokenPromise();
  191. const result = await shopClient.query(REQUEST_PASSWORD_RESET, {
  192. identifier: customer.emailAddress,
  193. });
  194. passwordResetToken = await passwordResetTokenPromise;
  195. expect(result.requestPasswordReset).toBe(true);
  196. expect(sendEmailFn).toHaveBeenCalled();
  197. expect(passwordResetToken).toBeDefined();
  198. });
  199. it(
  200. 'resetPassword fails with wrong token',
  201. assertThrowsWithMessage(
  202. () =>
  203. shopClient.query(RESET_PASSWORD, {
  204. password: 'newPassword',
  205. token: 'bad-token',
  206. }),
  207. `Password reset token not recognized`,
  208. ),
  209. );
  210. it('resetPassword works with valid token', async () => {
  211. const result = await shopClient.query(RESET_PASSWORD, {
  212. token: passwordResetToken,
  213. password: 'newPassword',
  214. });
  215. const loginResult = await shopClient.asUserWithCredentials(customer.emailAddress, 'newPassword');
  216. expect(loginResult.user.identifier).toBe(customer.emailAddress);
  217. });
  218. });
  219. async function assertRequestAllowed<V>(operation: DocumentNode, variables?: V) {
  220. try {
  221. const status = await shopClient.queryStatus(operation, variables);
  222. expect(status).toBe(200);
  223. } catch (e) {
  224. const errorCode = getErrorCode(e);
  225. if (!errorCode) {
  226. fail(`Unexpected failure: ${e}`);
  227. } else {
  228. fail(`Operation should be allowed, got status ${getErrorCode(e)}`);
  229. }
  230. }
  231. }
  232. async function assertRequestForbidden<V>(operation: DocumentNode, variables: V) {
  233. try {
  234. const status = await shopClient.query(operation, variables);
  235. fail(`Should have thrown`);
  236. } catch (e) {
  237. expect(getErrorCode(e)).toBe('FORBIDDEN');
  238. }
  239. }
  240. function getErrorCode(err: any): string {
  241. return err.response.errors[0].extensions.code;
  242. }
  243. async function createAdministratorWithPermissions(
  244. code: string,
  245. permissions: Permission[],
  246. ): Promise<{ identifier: string; password: string }> {
  247. const roleResult = await shopClient.query<CreateRole.Mutation, CreateRole.Variables>(CREATE_ROLE, {
  248. input: {
  249. code,
  250. description: '',
  251. permissions,
  252. },
  253. });
  254. const role = roleResult.createRole;
  255. const identifier = `${code}@${Math.random()
  256. .toString(16)
  257. .substr(2, 8)}`;
  258. const password = `test`;
  259. const adminResult = await shopClient.query<
  260. CreateAdministrator.Mutation,
  261. CreateAdministrator.Variables
  262. >(CREATE_ADMINISTRATOR, {
  263. input: {
  264. emailAddress: identifier,
  265. firstName: code,
  266. lastName: 'Admin',
  267. password,
  268. roleIds: [role.id],
  269. },
  270. });
  271. const admin = adminResult.createAdministrator;
  272. return {
  273. identifier,
  274. password,
  275. };
  276. }
  277. /**
  278. * A "sleep" function which allows the sendEmailFn time to get called.
  279. */
  280. function waitForSendEmailFn() {
  281. return new Promise(resolve => setTimeout(resolve, 10));
  282. }
  283. });
  284. describe('Expiring tokens', () => {
  285. const shopClient = new TestShopClient();
  286. const adminClient = new TestAdminClient();
  287. const server = new TestServer();
  288. beforeAll(async () => {
  289. const token = await server.init(
  290. {
  291. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  292. customerCount: 1,
  293. },
  294. {
  295. emailOptions,
  296. authOptions: {
  297. verificationTokenDuration: '1ms',
  298. },
  299. },
  300. );
  301. await shopClient.init();
  302. await adminClient.init();
  303. }, TEST_SETUP_TIMEOUT_MS);
  304. beforeEach(() => {
  305. sendEmailFn = jest.fn();
  306. });
  307. afterAll(async () => {
  308. await server.destroy();
  309. });
  310. it(
  311. 'attempting to verify after token has expired throws',
  312. assertThrowsWithMessage(async () => {
  313. const verificationTokenPromise = getVerificationTokenPromise();
  314. const input: RegisterCustomerInput = {
  315. firstName: 'Barry',
  316. lastName: 'Wallace',
  317. emailAddress: 'barry.wallace@test.com',
  318. };
  319. const result = await shopClient.query(REGISTER_ACCOUNT, { input });
  320. const verificationToken = await verificationTokenPromise;
  321. expect(result.registerCustomerAccount).toBe(true);
  322. expect(sendEmailFn).toHaveBeenCalledTimes(1);
  323. expect(verificationToken).toBeDefined();
  324. await new Promise(resolve => setTimeout(resolve, 3));
  325. return shopClient.query(VERIFY_EMAIL, {
  326. password: 'test',
  327. token: verificationToken,
  328. });
  329. }, `Verification token has expired. Use refreshCustomerVerification to send a new token.`),
  330. );
  331. it(
  332. 'attempting to reset password after token has expired throws',
  333. assertThrowsWithMessage(async () => {
  334. const { customer } = await adminClient.query<GetCustomer.Query, GetCustomer.Variables>(
  335. GET_CUSTOMER,
  336. { id: 'T_1' },
  337. );
  338. const passwordResetTokenPromise = getPasswordResetTokenPromise();
  339. const result = await shopClient.query(REQUEST_PASSWORD_RESET, {
  340. identifier: customer!.emailAddress,
  341. });
  342. const passwordResetToken = await passwordResetTokenPromise;
  343. expect(result.requestPasswordReset).toBe(true);
  344. expect(sendEmailFn).toHaveBeenCalledTimes(1);
  345. expect(passwordResetToken).toBeDefined();
  346. await new Promise(resolve => setTimeout(resolve, 3));
  347. return shopClient.query(RESET_PASSWORD, {
  348. password: 'test',
  349. token: passwordResetToken,
  350. });
  351. }, `Password reset token has expired.`),
  352. );
  353. });
  354. describe('Registration without email verification', () => {
  355. const shopClient = new TestShopClient();
  356. const server = new TestServer();
  357. const userEmailAddress = 'glen.beardsley@test.com';
  358. beforeAll(async () => {
  359. const token = await server.init(
  360. {
  361. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  362. customerCount: 1,
  363. },
  364. {
  365. emailOptions,
  366. authOptions: {
  367. requireVerification: false,
  368. },
  369. },
  370. );
  371. await shopClient.init();
  372. }, TEST_SETUP_TIMEOUT_MS);
  373. beforeEach(() => {
  374. sendEmailFn = jest.fn();
  375. });
  376. afterAll(async () => {
  377. await server.destroy();
  378. });
  379. it(
  380. 'errors if no password is provided',
  381. assertThrowsWithMessage(async () => {
  382. const input: RegisterCustomerInput = {
  383. firstName: 'Glen',
  384. lastName: 'Beardsley',
  385. emailAddress: userEmailAddress,
  386. };
  387. const result = await shopClient.query(REGISTER_ACCOUNT, { input });
  388. }, 'A password must be provided when `authOptions.requireVerification` is set to "false"'),
  389. );
  390. it('register a new account with password', async () => {
  391. const input: RegisterCustomerInput = {
  392. firstName: 'Glen',
  393. lastName: 'Beardsley',
  394. emailAddress: userEmailAddress,
  395. password: 'test',
  396. };
  397. const result = await shopClient.query(REGISTER_ACCOUNT, { input });
  398. expect(result.registerCustomerAccount).toBe(true);
  399. expect(sendEmailFn).not.toHaveBeenCalled();
  400. });
  401. it('can login after registering', async () => {
  402. await shopClient.asUserWithCredentials(userEmailAddress, 'test');
  403. const result = await shopClient.query(
  404. gql`
  405. query {
  406. me {
  407. identifier
  408. }
  409. }
  410. `,
  411. );
  412. expect(result.me.identifier).toBe(userEmailAddress);
  413. });
  414. });
  415. function getVerificationTokenPromise(): Promise<string> {
  416. return new Promise<string>(resolve => {
  417. sendEmailFn.mockImplementation(ctx => {
  418. resolve(ctx.event.user.verificationToken);
  419. });
  420. });
  421. }
  422. function getPasswordResetTokenPromise(): Promise<string> {
  423. return new Promise<string>(resolve => {
  424. sendEmailFn.mockImplementation(ctx => {
  425. resolve(ctx.event.user.passwordResetToken);
  426. });
  427. });
  428. }
  429. const REGISTER_ACCOUNT = gql`
  430. mutation Register($input: RegisterCustomerInput!) {
  431. registerCustomerAccount(input: $input)
  432. }
  433. `;
  434. const VERIFY_EMAIL = gql`
  435. mutation Verify($password: String!, $token: String!) {
  436. verifyCustomerAccount(password: $password, token: $token) {
  437. user {
  438. id
  439. identifier
  440. }
  441. }
  442. }
  443. `;
  444. const REFRESH_TOKEN = gql`
  445. mutation RefreshToken($emailAddress: String!) {
  446. refreshCustomerVerification(emailAddress: $emailAddress)
  447. }
  448. `;
  449. const REQUEST_PASSWORD_RESET = gql`
  450. mutation RequestPasswordReset($identifier: String!) {
  451. requestPasswordReset(emailAddress: $identifier)
  452. }
  453. `;
  454. const RESET_PASSWORD = gql`
  455. mutation ResetPassword($token: String!, $password: String!) {
  456. resetPassword(token: $token, password: $password) {
  457. user {
  458. id
  459. identifier
  460. }
  461. }
  462. }
  463. `;