diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-09-21 15:07:54 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-09-21 15:07:54 +0300 |
commit | da5028d723994eae4e66b0893ebf73e0a602955e (patch) | |
tree | fa21e80c039566630c584692da906921da718b20 /spec | |
parent | 7e29b8f3fcc32a6adcf735a5a9069de5d9d814d6 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
16 files changed, 774 insertions, 303 deletions
diff --git a/spec/factories/vs_code_settings.rb b/spec/factories/vs_code_settings.rb new file mode 100644 index 00000000000..ac4bf3e61bd --- /dev/null +++ b/spec/factories/vs_code_settings.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :vscode_setting, class: 'VsCode::VsCodeSetting' do + user + + setting_type { 'settings' } + content { '{}' } + end +end diff --git a/spec/finders/vs_code/settings_finder_spec.rb b/spec/finders/vs_code/settings_finder_spec.rb new file mode 100644 index 00000000000..4b8e9d00f30 --- /dev/null +++ b/spec/finders/vs_code/settings_finder_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe VsCode::SettingsFinder, feature_category: :web_ide do + let_it_be(:user) { create(:user) } + + describe '#execute' do + context 'when nil is passed in as the list of settings' do + let(:finder) { described_class.new(user, nil) } + + subject { finder.execute } + + context 'when user has no settings' do + it 'returns an empty array' do + expect(subject).to eq([]) + end + end + + context 'when user has settings' do + before do + create(:vscode_setting, user: user) + end + + it 'returns an array of settings' do + expect(subject.length).to eq(1) + expect(subject[0].user_id).to eq(user.id) + expect(subject[0].setting_type).to eq('settings') + end + end + end + + context 'when a list of settings is passed, filters by the setting' do + let_it_be(:setting) { create(:vscode_setting, user: user) } + + context 'when user has no settings with that type' do + subject { finder.execute } + + it 'returns an empty array' do + finder = described_class.new(user, ['profile']) + expect(finder.execute).to eq([]) + end + end + + context 'when user does have settings with the type' do + subject { finder.execute } + + it 'returns the record when a single setting exists' do + result = described_class.new(user, ['settings']).execute + expect(result.length).to eq(1) + expect(result[0].user_id).to eq(user.id) + expect(result[0].setting_type).to eq('settings') + end + + it 'returns multiple records when more than one setting exists' do + create(:vscode_setting, user: user, setting_type: 'profile') + + result = described_class.new(user, %w[settings profile]).execute + expect(result.length).to eq(2) + end + end + end + end +end diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js index 03d0920994c..773fdc093c7 100644 --- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js @@ -345,6 +345,28 @@ describe('import table', () => { ); }); + it('resets page to 1 when page size is changed', async () => { + wrapper.findComponent(PaginationBar).vm.$emit('set-page', 2); + await waitForPromises(); + + expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ page: 2, perPage: 50 }), + expect.anything(), + expect.anything(), + ); + + wrapper.findComponent(PaginationBar).vm.$emit('set-page-size', 200); + await waitForPromises(); + + expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ page: 1, perPage: 200 }), + expect.anything(), + expect.anything(), + ); + }); + it('updates status text when page is changed', async () => { const REQUESTED_PAGE = 2; bulkImportSourceGroupsQueryMock.mockResolvedValue({ diff --git a/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js b/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js index 0037934cbc5..7d65dddc3f9 100644 --- a/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js +++ b/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js @@ -141,6 +141,25 @@ describe('BulkImportsHistoryApp', () => { ); }); + it('resets page to 1 when page size is changed', async () => { + const NEW_PAGE_SIZE = 4; + + mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS); + createComponent(); + await axios.waitForAll(); + wrapper.findComponent(PaginationBar).vm.$emit('set-page', 2); + await axios.waitForAll(); + mock.resetHistory(); + + wrapper.findComponent(PaginationBar).vm.$emit('set-page-size', NEW_PAGE_SIZE); + await axios.waitForAll(); + + expect(mock.history.get.length).toBe(1); + expect(mock.history.get[0].params).toStrictEqual( + expect.objectContaining({ per_page: NEW_PAGE_SIZE, page: 1 }), + ); + }); + it('sets up the local storage sync correctly', async () => { const NEW_PAGE_SIZE = 4; diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js index 1d164b9f5c1..d18437ccec3 100644 --- a/spec/frontend/releases/stores/modules/detail/actions_spec.js +++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js @@ -1,9 +1,11 @@ import { cloneDeep } from 'lodash'; import originalOneReleaseForEditingQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release_for_editing.query.graphql.json'; import testAction from 'helpers/vuex_action_helper'; +import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import { getTag } from '~/api/tags_api'; import { createAlert } from '~/alert'; import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated +import AccessorUtilities from '~/lib/utils/accessor'; import { s__ } from '~/locale'; import { ASSET_LINK_TYPE } from '~/releases/constants'; import createReleaseAssetLinkMutation from '~/releases/graphql/mutations/create_release_link.mutation.graphql'; @@ -20,6 +22,7 @@ jest.mock('~/api/tags_api'); jest.mock('~/alert'); +jest.mock('~/lib/utils/accessor'); jest.mock('~/lib/utils/url_utility', () => ({ redirectTo: jest.fn(), joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths, @@ -34,78 +37,203 @@ jest.mock('~/releases/util', () => ({ })); describe('Release edit/new actions', () => { + useLocalStorageSpy(); + let state; let releaseResponse; let error; const projectPath = 'test/project-path'; + const draftActions = [{ type: 'saveDraftRelease' }, { type: 'saveDraftCreateFrom' }]; const setupState = (updates = {}) => { state = { ...createState({ projectPath, projectId: '18', - isExistingRelease: true, + isExistingRelease: false, tagName: releaseResponse.tag_name, releasesPagePath: 'path/to/releases/page', markdownDocsPath: 'path/to/markdown/docs', markdownPreviewPath: 'path/to/markdown/preview', }), + localStorageKey: `${projectPath}/release/new`, + localStorageCreateFromKey: `${projectPath}/release/new/createFrom`, ...updates, }; }; beforeEach(() => { + AccessorUtilities.canUseLocalStorage.mockReturnValue(true); releaseResponse = cloneDeep(originalOneReleaseForEditingQueryResponse); gon.api_version = 'v4'; error = new Error('Yikes!'); createAlert.mockClear(); }); + afterEach(() => { + jest.clearAllMocks(); + }); + describe('when creating a new release', () => { beforeEach(() => { setupState({ isExistingRelease: false }); }); describe('initializeRelease', () => { - it(`commits ${types.INITIALIZE_EMPTY_RELEASE}`, () => { - testAction(actions.initializeRelease, undefined, state, [ - { type: types.INITIALIZE_EMPTY_RELEASE }, - ]); + it('dispatches loadDraftRelease', () => { + return testAction({ + action: actions.initializeRelease, + state, + expectedMutations: [], + expectedActions: [{ type: 'loadDraftRelease' }], + }); + }); + }); + + describe('loadDraftRelease', () => { + it(`with no saved release, it commits ${types.INITIALIZE_EMPTY_RELEASE}`, () => { + testAction({ + action: actions.loadDraftRelease, + state, + expectedMutations: [{ type: types.INITIALIZE_EMPTY_RELEASE }], + }); + }); + + it('with saved release, loads the release from local storage', () => { + const release = { + tagName: 'v1.3', + tagMessage: 'hello', + name: '', + description: '', + milestones: [], + groupMilestones: [], + releasedAt: new Date(), + assets: { + links: [], + }, + }; + const createFrom = 'main'; + + window.localStorage.setItem(`${state.projectPath}/release/new`, JSON.stringify(release)); + window.localStorage.setItem( + `${state.projectPath}/release/new/createFrom`, + JSON.stringify(createFrom), + ); + + return testAction({ + action: actions.loadDraftRelease, + state, + expectedMutations: [ + { type: types.INITIALIZE_RELEASE, payload: release }, + { type: types.UPDATE_CREATE_FROM, payload: createFrom }, + ], + }); + }); + }); + + describe('clearDraftRelease', () => { + it('calls window.localStorage.clear', () => { + return testAction({ action: actions.clearDraftRelease, state }).then(() => { + expect(window.localStorage.removeItem).toHaveBeenCalledTimes(2); + expect(window.localStorage.removeItem).toHaveBeenCalledWith(state.localStorageKey); + expect(window.localStorage.removeItem).toHaveBeenCalledWith( + state.localStorageCreateFromKey, + ); + }); + }); + }); + + describe('saveDraftCreateFrom', () => { + it('saves the create from to local storage', () => { + const createFrom = 'main'; + setupState({ createFrom }); + return testAction({ action: actions.saveDraftCreateFrom, state }).then(() => { + expect(window.localStorage.setItem).toHaveBeenCalledTimes(1); + expect(window.localStorage.setItem).toHaveBeenCalledWith( + state.localStorageCreateFromKey, + JSON.stringify(createFrom), + ); + }); + }); + }); + + describe('saveDraftRelease', () => { + let release; + + beforeEach(() => { + release = { + tagName: 'v1.3', + tagMessage: 'hello', + name: '', + description: '', + milestones: [], + groupMilestones: [], + releasedAt: new Date(), + assets: { + links: [], + }, + }; + }); + + it('saves the draft release to local storage', () => { + setupState({ release, releasedAtChanged: true }); + + return testAction({ action: actions.saveDraftRelease, state }).then(() => { + expect(window.localStorage.setItem).toHaveBeenCalledTimes(1); + expect(window.localStorage.setItem).toHaveBeenCalledWith( + state.localStorageKey, + JSON.stringify(state.release), + ); + }); + }); + + it('ignores the released at date if it has not been changed', () => { + setupState({ release, releasedAtChanged: false }); + + return testAction({ action: actions.saveDraftRelease, state }).then(() => { + expect(window.localStorage.setItem).toHaveBeenCalledTimes(1); + expect(window.localStorage.setItem).toHaveBeenCalledWith( + state.localStorageKey, + JSON.stringify({ ...state.release, releasedAt: undefined }), + ); + }); }); }); describe('saveRelease', () => { it(`commits ${types.REQUEST_SAVE_RELEASE} and then dispatched "createRelease"`, () => { - testAction( - actions.saveRelease, - undefined, + testAction({ + action: actions.saveRelease, state, - [{ type: types.REQUEST_SAVE_RELEASE }], - [{ type: 'createRelease' }], - ); + expectedMutations: [{ type: types.REQUEST_SAVE_RELEASE }], + expectedActions: [{ type: 'createRelease' }], + }); }); }); }); describe('when editing an existing release', () => { - beforeEach(setupState); + beforeEach(() => setupState({ isExistingRelease: true })); describe('initializeRelease', () => { it('dispatches "fetchRelease"', () => { - testAction(actions.initializeRelease, undefined, state, [], [{ type: 'fetchRelease' }]); + testAction({ + action: actions.initializeRelease, + state, + expectedActions: [{ type: 'fetchRelease' }], + }); }); }); describe('saveRelease', () => { it(`commits ${types.REQUEST_SAVE_RELEASE} and then dispatched "updateRelease"`, () => { - testAction( - actions.saveRelease, - undefined, + testAction({ + action: actions.saveRelease, state, - [{ type: types.REQUEST_SAVE_RELEASE }], - [{ type: 'updateRelease' }], - ); + expectedMutations: [{ type: types.REQUEST_SAVE_RELEASE }], + expectedActions: [{ type: 'updateRelease' }], + }); }); }); }); @@ -120,15 +248,19 @@ describe('Release edit/new actions', () => { }); it(`commits ${types.REQUEST_RELEASE} and then commits ${types.RECEIVE_RELEASE_SUCCESS} with the converted release object`, () => { - return testAction(actions.fetchRelease, undefined, state, [ - { - type: types.REQUEST_RELEASE, - }, - { - type: types.RECEIVE_RELEASE_SUCCESS, - payload: convertOneReleaseGraphQLResponse(releaseResponse).data, - }, - ]); + return testAction({ + action: actions.fetchRelease, + state, + expectedMutations: [ + { + type: types.REQUEST_RELEASE, + }, + { + type: types.RECEIVE_RELEASE_SUCCESS, + payload: convertOneReleaseGraphQLResponse(releaseResponse).data, + }, + ], + }); }); }); @@ -138,15 +270,19 @@ describe('Release edit/new actions', () => { }); it(`commits ${types.REQUEST_RELEASE} and then commits ${types.RECEIVE_RELEASE_ERROR} with an error object`, () => { - return testAction(actions.fetchRelease, undefined, state, [ - { - type: types.REQUEST_RELEASE, - }, - { - type: types.RECEIVE_RELEASE_ERROR, - payload: expect.any(Error), - }, - ]); + return testAction({ + action: actions.fetchRelease, + state, + expectedMutations: [ + { + type: types.REQUEST_RELEASE, + }, + { + type: types.RECEIVE_RELEASE_ERROR, + payload: expect.any(Error), + }, + ], + }); }); it(`shows an alert message`, () => { @@ -163,89 +299,140 @@ describe('Release edit/new actions', () => { describe('updateReleaseTagName', () => { it(`commits ${types.UPDATE_RELEASE_TAG_NAME} with the updated tag name`, () => { const newTag = 'updated-tag-name'; - return testAction(actions.updateReleaseTagName, newTag, state, [ - { type: types.UPDATE_RELEASE_TAG_NAME, payload: newTag }, - ]); + return testAction({ + action: actions.updateReleaseTagName, + payload: newTag, + state, + expectedMutations: [{ type: types.UPDATE_RELEASE_TAG_NAME, payload: newTag }], + expectedActions: draftActions, + }); + }); + it('does not save drafts when editing', () => { + const newTag = 'updated-tag-name'; + return testAction({ + action: actions.updateReleaseTagName, + payload: newTag, + state: { ...state, isExistingRelease: true }, + expectedMutations: [{ type: types.UPDATE_RELEASE_TAG_NAME, payload: newTag }], + }); }); }); describe('updateReleaseTagMessage', () => { it(`commits ${types.UPDATE_RELEASE_TAG_MESSAGE} with the updated tag name`, () => { const newMessage = 'updated-tag-message'; - return testAction(actions.updateReleaseTagMessage, newMessage, state, [ - { type: types.UPDATE_RELEASE_TAG_MESSAGE, payload: newMessage }, - ]); + return testAction({ + action: actions.updateReleaseTagMessage, + payload: newMessage, + state, + expectedMutations: [{ type: types.UPDATE_RELEASE_TAG_MESSAGE, payload: newMessage }], + expectedActions: draftActions, + }); }); }); describe('updateReleasedAt', () => { it(`commits ${types.UPDATE_RELEASED_AT} with the updated date`, () => { const newDate = new Date(); - return testAction(actions.updateReleasedAt, newDate, state, [ - { type: types.UPDATE_RELEASED_AT, payload: newDate }, - ]); + return testAction({ + action: actions.updateReleasedAt, + payload: newDate, + state, + expectedMutations: [{ type: types.UPDATE_RELEASED_AT, payload: newDate }], + expectedActions: draftActions, + }); }); }); describe('updateCreateFrom', () => { it(`commits ${types.UPDATE_CREATE_FROM} with the updated ref`, () => { const newRef = 'my-feature-branch'; - return testAction(actions.updateCreateFrom, newRef, state, [ - { type: types.UPDATE_CREATE_FROM, payload: newRef }, - ]); + return testAction({ + action: actions.updateCreateFrom, + payload: newRef, + state, + expectedMutations: [{ type: types.UPDATE_CREATE_FROM, payload: newRef }], + expectedActions: draftActions, + }); }); }); describe('updateShowCreateFrom', () => { it(`commits ${types.UPDATE_SHOW_CREATE_FROM} with the updated ref`, () => { const newRef = 'my-feature-branch'; - return testAction(actions.updateShowCreateFrom, newRef, state, [ - { type: types.UPDATE_SHOW_CREATE_FROM, payload: newRef }, - ]); + return testAction({ + action: actions.updateShowCreateFrom, + payload: newRef, + state, + expectedMutations: [{ type: types.UPDATE_SHOW_CREATE_FROM, payload: newRef }], + }); }); }); describe('updateReleaseTitle', () => { it(`commits ${types.UPDATE_RELEASE_TITLE} with the updated release title`, () => { const newTitle = 'The new release title'; - return testAction(actions.updateReleaseTitle, newTitle, state, [ - { type: types.UPDATE_RELEASE_TITLE, payload: newTitle }, - ]); + return testAction({ + action: actions.updateReleaseTitle, + payload: newTitle, + state, + expectedMutations: [{ type: types.UPDATE_RELEASE_TITLE, payload: newTitle }], + expectedActions: draftActions, + }); }); }); describe('updateReleaseNotes', () => { it(`commits ${types.UPDATE_RELEASE_NOTES} with the updated release notes`, () => { const newReleaseNotes = 'The new release notes'; - return testAction(actions.updateReleaseNotes, newReleaseNotes, state, [ - { type: types.UPDATE_RELEASE_NOTES, payload: newReleaseNotes }, - ]); + return testAction({ + action: actions.updateReleaseNotes, + payload: newReleaseNotes, + state, + expectedMutations: [{ type: types.UPDATE_RELEASE_NOTES, payload: newReleaseNotes }], + expectedActions: draftActions, + }); }); }); describe('updateReleaseMilestones', () => { it(`commits ${types.UPDATE_RELEASE_MILESTONES} with the updated release milestones`, () => { const newReleaseMilestones = ['v0.0', 'v0.1']; - return testAction(actions.updateReleaseMilestones, newReleaseMilestones, state, [ - { type: types.UPDATE_RELEASE_MILESTONES, payload: newReleaseMilestones }, - ]); + return testAction({ + action: actions.updateReleaseMilestones, + payload: newReleaseMilestones, + state, + expectedMutations: [ + { type: types.UPDATE_RELEASE_MILESTONES, payload: newReleaseMilestones }, + ], + expectedActions: draftActions, + }); }); }); describe('updateReleaseGroupMilestones', () => { it(`commits ${types.UPDATE_RELEASE_GROUP_MILESTONES} with the updated release group milestones`, () => { const newReleaseGroupMilestones = ['v0.0', 'v0.1']; - return testAction(actions.updateReleaseGroupMilestones, newReleaseGroupMilestones, state, [ - { type: types.UPDATE_RELEASE_GROUP_MILESTONES, payload: newReleaseGroupMilestones }, - ]); + return testAction({ + action: actions.updateReleaseGroupMilestones, + payload: newReleaseGroupMilestones, + state, + expectedMutations: [ + { type: types.UPDATE_RELEASE_GROUP_MILESTONES, payload: newReleaseGroupMilestones }, + ], + expectedActions: draftActions, + }); }); }); describe('addEmptyAssetLink', () => { it(`commits ${types.ADD_EMPTY_ASSET_LINK}`, () => { - return testAction(actions.addEmptyAssetLink, undefined, state, [ - { type: types.ADD_EMPTY_ASSET_LINK }, - ]); + return testAction({ + action: actions.addEmptyAssetLink, + state, + expectedMutations: [{ type: types.ADD_EMPTY_ASSET_LINK }], + expectedActions: draftActions, + }); }); }); @@ -256,9 +443,13 @@ describe('Release edit/new actions', () => { newUrl: 'https://example.com/updated', }; - return testAction(actions.updateAssetLinkUrl, params, state, [ - { type: types.UPDATE_ASSET_LINK_URL, payload: params }, - ]); + return testAction({ + action: actions.updateAssetLinkUrl, + payload: params, + state, + expectedMutations: [{ type: types.UPDATE_ASSET_LINK_URL, payload: params }], + expectedActions: draftActions, + }); }); }); @@ -269,9 +460,13 @@ describe('Release edit/new actions', () => { newName: 'Updated link name', }; - return testAction(actions.updateAssetLinkName, params, state, [ - { type: types.UPDATE_ASSET_LINK_NAME, payload: params }, - ]); + return testAction({ + action: actions.updateAssetLinkName, + payload: params, + state, + expectedMutations: [{ type: types.UPDATE_ASSET_LINK_NAME, payload: params }], + expectedActions: draftActions, + }); }); }); @@ -282,30 +477,45 @@ describe('Release edit/new actions', () => { newType: ASSET_LINK_TYPE.RUNBOOK, }; - return testAction(actions.updateAssetLinkType, params, state, [ - { type: types.UPDATE_ASSET_LINK_TYPE, payload: params }, - ]); + return testAction({ + action: actions.updateAssetLinkType, + payload: params, + state, + expectedMutations: [{ type: types.UPDATE_ASSET_LINK_TYPE, payload: params }], + expectedActions: draftActions, + }); }); }); describe('removeAssetLink', () => { it(`commits ${types.REMOVE_ASSET_LINK} with the ID of the asset link to remove`, () => { const idToRemove = 2; - return testAction(actions.removeAssetLink, idToRemove, state, [ - { type: types.REMOVE_ASSET_LINK, payload: idToRemove }, - ]); + return testAction({ + action: actions.removeAssetLink, + payload: idToRemove, + state, + expectedMutations: [{ type: types.REMOVE_ASSET_LINK, payload: idToRemove }], + expectedActions: draftActions, + }); }); }); describe('receiveSaveReleaseSuccess', () => { - it(`commits ${types.RECEIVE_SAVE_RELEASE_SUCCESS}`, () => - testAction(actions.receiveSaveReleaseSuccess, releaseResponse, state, [ - { type: types.RECEIVE_SAVE_RELEASE_SUCCESS }, - ])); + it(`commits ${types.RECEIVE_SAVE_RELEASE_SUCCESS} and dispatches clearDraftRelease`, () => + testAction({ + action: actions.receiveSaveReleaseSuccess, + payload: releaseResponse, + state, + expectedMutations: [{ type: types.RECEIVE_SAVE_RELEASE_SUCCESS }], + expectedActions: [{ type: 'clearDraftRelease' }], + })); it("redirects to the release's dedicated page", () => { const { selfUrl } = releaseResponse.data.project.release.links; - actions.receiveSaveReleaseSuccess({ commit: jest.fn(), state }, selfUrl); + actions.receiveSaveReleaseSuccess( + { commit: jest.fn(), state, dispatch: jest.fn() }, + selfUrl, + ); expect(redirectTo).toHaveBeenCalledTimes(1); // eslint-disable-line import/no-deprecated expect(redirectTo).toHaveBeenCalledWith(selfUrl); // eslint-disable-line import/no-deprecated }); @@ -346,18 +556,16 @@ describe('Release edit/new actions', () => { }); it(`dispatches "receiveSaveReleaseSuccess" with the converted release object`, () => { - return testAction( - actions.createRelease, - undefined, + return testAction({ + action: actions.createRelease, state, - [], - [ + expectedActions: [ { type: 'receiveSaveReleaseSuccess', payload: selfUrl, }, ], - ); + }); }); }); @@ -367,12 +575,16 @@ describe('Release edit/new actions', () => { }); it(`commits ${types.RECEIVE_SAVE_RELEASE_ERROR} with an error object`, () => { - return testAction(actions.createRelease, undefined, state, [ - { - type: types.RECEIVE_SAVE_RELEASE_ERROR, - payload: expect.any(Error), - }, - ]); + return testAction({ + action: actions.createRelease, + state, + expectedMutations: [ + { + type: types.RECEIVE_SAVE_RELEASE_ERROR, + payload: expect.any(Error), + }, + ], + }); }); it(`shows an alert message`, () => { @@ -393,12 +605,16 @@ describe('Release edit/new actions', () => { }); it(`commits ${types.RECEIVE_SAVE_RELEASE_ERROR} with an error object`, () => { - return testAction(actions.createRelease, undefined, state, [ - { - type: types.RECEIVE_SAVE_RELEASE_ERROR, - payload: expect.any(Error), - }, - ]); + return testAction({ + action: actions.createRelease, + state, + expectedMutations: [ + { + type: types.RECEIVE_SAVE_RELEASE_ERROR, + payload: expect.any(Error), + }, + ], + }); }); it(`shows an alert message`, () => { @@ -760,16 +976,15 @@ describe('Release edit/new actions', () => { const tag = { message: 'this is a tag' }; getTag.mockResolvedValue({ data: tag }); - await testAction( - actions.fetchTagNotes, - tagName, + await testAction({ + action: actions.fetchTagNotes, + payload: tagName, state, - [ + expectedMutations: [ { type: types.REQUEST_TAG_NOTES }, { type: types.RECEIVE_TAG_NOTES_SUCCESS, payload: tag }, ], - [], - ); + }); expect(getTag).toHaveBeenCalledWith(state.projectId, tagName); }); @@ -777,16 +992,15 @@ describe('Release edit/new actions', () => { error = new Error(); getTag.mockRejectedValue(error); - await testAction( - actions.fetchTagNotes, - tagName, + await testAction({ + action: actions.fetchTagNotes, + payload: tagName, state, - [ + expectedMutations: [ { type: types.REQUEST_TAG_NOTES }, { type: types.RECEIVE_TAG_NOTES_ERROR, payload: error }, ], - [], - ); + }); expect(createAlert).toHaveBeenCalledWith({ message: s__('Release|Unable to fetch the tag notes.'), diff --git a/spec/frontend/releases/stores/modules/detail/getters_spec.js b/spec/frontend/releases/stores/modules/detail/getters_spec.js index 736eae13fb3..24490e19296 100644 --- a/spec/frontend/releases/stores/modules/detail/getters_spec.js +++ b/spec/frontend/releases/stores/modules/detail/getters_spec.js @@ -470,4 +470,20 @@ describe('Release edit/new getters', () => { expect(getters.releasedAtChanged({ originalReleasedAt, release: { releasedAt } })).toBe(true); }); }); + + describe('localStorageKey', () => { + it('returns a string key with the project path for local storage', () => { + const projectPath = 'test/project'; + expect(getters.localStorageKey({ projectPath })).toBe('test/project/release/new'); + }); + }); + + describe('localStorageCreateFromKey', () => { + it('returns a string key with the project path for local storage', () => { + const projectPath = 'test/project'; + expect(getters.localStorageCreateFromKey({ projectPath })).toBe( + 'test/project/release/new/createFrom', + ); + }); + }); }); diff --git a/spec/lib/gitlab/ci/config/header/input_spec.rb b/spec/lib/gitlab/ci/config/header/input_spec.rb index b5155dff6e8..b4e02c2b005 100644 --- a/spec/lib/gitlab/ci/config/header/input_spec.rb +++ b/spec/lib/gitlab/ci/config/header/input_spec.rb @@ -46,6 +46,12 @@ RSpec.describe Gitlab::Ci::Config::Header::Input, feature_category: :pipeline_co it_behaves_like 'a valid input' end + context 'when has a description value' do + let(:input_hash) { { description: 'bar' } } + + it_behaves_like 'a valid input' + end + context 'when is a required input' do let(:input_hash) { nil } diff --git a/spec/lib/gitlab/git_audit_event_spec.rb b/spec/lib/gitlab/git_audit_event_spec.rb new file mode 100644 index 00000000000..c533b39f550 --- /dev/null +++ b/spec/lib/gitlab/git_audit_event_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GitAuditEvent, feature_category: :source_code_management do + let_it_be(:player) { create(:user) } + let_it_be(:group) { create(:group, :public) } + let_it_be(:project) { create(:project) } + + subject { described_class.new(player, project) } + + describe '#send_audit_event' do + let(:msg) { 'valid_msg' } + + context 'with successfully sending' do + let_it_be(:project) { create(:project, namespace: group) } + + before do + allow(::Gitlab::Audit::Auditor).to receive(:audit) + end + + context 'when player is a regular user' do + it 'sends git audit event' do + expect(::Gitlab::Audit::Auditor).to receive(:audit).with(a_hash_including( + name: 'repository_git_operation', + stream_only: true, + author: player, + scope: project, + target: project, + message: msg + )).once + + subject.send_audit_event(msg) + end + end + + context 'when player is ::API::Support::GitAccessActor' do + let_it_be(:user) { player } + let_it_be(:key) { create(:key, user: user) } + let_it_be(:git_access_actor) { ::API::Support::GitAccessActor.new(user: user, key: key) } + + subject { described_class.new(git_access_actor, project) } + + it 'sends git audit event' do + expect(::Gitlab::Audit::Auditor).to receive(:audit).with(a_hash_including( + name: 'repository_git_operation', + stream_only: true, + author: git_access_actor.deploy_key_or_user, + scope: project, + target: project, + message: msg + )).once + + subject.send_audit_event(msg) + end + end + end + + context 'when user is blank' do + let_it_be(:player) { nil } + + it 'does not send git audit event' do + expect(::Gitlab::Audit::Auditor).not_to receive(:audit) + + subject.send_audit_event(msg) + end + end + + context 'when project is blank' do + let_it_be(:project) { nil } + + it 'does not send git audit event' do + expect(::Gitlab::Audit::Auditor).not_to receive(:audit) + + subject.send_audit_event(msg) + end + end + end +end diff --git a/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb b/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb deleted file mode 100644 index 66b93d0dd72..00000000000 --- a/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Prometheus::Queries::DeploymentQuery do - let(:environment) { create(:environment, slug: 'environment-slug') } - let(:deployment) { create(:deployment, environment: environment) } - let(:client) { double('prometheus_client') } - - subject { described_class.new(client) } - - around do |example| - time_without_subsecond_values = Time.local(2008, 9, 1, 12, 0, 0) - travel_to(time_without_subsecond_values) { example.run } - end - - it 'sends appropriate queries to prometheus' do - start_time = (deployment.created_at - 30.minutes).to_f - end_time = (deployment.created_at + 30.minutes).to_f - created_at = deployment.created_at.to_f - - expect(client).to receive(:query_range).with('avg(container_memory_usage_bytes{container_name!="POD",environment="environment-slug"}) / 2^20', - start_time: start_time, end_time: end_time) - expect(client).to receive(:query).with('avg(avg_over_time(container_memory_usage_bytes{container_name!="POD",environment="environment-slug"}[30m]))', - time: created_at) - expect(client).to receive(:query).with('avg(avg_over_time(container_memory_usage_bytes{container_name!="POD",environment="environment-slug"}[30m]))', - time: end_time) - - expect(client).to receive(:query_range).with('avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="environment-slug"}[2m])) * 100', - start_time: start_time, end_time: end_time) - expect(client).to receive(:query).with('avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="environment-slug"}[30m])) * 100', - time: created_at) - expect(client).to receive(:query).with('avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="environment-slug"}[30m])) * 100', - time: end_time) - - expect(subject.query(deployment.id)).to eq(memory_values: nil, memory_before: nil, memory_after: nil, - cpu_values: nil, cpu_before: nil, cpu_after: nil) - end -end diff --git a/spec/models/concerns/prometheus_adapter_spec.rb b/spec/models/concerns/prometheus_adapter_spec.rb index abb89832174..d17059ccc6d 100644 --- a/spec/models/concerns/prometheus_adapter_spec.rb +++ b/spec/models/concerns/prometheus_adapter_spec.rb @@ -15,102 +15,6 @@ RSpec.describe PrometheusAdapter, :use_clean_rails_memory_store_caching do end end - let(:environment_query) { Gitlab::Prometheus::Queries::EnvironmentQuery } - - describe '#query' do - describe 'environment' do - let(:environment) { build_stubbed(:environment, slug: 'env-slug') } - - around do |example| - freeze_time { example.run } - end - - context 'with valid data' do - subject { integration.query(:environment, environment) } - - before do - stub_reactive_cache(integration, prometheus_data, environment_query, environment.id) - end - - it 'returns reactive data' do - is_expected.to eq(prometheus_metrics_data) - end - end - end - - describe 'deployment' do - let(:deployment) { build_stubbed(:deployment) } - let(:deployment_query) { Gitlab::Prometheus::Queries::DeploymentQuery } - - around do |example| - freeze_time { example.run } - end - - context 'with valid data' do - subject { integration.query(:deployment, deployment) } - - before do - stub_reactive_cache(integration, prometheus_data, deployment_query, deployment.id) - end - - it 'returns reactive data' do - expect(subject).to eq(prometheus_metrics_data) - end - end - end - end - - describe '#calculate_reactive_cache' do - let(:environment) { create(:environment, slug: 'env-slug') } - - before do - integration.manual_configuration = true - integration.active = true - end - - subject do - integration.calculate_reactive_cache(environment_query.name, environment.id) - end - - around do |example| - freeze_time { example.run } - end - - context 'when integration is inactive' do - before do - integration.active = false - end - - it { is_expected.to be_nil } - end - - context 'when Prometheus responds with valid data' do - before do - stub_all_prometheus_requests(environment.slug) - end - - it { expect(subject.to_json).to eq(prometheus_data.to_json) } - end - - [404, 500].each do |status| - context "when Prometheus responds with #{status}" do - before do - stub_all_prometheus_requests(environment.slug, status: status, body: "QUERY FAILED!") - end - - it { is_expected.to eq(success: false, result: %(#{status} - "QUERY FAILED!")) } - end - end - - context "when client raises Gitlab::PrometheusClient::ConnectionError" do - before do - stub_any_prometheus_request.to_raise(Gitlab::PrometheusClient::ConnectionError) - end - - it { is_expected.to include(success: false, result: kind_of(String)) } - end - end - describe '#build_query_args' do subject { integration.build_query_args(*args) } diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 9d4699cb91e..dcfee7fcc8c 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -1516,42 +1516,6 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ end end - describe '#metrics' do - let_it_be(:project) { create(:project, :with_prometheus_integration) } - - subject { environment.metrics } - - context 'when the environment has metrics' do - before do - allow(environment).to receive(:has_metrics?).and_return(true) - end - - it 'returns the metrics from the deployment service' do - expect(environment.prometheus_adapter) - .to receive(:query).with(:environment, environment) - .and_return(:fake_metrics) - - is_expected.to eq(:fake_metrics) - end - - context 'and the prometheus client is not present' do - before do - allow(environment.prometheus_adapter).to receive(:promethus_client).and_return(nil) - end - - it { is_expected.to be_nil } - end - end - - context 'when the environment does not have metrics' do - before do - allow(environment).to receive(:has_metrics?).and_return(false) - end - - it { is_expected.to be_nil } - end - end - describe '#additional_metrics' do let_it_be(:project) { create(:project, :with_prometheus_integration) } let(:metric_params) { [] } diff --git a/spec/models/vs_code/vs_code_setting_spec.rb b/spec/models/vs_code/vs_code_setting_spec.rb new file mode 100644 index 00000000000..aa206c72f29 --- /dev/null +++ b/spec/models/vs_code/vs_code_setting_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe VsCode::VsCodeSetting, feature_category: :web_ide do + let!(:user) { create(:user) } + let!(:setting) { create(:vscode_setting, user: user, setting_type: 'settings') } + + describe 'validates the presence of required attributes' do + it { is_expected.to validate_presence_of(:setting_type) } + it { is_expected.to validate_presence_of(:content) } + end + + describe 'relationship validation' do + it { is_expected.to belong_to(:user) } + end + + describe '.by_setting_type' do + subject { described_class.by_setting_type('settings') } + + it { is_expected.to contain_exactly(setting) } + end + + describe '.by_user' do + subject { described_class.by_user(user) } + + it { is_expected.to contain_exactly(setting) } + end +end diff --git a/spec/requests/api/vs_code_settings_sync_spec.rb b/spec/requests/api/vs_code_settings_sync_spec.rb new file mode 100644 index 00000000000..092b13e8e68 --- /dev/null +++ b/spec/requests/api/vs_code_settings_sync_spec.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::VsCodeSettingsSync, :aggregate_failures, factory_default: :keep, feature_category: :web_ide do + let_it_be(:user) { create_default(:user) } + let_it_be(:user_token) { create(:personal_access_token) } + + shared_examples "returns unauthorized when not authenticated" do + it 'returns 401 for non-authenticated' do + get api(path) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + shared_examples "returns 200 when authenticated" do + it 'returns 200 when authenticated' do + get api(path, personal_access_token: user_token) + expect(response).to have_gitlab_http_status(:ok) + end + end + + describe 'GET /vscode/settings_sync/v1/manifest' do + let(:path) { "/vscode/settings_sync/v1/manifest" } + + it_behaves_like "returns unauthorized when not authenticated" + it_behaves_like "returns 200 when authenticated" + + context 'when no settings record is present' do + it 'returns a session id' do + get api(path, personal_access_token: user_token) + expect(json_response).to have_key('latest') + expect(json_response).to have_key('session') + end + + it 'returns no latest keys' do + get api(path, personal_access_token: user_token) + expect(json_response).to have_key('latest') + expect(json_response['latest']).not_to have_key('settings') + end + end + + context 'when settings record is present' do + let_it_be(:settings) { create(:vscode_setting) } + + it 'returns the latest keys' do + get api(path, personal_access_token: user_token) + expect(json_response).to have_key('latest') + expect(json_response).to have_key('session') + expect(json_response['latest']).to have_key('settings') + expect(json_response.dig('latest', 'settings')).to eq settings.id + end + end + end + + describe 'GET /vscode/settings_sync/v1/resource/machines/latest' do + let(:path) { "/vscode/settings_sync/v1/resource/machines/latest" } + + it_behaves_like "returns unauthorized when not authenticated" + it_behaves_like "returns 200 when authenticated" + + it 'returns a list of machines' do + get api(path, personal_access_token: user_token) + expect(json_response).to have_key('version') + expect(json_response).to have_key('machines') + expect(json_response['machines']).to be_an Array + expect(json_response['machines'].first).to have_key('id') + end + end + + describe 'GET /vscode/settings_sync/v1/resource/:resource_name/:id' do + let(:path) { "/vscode/settings_sync/v1/resource/settings/1" } + + it_behaves_like "returns 200 when authenticated" + it_behaves_like "returns unauthorized when not authenticated" + + context 'when settings with that type are not present' do + it 'returns settings with empty json content' do + get api(path, personal_access_token: user_token) + expect(json_response).to have_key('content') + expect(json_response).to have_key('version') + expect(json_response['content']).to eq('{}') + end + end + + context 'when settings with that type are present' do + let_it_be(:settings) { create(:vscode_setting, content: '{ "key": "value" }') } + + it 'returns settings with the correct json content' do + get api(path, personal_access_token: user_token) + expect(json_response).to have_key('content') + expect(json_response).to have_key('version') + expect(json_response['content']).to eq('{ "key": "value" }') + end + end + end + + describe 'POST /vscode/settings_sync/v1/resource/:resource_name' do + let(:path) { "/vscode/settings_sync/v1/resource/settings" } + + subject(:request) do + post api(path, personal_access_token: user_token), params: { content: '{ "editor.fontSize": 12 }' } + end + + it 'returns unauthorized when not authenticated' do + post api(path) + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'returns 201 when a valid request is sent' do + request + + expect(response).to have_gitlab_http_status(:created) + end + + it 'creates a new record for the setting when the setting is not present' do + expect { request }.to change { User.find(user.id).vscode_settings.count }.from(0).to(1) + record = User.find(user.id).vscode_settings.by_setting_type('settings').first + expect(record.content).to eq('{ "editor.fontSize": 12 }') + end + + it 'updates a record if the setting is already present' do + create(:vscode_setting) + expect { request }.not_to change { User.find(user.id).vscode_settings.count } + record = User.find(user.id).vscode_settings.by_setting_type('settings').first + expect(record.content).to eq('{ "editor.fontSize": 12 }') + end + + it 'fails if required fields not passed' do + post api(path, personal_access_token: user_token), params: {} + expect(response).to have_gitlab_http_status(:bad_request) + end + end +end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index a28dd9e7a55..11f9708f9f3 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -1953,6 +1953,32 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes expect(pipeline.statuses.count).to eq 2 expect(pipeline.statuses.map(&:name)).to match_array %w[test-1 test-my-job] end + + context 'when inputs have a description' do + let(:template) do + <<~YAML + spec: + inputs: + stage: + suffix: + default: my-job + description: description + --- + test-$[[ inputs.suffix ]]: + stage: $[[ inputs.stage ]] + script: run tests + YAML + end + + it 'creates a pipeline' do + response = execute_service(save_on_errors: true) + + pipeline = response.payload + + expect(pipeline).to be_persisted + expect(pipeline.yaml_errors).to be_blank + end + end end context 'when interpolation is invalid' do diff --git a/spec/services/vs_code/settings/create_or_update_service_spec.rb b/spec/services/vs_code/settings/create_or_update_service_spec.rb new file mode 100644 index 00000000000..c2749cd01bc --- /dev/null +++ b/spec/services/vs_code/settings/create_or_update_service_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe VsCode::Settings::CreateOrUpdateService, feature_category: :web_ide do + describe '#execute' do + let_it_be(:user) { create(:user) } + + let(:opts) do + { + setting_type: "settings", + content: '{ "editor.fontSize": 12 }' + } + end + + subject { described_class.new(current_user: user, params: opts).execute } + + it 'creates a new record when a record with the setting does not exist' do + expect { subject }.to change { User.find(user.id).vscode_settings.count }.from(0).to(1) + record = User.find(user.id).vscode_settings.by_setting_type('settings').first + expect(record.content).to eq('{ "editor.fontSize": 12 }') + end + + it 'updates the existing record if setting exists' do + setting = create(:vscode_setting, user: user) + + expect { subject }.to change { + VsCode::VsCodeSetting.find(setting.id).content + }.from(setting.content).to(opts[:content]) + end + + it 'fails if an invalid value is passed' do + invalid_opts = { setting_type: nil, content: nil } + result = described_class.new(current_user: user, params: invalid_opts).execute + + expect(result.status).to eq(:error) + end + end +end diff --git a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb index b308295b5fb..627d1b33489 100644 --- a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb +++ b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb @@ -263,7 +263,7 @@ RSpec.shared_examples 'namespace traversal scopes' do include_examples '.self_and_descendant_ids' end - shared_examples '.self_and_hierarchy' do + describe '.self_and_hierarchy' do let(:base_scope) { Group.where(id: base_groups) } subject { base_scope.self_and_hierarchy } @@ -292,21 +292,4 @@ RSpec.shared_examples 'namespace traversal scopes' do it { is_expected.to contain_exactly(group_1, nested_group_1, deep_nested_group_1) } end end - - describe '.self_and_hierarchy' do - it_behaves_like '.self_and_hierarchy' - - context "use_traversal_ids_for_self_and_hierarchy_scopes feature flag is false" do - before do - stub_feature_flags(use_traversal_ids_for_self_and_hierarchy_scopes: false) - end - - it_behaves_like '.self_and_hierarchy' - - it 'makes recursive queries' do - base_groups = Group.where(id: nested_group_1) - expect { base_groups.self_and_hierarchy.load }.to make_queries_matching(/WITH RECURSIVE/) - end - end - end end |