Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/authentication/two_factor_auth')
-rw-r--r--spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js242
-rw-r--r--spec/frontend/authentication/two_factor_auth/index_spec.js80
-rw-r--r--spec/frontend/authentication/two_factor_auth/mock_data.js31
3 files changed, 353 insertions, 0 deletions
diff --git a/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js b/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js
new file mode 100644
index 00000000000..025e605b920
--- /dev/null
+++ b/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js
@@ -0,0 +1,242 @@
+import { mount } from '@vue/test-utils';
+import { GlAlert, GlButton } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { within } from '@testing-library/dom';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import Tracking from '~/tracking';
+import RecoveryCodes, {
+ i18n,
+} from '~/authentication/two_factor_auth/components/recovery_codes.vue';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import {
+ RECOVERY_CODE_DOWNLOAD_FILENAME,
+ COPY_KEYBOARD_SHORTCUT,
+} from '~/authentication/two_factor_auth/constants';
+import { codes, codesFormattedString, codesDownloadHref, profileAccountPath } from '../mock_data';
+
+describe('RecoveryCodes', () => {
+ let wrapper;
+
+ const createComponent = (options = {}) => {
+ wrapper = extendedWrapper(
+ mount(RecoveryCodes, {
+ propsData: {
+ codes,
+ profileAccountPath,
+ ...(options?.propsData || {}),
+ },
+ ...options,
+ }),
+ );
+ };
+
+ const queryByText = (text, options) => within(wrapper.element).queryByText(text, options);
+ const findAlert = () => wrapper.find(GlAlert);
+ const findRecoveryCodes = () => wrapper.findByTestId('recovery-codes');
+ const findCopyButton = () => wrapper.find(ClipboardButton);
+ const findButtonByText = text =>
+ wrapper.findAll(GlButton).wrappers.find(buttonWrapper => buttonWrapper.text() === text);
+ const findDownloadButton = () => findButtonByText('Download codes');
+ const findPrintButton = () => findButtonByText('Print codes');
+ const findProceedButton = () => findButtonByText('Proceed');
+ const manuallyCopyRecoveryCodes = () =>
+ wrapper.vm.$options.mousetrap.trigger(COPY_KEYBOARD_SHORTCUT);
+
+ beforeEach(() => {
+ jest.spyOn(Tracking, 'event');
+ createComponent();
+ });
+
+ it('renders title', () => {
+ expect(queryByText(i18n.pageTitle)).toEqual(expect.any(HTMLElement));
+ });
+
+ it('renders alert', () => {
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(i18n.alertTitle);
+ });
+
+ it('renders codes', () => {
+ const recoveryCodes = findRecoveryCodes().text();
+
+ codes.forEach(code => {
+ expect(recoveryCodes).toContain(code);
+ });
+ });
+
+ describe('"Proceed" button', () => {
+ it('renders button as disabled', () => {
+ const proceedButton = findProceedButton();
+
+ expect(proceedButton.exists()).toBe(true);
+ expect(proceedButton.props('disabled')).toBe(true);
+ expect(proceedButton.attributes()).toMatchObject({
+ title: i18n.proceedButton,
+ href: profileAccountPath,
+ });
+ });
+
+ it('fires Snowplow event', () => {
+ expect(findProceedButton().attributes()).toMatchObject({
+ 'data-track-event': 'click_button',
+ 'data-track-label': '2fa_recovery_codes_proceed_button',
+ });
+ });
+ });
+
+ describe('"Copy codes" button', () => {
+ it('renders button', () => {
+ const copyButton = findCopyButton();
+
+ expect(copyButton.exists()).toBe(true);
+ expect(copyButton.text()).toBe(i18n.copyButton);
+ expect(copyButton.props()).toMatchObject({
+ title: i18n.copyButton,
+ text: codesFormattedString,
+ });
+ });
+
+ describe('when button is clicked', () => {
+ beforeEach(async () => {
+ findCopyButton().trigger('click');
+
+ await nextTick();
+ });
+
+ it('enables "Proceed" button', () => {
+ expect(findProceedButton().props('disabled')).toBe(false);
+ });
+
+ it('fires Snowplow event', () => {
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
+ label: '2fa_recovery_codes_copy_button',
+ });
+ });
+ });
+ });
+
+ describe('"Download codes" button', () => {
+ it('renders button', () => {
+ const downloadButton = findDownloadButton();
+
+ expect(downloadButton.exists()).toBe(true);
+ expect(downloadButton.attributes()).toMatchObject({
+ title: i18n.downloadButton,
+ download: RECOVERY_CODE_DOWNLOAD_FILENAME,
+ href: codesDownloadHref,
+ });
+ });
+
+ describe('when button is clicked', () => {
+ beforeEach(async () => {
+ const downloadButton = findDownloadButton();
+ // jsdom does not support navigating.
+ // Since we are clicking an anchor tag there is no way to mock this
+ // and we are forced to instead remove the `href` attribute.
+ // More info: https://github.com/jsdom/jsdom/issues/2112#issuecomment-663672587
+ downloadButton.element.removeAttribute('href');
+ downloadButton.trigger('click');
+
+ await nextTick();
+ });
+
+ it('enables "Proceed" button', () => {
+ expect(findProceedButton().props('disabled')).toBe(false);
+ });
+
+ it('fires Snowplow event', () => {
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
+ label: '2fa_recovery_codes_download_button',
+ });
+ });
+ });
+ });
+
+ describe('"Print codes" button', () => {
+ it('renders button', () => {
+ const printButton = findPrintButton();
+
+ expect(printButton.exists()).toBe(true);
+ expect(printButton.attributes()).toMatchObject({
+ title: i18n.printButton,
+ });
+ });
+
+ describe('when button is clicked', () => {
+ beforeEach(async () => {
+ window.print = jest.fn();
+
+ findPrintButton().trigger('click');
+
+ await nextTick();
+ });
+
+ it('enables "Proceed" button and opens print dialog', () => {
+ expect(findProceedButton().props('disabled')).toBe(false);
+ expect(window.print).toHaveBeenCalled();
+ });
+
+ it('fires Snowplow event', () => {
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
+ label: '2fa_recovery_codes_print_button',
+ });
+ });
+ });
+ });
+
+ describe('when codes are manually copied', () => {
+ describe('when selected text is the recovery codes', () => {
+ beforeEach(async () => {
+ jest.spyOn(window, 'getSelection').mockImplementation(() => ({
+ toString: jest.fn(() => codesFormattedString),
+ }));
+
+ manuallyCopyRecoveryCodes();
+
+ await nextTick();
+ });
+
+ it('enables "Proceed" button', () => {
+ expect(findProceedButton().props('disabled')).toBe(false);
+ });
+
+ it('fires Snowplow event', () => {
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, 'copy_keyboard_shortcut', {
+ label: '2fa_recovery_codes_manual_copy',
+ });
+ });
+ });
+
+ describe('when selected text includes the recovery codes', () => {
+ beforeEach(() => {
+ jest.spyOn(window, 'getSelection').mockImplementation(() => ({
+ toString: jest.fn(() => `foo bar ${codesFormattedString}`),
+ }));
+ });
+
+ it('enables "Proceed" button', async () => {
+ manuallyCopyRecoveryCodes();
+
+ await nextTick();
+
+ expect(findProceedButton().props('disabled')).toBe(false);
+ });
+ });
+
+ describe('when selected text does not include the recovery codes', () => {
+ beforeEach(() => {
+ jest.spyOn(window, 'getSelection').mockImplementation(() => ({
+ toString: jest.fn(() => 'foo bar'),
+ }));
+ });
+
+ it('keeps "Proceed" button disabled', async () => {
+ manuallyCopyRecoveryCodes();
+
+ await nextTick();
+
+ expect(findProceedButton().props('disabled')).toBe(true);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/authentication/two_factor_auth/index_spec.js b/spec/frontend/authentication/two_factor_auth/index_spec.js
new file mode 100644
index 00000000000..b181170b0a1
--- /dev/null
+++ b/spec/frontend/authentication/two_factor_auth/index_spec.js
@@ -0,0 +1,80 @@
+import { createWrapper } from '@vue/test-utils';
+import { getByTestId, fireEvent } from '@testing-library/dom';
+import * as urlUtils from '~/lib/utils/url_utility';
+import { initRecoveryCodes, initClose2faSuccessMessage } from '~/authentication/two_factor_auth';
+import RecoveryCodes from '~/authentication/two_factor_auth/components/recovery_codes.vue';
+import { codesJsonString, codes, profileAccountPath } from './mock_data';
+
+describe('initRecoveryCodes', () => {
+ let el;
+ let wrapper;
+
+ const findRecoveryCodesComponent = () => wrapper.find(RecoveryCodes);
+
+ beforeEach(() => {
+ el = document.createElement('div');
+ el.setAttribute('class', 'js-2fa-recovery-codes');
+ el.setAttribute('data-codes', codesJsonString);
+ el.setAttribute('data-profile-account-path', profileAccountPath);
+ document.body.appendChild(el);
+
+ wrapper = createWrapper(initRecoveryCodes());
+ });
+
+ afterEach(() => {
+ document.body.innerHTML = '';
+ });
+
+ it('parses `data-codes` and passes to `RecoveryCodes` as `codes` prop', () => {
+ expect(findRecoveryCodesComponent().props('codes')).toEqual(codes);
+ });
+
+ it('parses `data-profile-account-path` and passes to `RecoveryCodes` as `profileAccountPath` prop', () => {
+ expect(findRecoveryCodesComponent().props('profileAccountPath')).toEqual(profileAccountPath);
+ });
+});
+
+describe('initClose2faSuccessMessage', () => {
+ beforeEach(() => {
+ document.body.innerHTML = `
+ <button
+ data-testid="close-2fa-enabled-success-alert"
+ class="js-close-2fa-enabled-success-alert"
+ >
+ </button>
+ `;
+
+ initClose2faSuccessMessage();
+ });
+
+ afterEach(() => {
+ document.body.innerHTML = '';
+ });
+
+ describe('when alert is closed', () => {
+ beforeEach(() => {
+ delete window.location;
+ window.location = new URL(
+ 'https://localhost/-/profile/account?two_factor_auth_enabled_successfully=true',
+ );
+
+ document.title = 'foo bar';
+
+ urlUtils.updateHistory = jest.fn();
+ });
+
+ afterEach(() => {
+ document.title = '';
+ });
+
+ it('removes `two_factor_auth_enabled_successfully` query param', () => {
+ fireEvent.click(getByTestId(document.body, 'close-2fa-enabled-success-alert'));
+
+ expect(urlUtils.updateHistory).toHaveBeenCalledWith({
+ url: 'https://localhost/-/profile/account',
+ title: 'foo bar',
+ replace: true,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/authentication/two_factor_auth/mock_data.js b/spec/frontend/authentication/two_factor_auth/mock_data.js
new file mode 100644
index 00000000000..7b2a1764abd
--- /dev/null
+++ b/spec/frontend/authentication/two_factor_auth/mock_data.js
@@ -0,0 +1,31 @@
+export const codes = [
+ 'e8471c403a6a84c0',
+ 'b1b92de21c68f08e',
+ 'd7689f332cd8cd73',
+ '05b706accfa95cfa',
+ 'b0a2b45ea956c1d2',
+ '599dc672d18d5161',
+ 'e14e9f4adf4b8bf2',
+ '1013007a75efeeec',
+ '26bd057c4c696a4f',
+ '1c46fba5a4275ef4',
+];
+
+export const codesJsonString =
+ '["e8471c403a6a84c0","b1b92de21c68f08e","d7689f332cd8cd73","05b706accfa95cfa","b0a2b45ea956c1d2","599dc672d18d5161","e14e9f4adf4b8bf2","1013007a75efeeec","26bd057c4c696a4f","1c46fba5a4275ef4"]';
+
+export const codesFormattedString = `e8471c403a6a84c0
+b1b92de21c68f08e
+d7689f332cd8cd73
+05b706accfa95cfa
+b0a2b45ea956c1d2
+599dc672d18d5161
+e14e9f4adf4b8bf2
+1013007a75efeeec
+26bd057c4c696a4f
+1c46fba5a4275ef4`;
+
+export const codesDownloadHref =
+ 'data:text/plain;charset=utf-8,e8471c403a6a84c0%0Ab1b92de21c68f08e%0Ad7689f332cd8cd73%0A05b706accfa95cfa%0Ab0a2b45ea956c1d2%0A599dc672d18d5161%0Ae14e9f4adf4b8bf2%0A1013007a75efeeec%0A26bd057c4c696a4f%0A1c46fba5a4275ef4';
+
+export const profileAccountPath = '/-/profile/account';