summaryrefslogtreecommitdiff
path: root/spec/frontend/jira_connect
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-03-18 20:02:30 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-03-18 20:02:30 +0000
commit41fe97390ceddf945f3d967b8fdb3de4c66b7dea (patch)
tree9c8d89a8624828992f06d892cd2f43818ff5dcc8 /spec/frontend/jira_connect
parent0804d2dc31052fb45a1efecedc8e06ce9bc32862 (diff)
downloadgitlab-ce-41fe97390ceddf945f3d967b8fdb3de4c66b7dea.tar.gz
Add latest changes from gitlab-org/gitlab@14-9-stable-eev14.9.0-rc42
Diffstat (limited to 'spec/frontend/jira_connect')
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/app_spec.js56
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/sign_in_legacy_button_spec.js (renamed from spec/frontend/jira_connect/subscriptions/components/sign_in_button_spec.js)8
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js204
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/user_link_spec.js45
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in_spec.js111
-rw-r--r--spec/frontend/jira_connect/subscriptions/pkce_spec.js48
6 files changed, 424 insertions, 48 deletions
diff --git a/spec/frontend/jira_connect/subscriptions/components/app_spec.js b/spec/frontend/jira_connect/subscriptions/components/app_spec.js
index aa0f1440b20..6b3ca7ffd65 100644
--- a/spec/frontend/jira_connect/subscriptions/components/app_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/app_spec.js
@@ -8,6 +8,7 @@ import SubscriptionsPage from '~/jira_connect/subscriptions/pages/subscriptions.
import UserLink from '~/jira_connect/subscriptions/components/user_link.vue';
import createStore from '~/jira_connect/subscriptions/store';
import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types';
+import { I18N_DEFAULT_SIGN_IN_ERROR_MESSAGE } from '~/jira_connect/subscriptions/constants';
import { __ } from '~/locale';
import { mockSubscription } from '../mock_data';
@@ -24,6 +25,7 @@ describe('JiraConnectApp', () => {
const findAlertLink = () => findAlert().findComponent(GlLink);
const findSignInPage = () => wrapper.findComponent(SignInPage);
const findSubscriptionsPage = () => wrapper.findComponent(SubscriptionsPage);
+ const findUserLink = () => wrapper.findComponent(UserLink);
const createComponent = ({ provide, mountFn = shallowMountExtended } = {}) => {
store = createStore();
@@ -78,10 +80,11 @@ describe('JiraConnectApp', () => {
},
});
- const userLink = wrapper.findComponent(UserLink);
+ const userLink = findUserLink();
expect(userLink.exists()).toBe(true);
expect(userLink.props()).toEqual({
hasSubscriptions: false,
+ user: null,
userSignedIn: false,
});
});
@@ -153,4 +156,55 @@ describe('JiraConnectApp', () => {
});
});
});
+
+ describe('when user signed out', () => {
+ describe('when sign in page emits `sign-in-oauth` event', () => {
+ const mockUser = { name: 'test' };
+ beforeEach(async () => {
+ createComponent({
+ provide: {
+ usersPath: '/mock',
+ subscriptions: [],
+ },
+ });
+ findSignInPage().vm.$emit('sign-in-oauth', mockUser);
+
+ await nextTick();
+ });
+
+ it('hides sign in page and renders subscriptions page', () => {
+ expect(findSignInPage().exists()).toBe(false);
+ expect(findSubscriptionsPage().exists()).toBe(true);
+ });
+
+ it('sets correct UserLink props', () => {
+ expect(findUserLink().props()).toMatchObject({
+ user: mockUser,
+ userSignedIn: true,
+ });
+ });
+ });
+
+ describe('when sign in page emits `error` event', () => {
+ beforeEach(async () => {
+ createComponent({
+ provide: {
+ usersPath: '/mock',
+ subscriptions: [],
+ },
+ });
+ findSignInPage().vm.$emit('error');
+
+ await nextTick();
+ });
+
+ it('displays alert', () => {
+ const alert = findAlert();
+
+ expect(alert.exists()).toBe(true);
+ expect(alert.html()).toContain(I18N_DEFAULT_SIGN_IN_ERROR_MESSAGE);
+ expect(alert.props('variant')).toBe('danger');
+ });
+ });
+ });
});
diff --git a/spec/frontend/jira_connect/subscriptions/components/sign_in_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/sign_in_legacy_button_spec.js
index 94dcf9decec..4ebfaed261e 100644
--- a/spec/frontend/jira_connect/subscriptions/components/sign_in_button_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/sign_in_legacy_button_spec.js
@@ -1,18 +1,18 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { getGitlabSignInURL } from '~/jira_connect/subscriptions/utils';
-import SignInButton from '~/jira_connect/subscriptions/components/sign_in_button.vue';
+import SignInLegacyButton from '~/jira_connect/subscriptions/components/sign_in_legacy_button.vue';
import waitForPromises from 'helpers/wait_for_promises';
const MOCK_USERS_PATH = '/user';
jest.mock('~/jira_connect/subscriptions/utils');
-describe('SignInButton', () => {
+describe('SignInLegacyButton', () => {
let wrapper;
const createComponent = ({ slots } = {}) => {
- wrapper = shallowMount(SignInButton, {
+ wrapper = shallowMount(SignInLegacyButton, {
propsData: {
usersPath: MOCK_USERS_PATH,
},
@@ -30,7 +30,7 @@ describe('SignInButton', () => {
createComponent();
expect(findButton().exists()).toBe(true);
- expect(findButton().text()).toBe(SignInButton.i18n.defaultButtonText);
+ expect(findButton().text()).toBe(SignInLegacyButton.i18n.defaultButtonText);
});
describe.each`
diff --git a/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js
new file mode 100644
index 00000000000..18274cd4362
--- /dev/null
+++ b/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js
@@ -0,0 +1,204 @@
+import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
+import SignInOauthButton from '~/jira_connect/subscriptions/components/sign_in_oauth_button.vue';
+import {
+ I18N_DEFAULT_SIGN_IN_BUTTON_TEXT,
+ OAUTH_WINDOW_OPTIONS,
+} from '~/jira_connect/subscriptions/constants';
+import axios from '~/lib/utils/axios_utils';
+import waitForPromises from 'helpers/wait_for_promises';
+import httpStatus from '~/lib/utils/http_status';
+import AccessorUtilities from '~/lib/utils/accessor';
+
+jest.mock('~/lib/utils/accessor');
+jest.mock('~/jira_connect/subscriptions/utils');
+jest.mock('~/jira_connect/subscriptions/pkce', () => ({
+ createCodeVerifier: jest.fn().mockReturnValue('mock-verifier'),
+ createCodeChallenge: jest.fn().mockResolvedValue('mock-challenge'),
+}));
+
+const mockOauthMetadata = {
+ oauth_authorize_url: 'https://gitlab.com/mockOauth',
+ oauth_token_url: 'https://gitlab.com/mockOauthToken',
+ state: 'good-state',
+};
+
+describe('SignInOauthButton', () => {
+ let wrapper;
+ let mockAxios;
+
+ const createComponent = ({ slots } = {}) => {
+ wrapper = shallowMount(SignInOauthButton, {
+ slots,
+ provide: {
+ oauthMetadata: mockOauthMetadata,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mockAxios = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ mockAxios.restore();
+ });
+
+ const findButton = () => wrapper.findComponent(GlButton);
+
+ it('displays a button', () => {
+ createComponent();
+
+ expect(findButton().exists()).toBe(true);
+ expect(findButton().text()).toBe(I18N_DEFAULT_SIGN_IN_BUTTON_TEXT);
+ });
+
+ it.each`
+ scenario | cryptoAvailable
+ ${'when crypto API is available'} | ${true}
+ ${'when crypto API is unavailable'} | ${false}
+ `('$scenario when canUseCrypto returns $cryptoAvailable', ({ cryptoAvailable }) => {
+ AccessorUtilities.canUseCrypto = jest.fn().mockReturnValue(cryptoAvailable);
+ createComponent();
+
+ expect(findButton().props('disabled')).toBe(!cryptoAvailable);
+ });
+
+ describe('on click', () => {
+ beforeEach(async () => {
+ jest.spyOn(window, 'open').mockReturnValue();
+ createComponent();
+
+ findButton().vm.$emit('click');
+
+ await nextTick();
+ });
+
+ it('sets `loading` prop of button to `true`', () => {
+ expect(findButton().props('loading')).toBe(true);
+ });
+
+ it('calls `window.open` with correct arguments', () => {
+ expect(window.open).toHaveBeenCalledWith(
+ `${mockOauthMetadata.oauth_authorize_url}?code_challenge=mock-challenge&code_challenge_method=S256`,
+ I18N_DEFAULT_SIGN_IN_BUTTON_TEXT,
+ OAUTH_WINDOW_OPTIONS,
+ );
+ });
+
+ it('sets the `codeVerifier` internal state', () => {
+ expect(wrapper.vm.codeVerifier).toBe('mock-verifier');
+ });
+
+ describe('on window message event', () => {
+ describe('when window message properties are corrupted', () => {
+ describe.each`
+ origin | state | messageOrigin | messageState
+ ${window.origin} | ${mockOauthMetadata.state} | ${'bad-origin'} | ${mockOauthMetadata.state}
+ ${window.origin} | ${mockOauthMetadata.state} | ${window.origin} | ${'bad-state'}
+ `(
+ 'when message is [state=$messageState, origin=$messageOrigin]',
+ ({ messageOrigin, messageState }) => {
+ beforeEach(async () => {
+ const mockEvent = {
+ origin: messageOrigin,
+ data: {
+ state: messageState,
+ code: '1234',
+ },
+ };
+ window.dispatchEvent(new MessageEvent('message', mockEvent));
+ await waitForPromises();
+ });
+
+ it('emits `error` event', () => {
+ expect(wrapper.emitted('error')).toBeTruthy();
+ });
+
+ it('does not emit `sign-in` event', () => {
+ expect(wrapper.emitted('sign-in')).toBeFalsy();
+ });
+
+ it('sets `loading` prop of button to `false`', () => {
+ expect(findButton().props('loading')).toBe(false);
+ });
+ },
+ );
+ });
+
+ describe('when window message properties are valid', () => {
+ const mockAccessToken = '5678';
+ const mockUser = { name: 'test user' };
+ const mockEvent = {
+ origin: window.origin,
+ data: {
+ state: mockOauthMetadata.state,
+ code: '1234',
+ },
+ };
+
+ describe('when API requests succeed', () => {
+ beforeEach(async () => {
+ jest.spyOn(axios, 'post');
+ jest.spyOn(axios, 'get');
+ mockAxios
+ .onPost(mockOauthMetadata.oauth_token_url)
+ .replyOnce(httpStatus.OK, { access_token: mockAccessToken });
+ mockAxios.onGet('/api/v4/user').replyOnce(httpStatus.OK, mockUser);
+
+ window.dispatchEvent(new MessageEvent('message', mockEvent));
+
+ await waitForPromises();
+ });
+
+ it('executes POST request to Oauth token endpoint', () => {
+ expect(axios.post).toHaveBeenCalledWith(mockOauthMetadata.oauth_token_url, {
+ code: '1234',
+ code_verifier: 'mock-verifier',
+ });
+ });
+
+ it('executes GET request to fetch user data', () => {
+ expect(axios.get).toHaveBeenCalledWith('/api/v4/user', {
+ headers: { Authorization: `Bearer ${mockAccessToken}` },
+ });
+ });
+
+ it('emits `sign-in` event with user data', () => {
+ expect(wrapper.emitted('sign-in')[0]).toEqual([mockUser]);
+ });
+ });
+
+ describe('when API requests fail', () => {
+ beforeEach(async () => {
+ jest.spyOn(axios, 'post');
+ jest.spyOn(axios, 'get');
+ mockAxios
+ .onPost(mockOauthMetadata.oauth_token_url)
+ .replyOnce(httpStatus.INTERNAL_SERVER_ERROR, { access_token: mockAccessToken });
+ mockAxios.onGet('/api/v4/user').replyOnce(httpStatus.INTERNAL_SERVER_ERROR, mockUser);
+
+ window.dispatchEvent(new MessageEvent('message', mockEvent));
+
+ await waitForPromises();
+ });
+
+ it('emits `error` event', () => {
+ expect(wrapper.emitted('error')).toBeTruthy();
+ });
+
+ it('does not emit `sign-in` event', () => {
+ expect(wrapper.emitted('sign-in')).toBeFalsy();
+ });
+
+ it('sets `loading` prop of button to `false`', () => {
+ expect(findButton().props('loading')).toBe(false);
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/jira_connect/subscriptions/components/user_link_spec.js b/spec/frontend/jira_connect/subscriptions/components/user_link_spec.js
index b98a36269a3..2f5e47d1ae4 100644
--- a/spec/frontend/jira_connect/subscriptions/components/user_link_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/user_link_spec.js
@@ -7,7 +7,7 @@ jest.mock('~/jira_connect/subscriptions/utils', () => ({
getGitlabSignInURL: jest.fn().mockImplementation((path) => Promise.resolve(path)),
}));
-describe('SubscriptionsList', () => {
+describe('UserLink', () => {
let wrapper;
const createComponent = (propsData = {}, { provide } = {}) => {
@@ -68,24 +68,35 @@ describe('SubscriptionsList', () => {
});
describe('gitlab user link', () => {
- window.gon = { current_username: 'root' };
+ describe.each`
+ current_username | gitlabUserPath | user | expectedUserHandle | expectedUserLink
+ ${'root'} | ${'/root'} | ${{ username: 'test-user' }} | ${'@root'} | ${'/root'}
+ ${'root'} | ${'/root'} | ${undefined} | ${'@root'} | ${'/root'}
+ ${undefined} | ${undefined} | ${{ username: 'test-user' }} | ${'@test-user'} | ${'/test-user'}
+ `(
+ 'when current_username=$current_username, gitlabUserPath=$gitlabUserPath and user=$user',
+ ({ current_username, gitlabUserPath, user, expectedUserHandle, expectedUserLink }) => {
+ beforeEach(() => {
+ window.gon = { current_username, relative_root_url: '' };
- beforeEach(() => {
- createComponent(
- {
- userSignedIn: true,
- hasSubscriptions: true,
- },
- { provide: { gitlabUserPath: '/root' } },
- );
- });
+ createComponent(
+ {
+ userSignedIn: true,
+ hasSubscriptions: true,
+ user,
+ },
+ { provide: { gitlabUserPath } },
+ );
+ });
- it('renders with correct href', () => {
- expect(findGitlabUserLink().attributes('href')).toBe('/root');
- });
+ it(`sets href to ${expectedUserLink}`, () => {
+ expect(findGitlabUserLink().attributes('href')).toBe(expectedUserLink);
+ });
- it('contains GitLab user handle', () => {
- expect(findGitlabUserLink().text()).toBe('@root');
- });
+ it(`renders ${expectedUserHandle} as text`, () => {
+ expect(findGitlabUserLink().text()).toBe(expectedUserHandle);
+ });
+ },
+ );
});
});
diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in_spec.js
index 4e3297506f1..175896c4ab0 100644
--- a/spec/frontend/jira_connect/subscriptions/pages/sign_in_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in_spec.js
@@ -1,26 +1,44 @@
-import { mount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import SignInPage from '~/jira_connect/subscriptions/pages/sign_in.vue';
-import SignInButton from '~/jira_connect/subscriptions/components/sign_in_button.vue';
+import SignInLegacyButton from '~/jira_connect/subscriptions/components/sign_in_legacy_button.vue';
+import SignInOauthButton from '~/jira_connect/subscriptions/components/sign_in_oauth_button.vue';
import SubscriptionsList from '~/jira_connect/subscriptions/components/subscriptions_list.vue';
import createStore from '~/jira_connect/subscriptions/store';
+import { I18N_DEFAULT_SIGN_IN_BUTTON_TEXT } from '../../../../../app/assets/javascripts/jira_connect/subscriptions/constants';
jest.mock('~/jira_connect/subscriptions/utils');
+const mockUsersPath = '/test';
+const defaultProvide = {
+ oauthMetadata: {},
+ usersPath: mockUsersPath,
+};
+
describe('SignInPage', () => {
let wrapper;
let store;
- const findSignInButton = () => wrapper.findComponent(SignInButton);
+ const findSignInLegacyButton = () => wrapper.findComponent(SignInLegacyButton);
+ const findSignInOauthButton = () => wrapper.findComponent(SignInOauthButton);
const findSubscriptionsList = () => wrapper.findComponent(SubscriptionsList);
- const createComponent = ({ provide, props } = {}) => {
+ const createComponent = ({ props, jiraConnectOauthEnabled } = {}) => {
store = createStore();
- wrapper = mount(SignInPage, {
+ wrapper = shallowMount(SignInPage, {
store,
- provide,
+ provide: {
+ ...defaultProvide,
+ glFeatures: {
+ jiraConnectOauth: jiraConnectOauthEnabled,
+ },
+ },
propsData: props,
+ stubs: {
+ SignInLegacyButton,
+ SignInOauthButton,
+ },
});
};
@@ -29,33 +47,74 @@ describe('SignInPage', () => {
});
describe('template', () => {
- const mockUsersPath = '/test';
describe.each`
- scenario | expectSubscriptionsList | signInButtonText
- ${'with subscriptions'} | ${true} | ${SignInPage.i18n.signinButtonTextWithSubscriptions}
- ${'without subscriptions'} | ${false} | ${SignInButton.i18n.defaultButtonText}
- `('$scenario', ({ expectSubscriptionsList, signInButtonText }) => {
- beforeEach(() => {
- createComponent({
- provide: {
- usersPath: mockUsersPath,
- },
- props: {
- hasSubscriptions: expectSubscriptionsList,
- },
+ scenario | hasSubscriptions | signInButtonText
+ ${'with subscriptions'} | ${true} | ${SignInPage.i18n.signInButtonTextWithSubscriptions}
+ ${'without subscriptions'} | ${false} | ${I18N_DEFAULT_SIGN_IN_BUTTON_TEXT}
+ `('$scenario', ({ hasSubscriptions, signInButtonText }) => {
+ describe('when `jiraConnectOauthEnabled` feature flag is disabled', () => {
+ beforeEach(() => {
+ createComponent({
+ jiraConnectOauthEnabled: false,
+ props: {
+ hasSubscriptions,
+ },
+ });
});
- });
- it(`renders sign in button with text ${signInButtonText}`, () => {
- expect(findSignInButton().text()).toMatchInterpolatedText(signInButtonText);
+ it('renders legacy sign in button', () => {
+ const button = findSignInLegacyButton();
+ expect(button.props('usersPath')).toBe(mockUsersPath);
+ expect(button.text()).toMatchInterpolatedText(signInButtonText);
+ });
});
- it('renders sign in button with `usersPath` prop', () => {
- expect(findSignInButton().props('usersPath')).toBe(mockUsersPath);
+ describe('when `jiraConnectOauthEnabled` feature flag is enabled', () => {
+ beforeEach(() => {
+ createComponent({
+ jiraConnectOauthEnabled: true,
+ props: {
+ hasSubscriptions,
+ },
+ });
+ });
+
+ describe('oauth sign in button', () => {
+ it('renders oauth sign in button', () => {
+ const button = findSignInOauthButton();
+ expect(button.text()).toMatchInterpolatedText(signInButtonText);
+ });
+
+ describe('when button emits `sign-in` event', () => {
+ it('emits `sign-in-oauth` event', () => {
+ const button = findSignInOauthButton();
+
+ const mockUser = { name: 'test' };
+ button.vm.$emit('sign-in', mockUser);
+
+ expect(wrapper.emitted('sign-in-oauth')[0]).toEqual([mockUser]);
+ });
+ });
+
+ describe('when button emits `error` event', () => {
+ it('emits `error` event', () => {
+ const button = findSignInOauthButton();
+ button.vm.$emit('error');
+
+ expect(wrapper.emitted('error')).toBeTruthy();
+ });
+ });
+ });
});
- it(`${expectSubscriptionsList ? 'renders' : 'does not render'} subscriptions list`, () => {
- expect(findSubscriptionsList().exists()).toBe(expectSubscriptionsList);
+ it(`${hasSubscriptions ? 'renders' : 'does not render'} subscriptions list`, () => {
+ createComponent({
+ props: {
+ hasSubscriptions,
+ },
+ });
+
+ expect(findSubscriptionsList().exists()).toBe(hasSubscriptions);
});
});
});
diff --git a/spec/frontend/jira_connect/subscriptions/pkce_spec.js b/spec/frontend/jira_connect/subscriptions/pkce_spec.js
new file mode 100644
index 00000000000..4ee88059b7a
--- /dev/null
+++ b/spec/frontend/jira_connect/subscriptions/pkce_spec.js
@@ -0,0 +1,48 @@
+import crypto from 'crypto';
+import { TextEncoder, TextDecoder } from 'util';
+
+import { createCodeVerifier, createCodeChallenge } from '~/jira_connect/subscriptions/pkce';
+
+global.TextEncoder = TextEncoder;
+global.TextDecoder = TextDecoder;
+
+describe('pkce', () => {
+ beforeAll(() => {
+ Object.defineProperty(global.self, 'crypto', {
+ value: {
+ getRandomValues: (arr) => crypto.randomBytes(arr.length),
+ subtle: {
+ digest: jest.fn().mockResolvedValue(new ArrayBuffer(1)),
+ },
+ },
+ });
+ });
+
+ describe('createCodeVerifier', () => {
+ it('calls `window.crypto.getRandomValues`', () => {
+ window.crypto.getRandomValues = jest.fn();
+ createCodeVerifier();
+
+ expect(window.crypto.getRandomValues).toHaveBeenCalled();
+ });
+
+ it(`returns a string with 128 characters`, () => {
+ const codeVerifier = createCodeVerifier();
+ expect(codeVerifier).toHaveLength(128);
+ });
+ });
+
+ describe('createCodeChallenge', () => {
+ it('calls `window.crypto.subtle.digest` with correct arguments', async () => {
+ await createCodeChallenge('1234');
+
+ expect(window.crypto.subtle.digest).toHaveBeenCalledWith('SHA-256', expect.anything());
+ });
+
+ it('returns base64 URL-encoded string', async () => {
+ const codeChallenge = await createCodeChallenge('1234');
+
+ expect(codeChallenge).toBe('AA');
+ });
+ });
+});