diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-18 20:02:30 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-18 20:02:30 +0000 |
commit | 41fe97390ceddf945f3d967b8fdb3de4c66b7dea (patch) | |
tree | 9c8d89a8624828992f06d892cd2f43818ff5dcc8 /spec/frontend/jira_connect | |
parent | 0804d2dc31052fb45a1efecedc8e06ce9bc32862 (diff) | |
download | gitlab-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.js | 56 | ||||
-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.js | 204 | ||||
-rw-r--r-- | spec/frontend/jira_connect/subscriptions/components/user_link_spec.js | 45 | ||||
-rw-r--r-- | spec/frontend/jira_connect/subscriptions/pages/sign_in_spec.js | 111 | ||||
-rw-r--r-- | spec/frontend/jira_connect/subscriptions/pkce_spec.js | 48 |
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'); + }); + }); +}); |