From 6ed4ec3e0b1340f96b7c043ef51d1b33bbe85fde Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 19 Sep 2022 23:18:09 +0000 Subject: Add latest changes from gitlab-org/gitlab@15-4-stable-ee --- .../jira_connect/subscriptions/api_spec.js | 118 ++++++++++++++++++--- .../add_namespace_modal/groups_list_spec.js | 2 +- .../subscriptions/components/app_spec.js | 21 ++-- .../components/sign_in_oauth_button_spec.js | 87 ++++++++++----- .../pages/sign_in/sign_in_gitlab_com_spec.js | 2 +- .../sign_in_gitlab_multiversion/index_spec.js | 33 ++++-- .../subscriptions/store/actions_spec.js | 16 ++- 7 files changed, 206 insertions(+), 73 deletions(-) (limited to 'spec/frontend/jira_connect/subscriptions') diff --git a/spec/frontend/jira_connect/subscriptions/api_spec.js b/spec/frontend/jira_connect/subscriptions/api_spec.js index 57b11bdbc27..cf496d5836a 100644 --- a/spec/frontend/jira_connect/subscriptions/api_spec.js +++ b/spec/frontend/jira_connect/subscriptions/api_spec.js @@ -1,7 +1,14 @@ import MockAdapter from 'axios-mock-adapter'; -import { addSubscription, removeSubscription, fetchGroups } from '~/jira_connect/subscriptions/api'; +import { + axiosInstance, + addSubscription, + removeSubscription, + fetchGroups, + getCurrentUser, + addJiraConnectSubscription, + updateInstallation, +} from '~/jira_connect/subscriptions/api'; import { getJwt } from '~/jira_connect/subscriptions/utils'; -import axios from '~/lib/utils/axios_utils'; import httpStatus from '~/lib/utils/http_status'; jest.mock('~/jira_connect/subscriptions/utils', () => ({ @@ -9,21 +16,26 @@ jest.mock('~/jira_connect/subscriptions/utils', () => ({ })); describe('JiraConnect API', () => { - let mock; + let axiosMock; + let originalGon; let response; const mockAddPath = 'addPath'; const mockRemovePath = 'removePath'; const mockNamespace = 'namespace'; const mockJwt = 'jwt'; + const mockAccessToken = 'accessToken'; const mockResponse = { success: true }; beforeEach(() => { - mock = new MockAdapter(axios); + axiosMock = new MockAdapter(axiosInstance); + originalGon = window.gon; + window.gon = { api_version: 'v4' }; }); afterEach(() => { - mock.restore(); + axiosMock.restore(); + window.gon = originalGon; response = null; }); @@ -31,8 +43,8 @@ describe('JiraConnect API', () => { const makeRequest = () => addSubscription(mockAddPath, mockNamespace); it('returns success response', async () => { - jest.spyOn(axios, 'post'); - mock + jest.spyOn(axiosInstance, 'post'); + axiosMock .onPost(mockAddPath, { jwt: mockJwt, namespace_path: mockNamespace, @@ -42,7 +54,7 @@ describe('JiraConnect API', () => { response = await makeRequest(); expect(getJwt).toHaveBeenCalled(); - expect(axios.post).toHaveBeenCalledWith(mockAddPath, { + expect(axiosInstance.post).toHaveBeenCalledWith(mockAddPath, { jwt: mockJwt, namespace_path: mockNamespace, }); @@ -54,13 +66,13 @@ describe('JiraConnect API', () => { const makeRequest = () => removeSubscription(mockRemovePath); it('returns success response', async () => { - jest.spyOn(axios, 'delete'); - mock.onDelete(mockRemovePath).replyOnce(httpStatus.OK, mockResponse); + jest.spyOn(axiosInstance, 'delete'); + axiosMock.onDelete(mockRemovePath).replyOnce(httpStatus.OK, mockResponse); response = await makeRequest(); expect(getJwt).toHaveBeenCalled(); - expect(axios.delete).toHaveBeenCalledWith(mockRemovePath, { + expect(axiosInstance.delete).toHaveBeenCalledWith(mockRemovePath, { params: { jwt: mockJwt, }, @@ -81,8 +93,8 @@ describe('JiraConnect API', () => { }); it('returns success response', async () => { - jest.spyOn(axios, 'get'); - mock + jest.spyOn(axiosInstance, 'get'); + axiosMock .onGet(mockGroupsPath, { page: mockPage, per_page: mockPerPage, @@ -91,7 +103,7 @@ describe('JiraConnect API', () => { response = await makeRequest(); - expect(axios.get).toHaveBeenCalledWith(mockGroupsPath, { + expect(axiosInstance.get).toHaveBeenCalledWith(mockGroupsPath, { params: { page: mockPage, per_page: mockPerPage, @@ -100,4 +112,82 @@ describe('JiraConnect API', () => { expect(response.data).toEqual(mockResponse); }); }); + + describe('getCurrentUser', () => { + const makeRequest = () => getCurrentUser(); + + it('returns success response', async () => { + const expectedUrl = '/api/v4/user'; + + jest.spyOn(axiosInstance, 'get'); + + axiosMock.onGet(expectedUrl).replyOnce(httpStatus.OK, mockResponse); + + response = await makeRequest(); + + expect(axiosInstance.get).toHaveBeenCalledWith(expectedUrl, {}); + expect(response.data).toEqual(mockResponse); + }); + }); + + describe('addJiraConnectSubscription', () => { + const makeRequest = () => + addJiraConnectSubscription(mockNamespace, { jwt: mockJwt, accessToken: mockAccessToken }); + + it('returns success response', async () => { + const expectedUrl = '/api/v4/integrations/jira_connect/subscriptions'; + + jest.spyOn(axiosInstance, 'post'); + + axiosMock.onPost(expectedUrl).replyOnce(httpStatus.OK, mockResponse); + + response = await makeRequest(); + + expect(axiosInstance.post).toHaveBeenCalledWith( + expectedUrl, + { + jwt: mockJwt, + namespace_path: mockNamespace, + }, + { headers: { Authorization: `Bearer ${mockAccessToken}` } }, + ); + expect(response.data).toEqual(mockResponse); + }); + }); + + describe('updateInstallation', () => { + const expectedUrl = '/-/jira_connect/installations'; + + it.each` + instanceUrl | expectedInstanceUrl + ${'https://gitlab.com'} | ${null} + ${'https://gitlab.mycompany.com'} | ${'https://gitlab.mycompany.com'} + `( + 'when instanceUrl is $instanceUrl, it passes `instance_url` as $expectedInstanceUrl', + async ({ instanceUrl, expectedInstanceUrl }) => { + const makeRequest = () => updateInstallation(instanceUrl); + + jest.spyOn(axiosInstance, 'put'); + axiosMock + .onPut(expectedUrl, { + jwt: mockJwt, + installation: { + instance_url: expectedInstanceUrl, + }, + }) + .replyOnce(httpStatus.OK, mockResponse); + + response = await makeRequest(); + + expect(getJwt).toHaveBeenCalled(); + expect(axiosInstance.put).toHaveBeenCalledWith(expectedUrl, { + jwt: mockJwt, + installation: { + instance_url: expectedInstanceUrl, + }, + }); + expect(response.data).toEqual(mockResponse); + }, + ); + }); }); diff --git a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js index d871b1e1dcc..f1fc5e4d90b 100644 --- a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js @@ -50,7 +50,7 @@ describe('GroupsList', () => { const findGlAlert = () => wrapper.findComponent(GlAlert); const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - const findAllItems = () => wrapper.findAll(GroupsListItem); + const findAllItems = () => wrapper.findAllComponents(GroupsListItem); const findFirstItem = () => findAllItems().at(0); const findSecondItem = () => findAllItems().at(1); const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); diff --git a/spec/frontend/jira_connect/subscriptions/components/app_spec.js b/spec/frontend/jira_connect/subscriptions/components/app_spec.js index 9894141be5a..369ddda8dbe 100644 --- a/spec/frontend/jira_connect/subscriptions/components/app_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/app_spec.js @@ -31,8 +31,8 @@ describe('JiraConnectApp', () => { const findUserLink = () => wrapper.findComponent(UserLink); const findBrowserSupportAlert = () => wrapper.findComponent(BrowserSupportAlert); - const createComponent = ({ provide, mountFn = shallowMountExtended } = {}) => { - store = createStore({ subscriptions: [mockSubscription] }); + const createComponent = ({ provide, mountFn = shallowMountExtended, initialState = {} } = {}) => { + store = createStore({ ...initialState, subscriptions: [mockSubscription] }); jest.spyOn(store, 'dispatch').mockImplementation(); wrapper = mountFn(JiraConnectApp, { @@ -60,7 +60,7 @@ describe('JiraConnectApp', () => { }); it(`${shouldRenderSignInPage ? 'renders' : 'does not render'} sign in page`, () => { - expect(findSignInPage().exists()).toBe(shouldRenderSignInPage); + expect(findSignInPage().isVisible()).toBe(shouldRenderSignInPage); if (shouldRenderSignInPage) { expect(findSignInPage().props('hasSubscriptions')).toBe(true); } @@ -133,7 +133,7 @@ describe('JiraConnectApp', () => { }); it('renders link when `linkUrl` is set', async () => { - createComponent({ mountFn: mountExtended }); + createComponent({ provide: { usersPath: '' }, mountFn: mountExtended }); store.commit(SET_ALERT, { message: __('test message %{linkStart}test link%{linkEnd}'), @@ -211,21 +211,22 @@ describe('JiraConnectApp', () => { describe('when `jiraConnectOauth` feature flag is enabled', () => { const mockSubscriptionsPath = '/mockSubscriptionsPath'; - beforeEach(() => { + beforeEach(async () => { jest.spyOn(api, 'fetchSubscriptions').mockResolvedValue({ data: { subscriptions: [] } }); + jest.spyOn(AccessorUtilities, 'canUseCrypto').mockReturnValue(true); createComponent({ + initialState: { + currentUser: { name: 'root' }, + }, provide: { glFeatures: { jiraConnectOauth: true }, subscriptionsPath: mockSubscriptionsPath, }, }); - }); - describe('when component mounts', () => { - it('dispatches `fetchSubscriptions` action', async () => { - expect(store.dispatch).toHaveBeenCalledWith('fetchSubscriptions', mockSubscriptionsPath); - }); + findSignInPage().vm.$emit('sign-in-oauth'); + await nextTick(); }); describe('when oauth button emits `sign-in-oauth` event', () => { 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 index ed0abaaf576..01317eb5dba 100644 --- 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 @@ -1,39 +1,41 @@ 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'; -import { getCurrentUser } from '~/rest_api'; +import { + getCurrentUser, + fetchOAuthApplicationId, + fetchOAuthToken, +} from '~/jira_connect/subscriptions/api'; import createStore from '~/jira_connect/subscriptions/store'; import { SET_ACCESS_TOKEN } from '~/jira_connect/subscriptions/store/mutation_types'; jest.mock('~/lib/utils/accessor'); jest.mock('~/jira_connect/subscriptions/utils'); jest.mock('~/jira_connect/subscriptions/api'); -jest.mock('~/rest_api'); 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; let store; + const mockOauthMetadata = { + oauth_authorize_url: 'https://gitlab.com/mockOauth', + oauth_token_path: 'https://gitlab.com/mockOauthToken', + oauth_token_payload: { + client_id: '543678901', + }, + state: 'good-state', + }; const createComponent = ({ slots, props } = {}) => { store = createStore(); @@ -50,13 +52,8 @@ describe('SignInOauthButton', () => { }); }; - beforeEach(() => { - mockAxios = new MockAdapter(axios); - }); - afterEach(() => { wrapper.destroy(); - mockAxios.restore(); }); const findButton = () => wrapper.findComponent(GlButton); @@ -69,6 +66,46 @@ describe('SignInOauthButton', () => { expect(findButton().props('category')).toBe('primary'); }); + describe('when `gitlabBasePath` is passed', () => { + const mockBasePath = 'https://gitlab.mycompany.com'; + + it('uses custom text for button', () => { + createComponent({ + props: { + gitlabBasePath: mockBasePath, + }, + }); + + expect(findButton().text()).toBe(`Sign in to ${mockBasePath}`); + }); + + describe('on click', () => { + const mockClientId = '798412381'; + + beforeEach(async () => { + fetchOAuthApplicationId.mockReturnValue({ data: { application_id: mockClientId } }); + jest.spyOn(window, 'open').mockReturnValue(); + createComponent({ + props: { + gitlabBasePath: mockBasePath, + }, + }); + + findButton().vm.$emit('click'); + + await nextTick(); + }); + + it('calls `window.open` with correct arguments', () => { + expect(window.open).toHaveBeenCalledWith( + `${mockBasePath}/mockOauth?code_challenge=mock-challenge&code_challenge_method=S256&client_id=${mockClientId}`, + I18N_DEFAULT_SIGN_IN_BUTTON_TEXT, + OAUTH_WINDOW_OPTIONS, + ); + }); + }); + }); + it.each` scenario | cryptoAvailable ${'when crypto API is available'} | ${true} @@ -96,7 +133,7 @@ describe('SignInOauthButton', () => { it('calls `window.open` with correct arguments', () => { expect(window.open).toHaveBeenCalledWith( - `${mockOauthMetadata.oauth_authorize_url}?code_challenge=mock-challenge&code_challenge_method=S256`, + `${mockOauthMetadata.oauth_authorize_url}?code_challenge=mock-challenge&code_challenge_method=S256&client_id=${mockOauthMetadata.oauth_token_payload.client_id}`, I18N_DEFAULT_SIGN_IN_BUTTON_TEXT, OAUTH_WINDOW_OPTIONS, ); @@ -151,11 +188,7 @@ describe('SignInOauthButton', () => { 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 }); + fetchOAuthToken.mockResolvedValue({ data: { access_token: mockAccessToken } }); getCurrentUser.mockResolvedValue({ data: mockUser }); window.dispatchEvent(new MessageEvent('message', mockEvent)); @@ -164,9 +197,10 @@ describe('SignInOauthButton', () => { }); it('executes POST request to Oauth token endpoint', () => { - expect(axios.post).toHaveBeenCalledWith(mockOauthMetadata.oauth_token_url, { + expect(fetchOAuthToken).toHaveBeenCalledWith(mockOauthMetadata.oauth_token_path, { code: '1234', code_verifier: 'mock-verifier', + client_id: mockOauthMetadata.oauth_token_payload.client_id, }); }); @@ -185,10 +219,7 @@ describe('SignInOauthButton', () => { describe('when API requests fail', () => { beforeEach(async () => { - jest.spyOn(axios, 'post'); - mockAxios - .onPost(mockOauthMetadata.oauth_token_url) - .replyOnce(httpStatus.INTERNAL_SERVER_ERROR); + fetchOAuthToken.mockRejectedValue(); window.dispatchEvent(new MessageEvent('message', mockEvent)); diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com_spec.js index 1649920b48b..b9a8451f3b3 100644 --- a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com_spec.js +++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com_spec.js @@ -101,7 +101,7 @@ describe('SignInGitlabCom', () => { const button = findSignInOauthButton(); button.vm.$emit('error'); - expect(wrapper.emitted('error')).toBeTruthy(); + expect(wrapper.emitted('error')).toHaveLength(1); }); }); }); diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js index f4be8bf121d..10696d25f17 100644 --- a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js +++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js @@ -5,9 +5,22 @@ import SignInGitlabMultiversion from '~/jira_connect/subscriptions/pages/sign_in import VersionSelectForm from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue'; import SignInOauthButton from '~/jira_connect/subscriptions/components/sign_in_oauth_button.vue'; +import { updateInstallation } from '~/jira_connect/subscriptions/api'; +import { reloadPage, persistBaseUrl, retrieveBaseUrl } from '~/jira_connect/subscriptions/utils'; + +jest.mock('~/jira_connect/subscriptions/api', () => { + return { + updateInstallation: jest.fn(), + setApiBaseURL: jest.fn(), + }; +}); +jest.mock('~/jira_connect/subscriptions/utils'); + describe('SignInGitlabMultiversion', () => { let wrapper; + const mockBasePath = 'gitlab.mycompany.com'; + const findVersionSelectForm = () => wrapper.findComponent(VersionSelectForm); const findSignInOauthButton = () => wrapper.findComponent(SignInOauthButton); const findSubtitle = () => wrapper.findByTestId('subtitle'); @@ -29,30 +42,32 @@ describe('SignInGitlabMultiversion', () => { }); describe('when form emits "submit" event', () => { - it('hides the version select form and shows the sign in button', async () => { + it('updates the backend, then saves the baseUrl and reloads', async () => { + updateInstallation.mockResolvedValue({}); + createComponent(); - findVersionSelectForm().vm.$emit('submit', 'gitlab.mycompany.com'); + findVersionSelectForm().vm.$emit('submit', mockBasePath); await nextTick(); - expect(findVersionSelectForm().exists()).toBe(false); - expect(findSignInOauthButton().exists()).toBe(true); + expect(updateInstallation).toHaveBeenCalled(); + expect(persistBaseUrl).toHaveBeenCalledWith(mockBasePath); + expect(reloadPage).toHaveBeenCalled(); }); }); }); }); describe('when version is selected', () => { - beforeEach(async () => { + beforeEach(() => { + retrieveBaseUrl.mockReturnValue(mockBasePath); createComponent(); - - findVersionSelectForm().vm.$emit('submit', 'gitlab.mycompany.com'); - await nextTick(); }); describe('sign in button', () => { it('renders sign in button', () => { expect(findSignInOauthButton().exists()).toBe(true); + expect(findSignInOauthButton().props('gitlabBasePath')).toBe(mockBasePath); }); describe('when button emits `sign-in` event', () => { @@ -71,7 +86,7 @@ describe('SignInGitlabMultiversion', () => { const button = findSignInOauthButton(); button.vm.$emit('error'); - expect(wrapper.emitted('error')).toBeTruthy(); + expect(wrapper.emitted('error')).toHaveLength(1); }); }); }); diff --git a/spec/frontend/jira_connect/subscriptions/store/actions_spec.js b/spec/frontend/jira_connect/subscriptions/store/actions_spec.js index 53b5d8e70af..5e3c30269b5 100644 --- a/spec/frontend/jira_connect/subscriptions/store/actions_spec.js +++ b/spec/frontend/jira_connect/subscriptions/store/actions_spec.js @@ -8,8 +8,6 @@ import { } from '~/jira_connect/subscriptions/store/actions'; import state from '~/jira_connect/subscriptions/store/state'; import * as api from '~/jira_connect/subscriptions/api'; -import * as userApi from '~/api/user_api'; -import * as integrationsApi from '~/api/integrations_api'; import { I18N_DEFAULT_SUBSCRIPTIONS_ERROR_MESSAGE, I18N_ADD_SUBSCRIPTION_SUCCESS_ALERT_TITLE, @@ -79,7 +77,7 @@ describe('JiraConnect actions', () => { describe('when API request succeeds', () => { it('commits the SET_ACCESS_TOKEN and SET_CURRENT_USER mutations', async () => { const mockUser = { name: 'root' }; - jest.spyOn(userApi, 'getCurrentUser').mockResolvedValue({ data: mockUser }); + jest.spyOn(api, 'getCurrentUser').mockResolvedValue({ data: mockUser }); await testAction( loadCurrentUser, @@ -89,7 +87,7 @@ describe('JiraConnect actions', () => { [], ); - expect(userApi.getCurrentUser).toHaveBeenCalledWith({ + expect(api.getCurrentUser).toHaveBeenCalledWith({ headers: { Authorization: `Bearer ${mockAccessToken}` }, }); }); @@ -97,7 +95,7 @@ describe('JiraConnect actions', () => { describe('when API request fails', () => { it('commits the SET_CURRENT_USER_ERROR mutation', async () => { - jest.spyOn(userApi, 'getCurrentUser').mockRejectedValue(); + jest.spyOn(api, 'getCurrentUser').mockRejectedValue(); await testAction( loadCurrentUser, @@ -120,9 +118,7 @@ describe('JiraConnect actions', () => { describe('when API request succeeds', () => { it('commits the SET_ACCESS_TOKEN and SET_CURRENT_USER mutations', async () => { - jest - .spyOn(integrationsApi, 'addJiraConnectSubscription') - .mockResolvedValue({ success: true }); + jest.spyOn(api, 'addJiraConnectSubscription').mockResolvedValue({ success: true }); await testAction( addSubscription, @@ -144,7 +140,7 @@ describe('JiraConnect actions', () => { [{ type: 'fetchSubscriptions', payload: mockSubscriptionsPath }], ); - expect(integrationsApi.addJiraConnectSubscription).toHaveBeenCalledWith(mockNamespace, { + expect(api.addJiraConnectSubscription).toHaveBeenCalledWith(mockNamespace, { accessToken: null, jwt: '1234', }); @@ -153,7 +149,7 @@ describe('JiraConnect actions', () => { describe('when API request fails', () => { it('commits the SET_CURRENT_USER_ERROR mutation', async () => { - jest.spyOn(integrationsApi, 'addJiraConnectSubscription').mockRejectedValue(); + jest.spyOn(api, 'addJiraConnectSubscription').mockRejectedValue(); await testAction( addSubscription, -- cgit v1.2.1