diff options
Diffstat (limited to 'spec')
38 files changed, 699 insertions, 225 deletions
diff --git a/spec/features/callouts/registration_enabled_spec.rb b/spec/features/callouts/registration_enabled_spec.rb index 79e99712183..4fc73863de6 100644 --- a/spec/features/callouts/registration_enabled_spec.rb +++ b/spec/features/callouts/registration_enabled_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Registration enabled callout' do +RSpec.describe 'Registration enabled callout', feature_category: :authentication_and_authorization do let_it_be(:admin) { create(:admin) } let_it_be(:non_admin) { create(:user) } let_it_be(:project) { create(:project) } diff --git a/spec/features/clusters/cluster_detail_page_spec.rb b/spec/features/clusters/cluster_detail_page_spec.rb index 06e3e00db7d..e8fb5f4105d 100644 --- a/spec/features/clusters/cluster_detail_page_spec.rb +++ b/spec/features/clusters/cluster_detail_page_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Clusterable > Show page' do +RSpec.describe 'Clusterable > Show page', feature_category: :kubernetes_management do include KubernetesHelpers let(:current_user) { create(:user) } diff --git a/spec/features/clusters/cluster_health_dashboard_spec.rb b/spec/features/clusters/cluster_health_dashboard_spec.rb index 88d6976c2be..b557f803a99 100644 --- a/spec/features/clusters/cluster_health_dashboard_spec.rb +++ b/spec/features/clusters/cluster_health_dashboard_spec.rb @@ -2,7 +2,8 @@ require 'spec_helper' -RSpec.describe 'Cluster Health board', :js, :kubeclient, :use_clean_rails_memory_store_caching, :sidekiq_inline do +RSpec.describe 'Cluster Health board', :js, :kubeclient, :use_clean_rails_memory_store_caching, :sidekiq_inline, +feature_category: :kubernetes_management do include KubernetesHelpers include PrometheusHelpers diff --git a/spec/features/clusters/create_agent_spec.rb b/spec/features/clusters/create_agent_spec.rb index b19e57c550c..d01fa520cb0 100644 --- a/spec/features/clusters/create_agent_spec.rb +++ b/spec/features/clusters/create_agent_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Cluster agent registration', :js do +RSpec.describe 'Cluster agent registration', :js, feature_category: :kubernetes_management do let_it_be(:project) { create(:project, :custom_repo, files: { '.gitlab/agents/example-agent-1/config.yaml' => '' }) } let_it_be(:current_user) { create(:user, maintainer_projects: [project]) } let_it_be(:token) { Devise.friendly_token } diff --git a/spec/features/commits/user_uses_quick_actions_spec.rb b/spec/features/commits/user_uses_quick_actions_spec.rb index 12e7865e490..6d043a0bb2f 100644 --- a/spec/features/commits/user_uses_quick_actions_spec.rb +++ b/spec/features/commits/user_uses_quick_actions_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Commit > User uses quick actions', :js do +RSpec.describe 'Commit > User uses quick actions', :js, feature_category: :source_code_management do include Spec::Support::Helpers::Features::NotesHelpers include RepoHelpers diff --git a/spec/features/commits/user_view_commits_spec.rb b/spec/features/commits/user_view_commits_spec.rb index f7fd3a6e209..b58d7cf3741 100644 --- a/spec/features/commits/user_view_commits_spec.rb +++ b/spec/features/commits/user_view_commits_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Commit > User view commits' do +RSpec.describe 'Commit > User view commits', feature_category: :source_code_management do let_it_be(:user) { create(:user) } let_it_be(:group) { create(:group, :public) } diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 96a8168e708..4f7b7b5b98f 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -215,10 +215,6 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do visit project_job_path(project, job) end - it 'shows retry button' do - expect(page).to have_link('Retry') - end - context 'if job passed' do it 'does not show New issue button' do expect(page).not_to have_link('New issue') diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb index f14c60c4b8f..18f8d1adecc 100644 --- a/spec/finders/branches_finder_spec.rb +++ b/spec/finders/branches_finder_spec.rb @@ -72,16 +72,6 @@ RSpec.describe BranchesFinder do end end - context 'with an unknown name' do - let(:params) { { search: 'random' } } - - it 'does not find any branch' do - result = subject - - expect(result.count).to eq(0) - end - end - context 'by provided names' do let(:params) { { names: %w[fix csv lfs does-not-exist] } } @@ -115,6 +105,49 @@ RSpec.describe BranchesFinder do end end + context 'by name with wildcard' do + let(:params) { { search: 'f*e' } } + + it 'filters branches' do + result = subject + + expect(result.first.name).to eq('2-mb-file') + expect(result.count).to eq(30) + end + end + + context 'by mixed regex operators' do + let(:params) { { search: '^f*e$' } } + + it 'filters branches' do + result = subject + + expect(result.first.name).to eq('feature') + expect(result.count).to eq(1) + end + end + + context 'by name with multiple wildcards' do + let(:params) { { search: 'f*a*e' } } + + it 'filters branches' do + result = subject + + expect(result.first.name).to eq('after-create-delete-modify-move') + expect(result.count).to eq(11) + end + end + + context 'with an unknown name' do + let(:params) { { search: 'random' } } + + it 'does not find any branch' do + result = subject + + expect(result.count).to eq(0) + end + end + context 'by nonexistent name that begins with' do let(:params) { { search: '^nope' } } @@ -134,6 +167,16 @@ RSpec.describe BranchesFinder do expect(result.count).to eq(0) end end + + context 'by nonexistent name with wildcard' do + let(:params) { { search: 'zz*asdf' } } + + it 'filters branches' do + result = subject + + expect(result.count).to eq(0) + end + end end context 'filter and sort' do diff --git a/spec/finders/tags_finder_spec.rb b/spec/finders/tags_finder_spec.rb index 0bf9b228c8a..2af23c466fb 100644 --- a/spec/finders/tags_finder_spec.rb +++ b/spec/finders/tags_finder_spec.rb @@ -68,6 +68,14 @@ RSpec.describe TagsFinder do expect(result.count).to eq(1) end + it 'filters tags by name with wildcard' do + result = load_tags({ search: 'v1.*.0' }) + + expect(result.first.name).to eq('v1.0.0') + expect(result.second.name).to eq('v1.1.0') + expect(result.count).to eq(2) + end + it 'filters tags by nonexistent name that begins with' do result = load_tags({ search: '^nope' }) @@ -79,6 +87,11 @@ RSpec.describe TagsFinder do expect(result.count).to eq(0) end + it 'filters tags by nonexistent name with wildcard' do + result = load_tags({ search: 'n*e' }) + expect(result.count).to eq(0) + end + context 'when search is not a string' do it 'returns no matches' do result = load_tags({ search: { 'a' => 'b' } }) diff --git a/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap b/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap index 4693d5a47e4..bff4905a12c 100644 --- a/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap +++ b/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap @@ -16,7 +16,7 @@ exports[`Alert integration settings form default state should match the default > <gl-form-checkbox-stub checked="true" - data-qa-selector="create_issue_checkbox" + data-qa-selector="create_incident_checkbox" id="2" > <span> diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js index 9921d8cba18..2f9fd957c6b 100644 --- a/spec/frontend/ide/components/repo_editor_spec.js +++ b/spec/frontend/ide/components/repo_editor_spec.js @@ -8,9 +8,14 @@ import '~/behaviors/markdown/render_gfm'; import waitForPromises from 'helpers/wait_for_promises'; import { stubPerformanceWebAPI } from 'helpers/performance'; import { exampleConfigs, exampleFiles } from 'jest/ide/lib/editorconfig/mock_data'; -import { EDITOR_CODE_INSTANCE_FN, EDITOR_DIFF_INSTANCE_FN } from '~/editor/constants'; +import { + EDITOR_CODE_INSTANCE_FN, + EDITOR_DIFF_INSTANCE_FN, + EXTENSION_CI_SCHEMA_FILE_NAME_MATCH, +} from '~/editor/constants'; import { EditorMarkdownExtension } from '~/editor/extensions/source_editor_markdown_ext'; import { EditorMarkdownPreviewExtension } from '~/editor/extensions/source_editor_markdown_livepreview_ext'; +import { CiSchemaExtension } from '~/editor/extensions/source_editor_ci_schema_ext'; import SourceEditor from '~/editor/source_editor'; import RepoEditor from '~/ide/components/repo_editor.vue'; import { leftSidebarViews, FILE_VIEW_MODE_PREVIEW, viewerTypes } from '~/ide/constants'; @@ -22,6 +27,8 @@ import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer import SourceEditorInstance from '~/editor/source_editor_instance'; import { file } from '../helpers'; +jest.mock('~/editor/extensions/source_editor_ci_schema_ext'); + const PREVIEW_MARKDOWN_PATH = '/foo/bar/preview_markdown'; const CURRENT_PROJECT_ID = 'gitlab-org/gitlab'; @@ -46,6 +53,12 @@ const dummyFile = { tempFile: true, active: true, }, + ciConfig: { + ...file(EXTENSION_CI_SCHEMA_FILE_NAME_MATCH), + content: '', + tempFile: true, + active: true, + }, empty: { ...file('empty'), tempFile: false, @@ -101,6 +114,7 @@ describe('RepoEditor', () => { let createDiffInstanceSpy; let createModelSpy; let applyExtensionSpy; + let removeExtensionSpy; let extensionsStore; const waitForEditorSetup = () => @@ -108,7 +122,7 @@ describe('RepoEditor', () => { vm.$once('editorSetup', resolve); }); - const createComponent = async ({ state = {}, activeFile = dummyFile.text } = {}) => { + const createComponent = async ({ state = {}, activeFile = dummyFile.text, flags = {} } = {}) => { const store = prepareStore(state, activeFile); wrapper = shallowMount(RepoEditor, { store, @@ -118,6 +132,9 @@ describe('RepoEditor', () => { mocks: { ContentViewer, }, + provide: { + glFeatures: flags, + }, }); await waitForPromises(); vm = wrapper.vm; @@ -137,6 +154,7 @@ describe('RepoEditor', () => { createDiffInstanceSpy = jest.spyOn(SourceEditor.prototype, EDITOR_DIFF_INSTANCE_FN); createModelSpy = jest.spyOn(monacoEditor, 'createModel'); applyExtensionSpy = jest.spyOn(SourceEditorInstance.prototype, 'use'); + removeExtensionSpy = jest.spyOn(SourceEditorInstance.prototype, 'unuse'); jest.spyOn(service, 'getFileData').mockResolvedValue(); jest.spyOn(service, 'getRawFileData').mockResolvedValue(); }); @@ -177,6 +195,76 @@ describe('RepoEditor', () => { }); }); + describe('schema registration for .gitlab-ci.yml', () => { + const setup = async (activeFile, flagIsOn = true) => { + await createComponent({ + flags: { + schemaLinting: flagIsOn, + }, + }); + vm.editor.registerCiSchema = jest.fn(); + if (activeFile) { + wrapper.setProps({ file: activeFile }); + } + await waitForPromises(); + await nextTick(); + }; + it.each` + flagIsOn | activeFile | shouldUseExtension | desc + ${false} | ${dummyFile.markdown} | ${false} | ${`file is not CI config; should NOT`} + ${true} | ${dummyFile.markdown} | ${false} | ${`file is not CI config; should NOT`} + ${false} | ${dummyFile.ciConfig} | ${false} | ${`file is CI config; should NOT`} + ${true} | ${dummyFile.ciConfig} | ${true} | ${`file is CI config; should`} + `( + 'when the flag is "$flagIsOn", $desc use extension', + async ({ flagIsOn, activeFile, shouldUseExtension }) => { + await setup(activeFile, flagIsOn); + + if (shouldUseExtension) { + expect(applyExtensionSpy).toHaveBeenCalledWith({ + definition: CiSchemaExtension, + }); + } else { + expect(applyExtensionSpy).not.toHaveBeenCalledWith({ + definition: CiSchemaExtension, + }); + } + }, + ); + it('stores the fetched extension and does not double-fetch the schema', async () => { + await setup(); + expect(CiSchemaExtension).toHaveBeenCalledTimes(0); + + wrapper.setProps({ file: dummyFile.ciConfig }); + await waitForPromises(); + await nextTick(); + expect(CiSchemaExtension).toHaveBeenCalledTimes(1); + expect(vm.CiSchemaExtension).toEqual(CiSchemaExtension); + expect(vm.editor.registerCiSchema).toHaveBeenCalledTimes(1); + + wrapper.setProps({ file: dummyFile.markdown }); + await waitForPromises(); + await nextTick(); + expect(CiSchemaExtension).toHaveBeenCalledTimes(1); + expect(vm.editor.registerCiSchema).toHaveBeenCalledTimes(1); + + wrapper.setProps({ file: dummyFile.ciConfig }); + await waitForPromises(); + await nextTick(); + expect(CiSchemaExtension).toHaveBeenCalledTimes(1); + expect(vm.editor.registerCiSchema).toHaveBeenCalledTimes(2); + }); + it('unuses the existing CI extension if the new model is not CI config', async () => { + await setup(dummyFile.ciConfig); + + expect(removeExtensionSpy).not.toHaveBeenCalled(); + wrapper.setProps({ file: dummyFile.markdown }); + await waitForPromises(); + await nextTick(); + expect(removeExtensionSpy).toHaveBeenCalledWith(CiSchemaExtension); + }); + }); + describe('when file is markdown', () => { let mock; let activeFile; diff --git a/spec/frontend/jobs/components/job/empty_state_spec.js b/spec/frontend/jobs/components/job/empty_state_spec.js index 299b607ad78..e1b9aa743e0 100644 --- a/spec/frontend/jobs/components/job/empty_state_spec.js +++ b/spec/frontend/jobs/components/job/empty_state_spec.js @@ -1,5 +1,6 @@ import { mount } from '@vue/test-utils'; import EmptyState from '~/jobs/components/job/empty_state.vue'; +import { mockId } from './mock_data'; describe('Empty State', () => { let wrapper; @@ -7,6 +8,7 @@ describe('Empty State', () => { const defaultProps = { illustrationPath: 'illustrations/pending_job_empty.svg', illustrationSizeClass: 'svg-430', + jobId: mockId, title: 'This job has not started yet', playable: false, }; diff --git a/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js b/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js index 18d5f35bde4..b04a5e07ea5 100644 --- a/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js +++ b/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js @@ -16,6 +16,7 @@ describe('Job Sidebar Retry Button', () => { wrapper = shallowMountExtended(JobsSidebarRetryButton, { propsData: { href: job.retry_path, + isManualJob: true, modalId: 'modal-id', ...props, }, diff --git a/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js b/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js index 95eb10118ee..8fbb418232b 100644 --- a/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js +++ b/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js @@ -32,12 +32,8 @@ describe('Legacy Sidebar Header', () => { }); describe('when job log is erasable', () => { - const path = '/root/ci-project/-/jobs/1447/erase'; - beforeEach(() => { - createWrapper({ - erasePath: path, - }); + createWrapper(); }); it('renders erase job link', () => { @@ -45,13 +41,13 @@ describe('Legacy Sidebar Header', () => { }); it('erase job link has correct path', () => { - expect(findEraseLink().attributes('href')).toBe(path); + expect(findEraseLink().attributes('href')).toBe(job.erase_path); }); }); describe('when job log is not erasable', () => { beforeEach(() => { - createWrapper(); + createWrapper({ job: { ...job, erase_path: null } }); }); it('does not render erase button', () => { @@ -77,8 +73,7 @@ describe('Legacy Sidebar Header', () => { describe('when there is no retry path', () => { it('should not render a retry button', async () => { - const copy = { ...job, retry_path: null }; - createWrapper({ job: copy }); + createWrapper({ job: { ...job, retry_path: null } }); expect(findRetryButton().exists()).toBe(false); }); @@ -100,9 +95,7 @@ describe('Legacy Sidebar Header', () => { it('should have a different label when the job status is failed', () => { createWrapper({ job: { ...job, status: failedJobStatus } }); - expect(findRetryButton().attributes('title')).toBe( - LegacySidebarHeader.i18n.retryJobButtonLabel, - ); + expect(findRetryButton().attributes('title')).toBe(LegacySidebarHeader.i18n.retryJobLabel); }); }); }); diff --git a/spec/frontend/jobs/components/job/manual_variables_form_spec.js b/spec/frontend/jobs/components/job/manual_variables_form_spec.js index 5806f9f75f9..4384b2f4d7f 100644 --- a/spec/frontend/jobs/components/job/manual_variables_form_spec.js +++ b/spec/frontend/jobs/components/job/manual_variables_form_spec.js @@ -1,46 +1,70 @@ import { GlSprintf, GlLink } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; -import Vuex from 'vuex'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import { createLocalVue } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; +import { nextTick } from 'vue'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; +import { GRAPHQL_ID_TYPES } from '~/jobs/constants'; +import waitForPromises from 'helpers/wait_for_promises'; import ManualVariablesForm from '~/jobs/components/job/manual_variables_form.vue'; - -Vue.use(Vuex); +import getJobQuery from '~/jobs/components/job/graphql/queries/get_job.query.graphql'; +import retryJobMutation from '~/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql'; +import { + mockFullPath, + mockId, + mockJobResponse, + mockJobWithVariablesResponse, + mockJobMutationData, +} from './mock_data'; + +const localVue = createLocalVue(); +localVue.use(VueApollo); + +const defaultProvide = { + projectPath: mockFullPath, +}; describe('Manual Variables Form', () => { let wrapper; - let store; - - const requiredProps = { - action: { - path: '/play', - method: 'post', - button_title: 'Trigger this manual action', - }, + let mockApollo; + let getJobQueryResponse; + + const createComponent = ({ options = {}, props = {} } = {}) => { + wrapper = mountExtended(ManualVariablesForm, { + propsData: { + ...props, + jobId: mockId, + }, + provide: { + ...defaultProvide, + }, + ...options, + }); }; - const createComponent = (props = {}) => { - store = new Vuex.Store({ - actions: { - triggerManualJob: jest.fn(), - }, + const createComponentWithApollo = async ({ props = {} } = {}) => { + const requestHandlers = [[getJobQuery, getJobQueryResponse]]; + + mockApollo = createMockApollo(requestHandlers); + + const options = { + localVue, + apolloProvider: mockApollo, + }; + + createComponent({ + props, + options, }); - wrapper = extendedWrapper( - mount(ManualVariablesForm, { - propsData: { ...requiredProps, ...props }, - store, - stubs: { - GlSprintf, - }, - }), - ); + return waitForPromises(); }; const findHelpText = () => wrapper.findComponent(GlSprintf); const findHelpLink = () => wrapper.findComponent(GlLink); - - const findTriggerBtn = () => wrapper.findByTestId('trigger-manual-job-btn'); + const findCancelBtn = () => wrapper.findByTestId('cancel-btn'); + const findRerunBtn = () => wrapper.findByTestId('run-manual-job-btn'); const findDeleteVarBtn = () => wrapper.findByTestId('delete-variable-btn'); const findAllDeleteVarBtns = () => wrapper.findAllByTestId('delete-variable-btn'); const findDeleteVarBtnPlaceholder = () => wrapper.findByTestId('delete-variable-btn-placeholder'); @@ -62,95 +86,134 @@ describe('Manual Variables Form', () => { }; beforeEach(() => { - createComponent(); + getJobQueryResponse = jest.fn(); }); afterEach(() => { wrapper.destroy(); }); - it('creates a new variable when user enters a new key value', async () => { - expect(findAllVariables()).toHaveLength(1); + describe('when page renders', () => { + beforeEach(async () => { + getJobQueryResponse.mockResolvedValue(mockJobResponse); + await createComponentWithApollo(); + }); + + it('renders help text with provided link', () => { + expect(findHelpText().exists()).toBe(true); + expect(findHelpLink().attributes('href')).toBe( + '/help/ci/variables/index#add-a-cicd-variable-to-a-project', + ); + }); + + it('renders buttons', () => { + expect(findCancelBtn().exists()).toBe(true); + expect(findRerunBtn().exists()).toBe(true); + }); + }); + + describe('when job has variables', () => { + beforeEach(async () => { + getJobQueryResponse.mockResolvedValue(mockJobWithVariablesResponse); + await createComponentWithApollo(); + }); - await setCiVariableKey(); + it('sets manual job variables', () => { + const queryKey = mockJobWithVariablesResponse.data.project.job.manualVariables.nodes[0].key; + const queryValue = + mockJobWithVariablesResponse.data.project.job.manualVariables.nodes[0].value; - expect(findAllVariables()).toHaveLength(2); + expect(findCiVariableKey().element.value).toBe(queryKey); + expect(findCiVariableValue().element.value).toBe(queryValue); + }); }); - it('does not create extra empty variables', async () => { - expect(findAllVariables()).toHaveLength(1); + describe('when mutation fires', () => { + beforeEach(async () => { + await createComponentWithApollo(); + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockJobMutationData); + }); - await setCiVariableKey(); + it('passes variables in correct format', async () => { + await setCiVariableKey(); - expect(findAllVariables()).toHaveLength(2); + await findCiVariableValue().setValue('new value'); - await setCiVariableKey(); + await findRerunBtn().vm.$emit('click'); - expect(findAllVariables()).toHaveLength(2); + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1); + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: retryJobMutation, + variables: { + id: convertToGraphQLId(GRAPHQL_ID_TYPES.ciBuild, mockId), + variables: [ + { + key: 'new key', + value: 'new value', + }, + ], + }, + }); + }); }); - it('removes the correct variable row', async () => { - const variableKeyNameOne = 'key-one'; - const variableKeyNameThree = 'key-three'; + describe('updating variables in UI', () => { + beforeEach(async () => { + getJobQueryResponse.mockResolvedValue(mockJobResponse); + await createComponentWithApollo(); + }); - await setCiVariableKeyByPosition(0, variableKeyNameOne); + it('creates a new variable when user enters a new key value', async () => { + expect(findAllVariables()).toHaveLength(1); - await setCiVariableKeyByPosition(1, 'key-two'); + await setCiVariableKey(); - await setCiVariableKeyByPosition(2, variableKeyNameThree); + expect(findAllVariables()).toHaveLength(2); + }); - expect(findAllVariables()).toHaveLength(4); + it('does not create extra empty variables', async () => { + expect(findAllVariables()).toHaveLength(1); - await findAllDeleteVarBtns().at(1).trigger('click'); + await setCiVariableKey(); - expect(findAllVariables()).toHaveLength(3); + expect(findAllVariables()).toHaveLength(2); - expect(findAllCiVariableKeys().at(0).element.value).toBe(variableKeyNameOne); - expect(findAllCiVariableKeys().at(1).element.value).toBe(variableKeyNameThree); - expect(findAllCiVariableKeys().at(2).element.value).toBe(''); - }); + await setCiVariableKey(); - it('trigger button is disabled after trigger action', async () => { - expect(findTriggerBtn().props('disabled')).toBe(false); + expect(findAllVariables()).toHaveLength(2); + }); - await findTriggerBtn().trigger('click'); + it('removes the correct variable row', async () => { + const variableKeyNameOne = 'key-one'; + const variableKeyNameThree = 'key-three'; - expect(findTriggerBtn().props('disabled')).toBe(true); - }); + await setCiVariableKeyByPosition(0, variableKeyNameOne); - it('delete variable button should only show when there is more than one variable', async () => { - expect(findDeleteVarBtn().exists()).toBe(false); + await setCiVariableKeyByPosition(1, 'key-two'); - await setCiVariableKey(); + await setCiVariableKeyByPosition(2, variableKeyNameThree); - expect(findDeleteVarBtn().exists()).toBe(true); - }); + expect(findAllVariables()).toHaveLength(4); - it('delete variable button placeholder should only exist when a user cannot remove', async () => { - expect(findDeleteVarBtnPlaceholder().exists()).toBe(true); - }); + await findAllDeleteVarBtns().at(1).trigger('click'); - it('renders help text with provided link', () => { - expect(findHelpText().exists()).toBe(true); - expect(findHelpLink().attributes('href')).toBe( - '/help/ci/variables/index#add-a-cicd-variable-to-a-project', - ); - }); + expect(findAllVariables()).toHaveLength(3); - it('passes variables in correct format', async () => { - jest.spyOn(store, 'dispatch'); + expect(findAllCiVariableKeys().at(0).element.value).toBe(variableKeyNameOne); + expect(findAllCiVariableKeys().at(1).element.value).toBe(variableKeyNameThree); + expect(findAllCiVariableKeys().at(2).element.value).toBe(''); + }); - await setCiVariableKey(); + it('delete variable button should only show when there is more than one variable', async () => { + expect(findDeleteVarBtn().exists()).toBe(false); - await findCiVariableValue().setValue('new value'); + await setCiVariableKey(); - await findTriggerBtn().trigger('click'); + expect(findDeleteVarBtn().exists()).toBe(true); + }); - expect(store.dispatch).toHaveBeenCalledWith('triggerManualJob', [ - { - key: 'new key', - secret_value: 'new value', - }, - ]); + it('delete variable button placeholder should only exist when a user cannot remove', async () => { + expect(findDeleteVarBtnPlaceholder().exists()).toBe(true); + }); }); }); diff --git a/spec/frontend/jobs/components/job/mock_data.js b/spec/frontend/jobs/components/job/mock_data.js new file mode 100644 index 00000000000..9596e859475 --- /dev/null +++ b/spec/frontend/jobs/components/job/mock_data.js @@ -0,0 +1,76 @@ +export const mockFullPath = 'Commit451/lab-coat'; +export const mockId = 401; + +export const mockJobResponse = { + data: { + project: { + id: 'gid://gitlab/Project/4', + job: { + id: 'gid://gitlab/Ci::Build/401', + manualJob: true, + manualVariables: { + nodes: [], + __typename: 'CiManualVariableConnection', + }, + name: 'manual_job', + retryable: true, + status: 'SUCCESS', + __typename: 'CiJob', + }, + __typename: 'Project', + }, + }, +}; + +export const mockJobWithVariablesResponse = { + data: { + project: { + id: 'gid://gitlab/Project/4', + job: { + id: 'gid://gitlab/Ci::Build/401', + manualJob: true, + manualVariables: { + nodes: [ + { + id: 'gid://gitlab/Ci::JobVariable/150', + key: 'new key', + value: 'new value', + __typename: 'CiManualVariable', + }, + ], + __typename: 'CiManualVariableConnection', + }, + name: 'manual_job', + retryable: true, + status: 'SUCCESS', + __typename: 'CiJob', + }, + __typename: 'Project', + }, + }, +}; + +export const mockJobMutationData = { + data: { + jobRetry: { + job: { + id: 'gid://gitlab/Ci::Build/401', + manualVariables: { + nodes: [ + { + id: 'gid://gitlab/Ci::JobVariable/151', + key: 'new key', + value: 'new value', + __typename: 'CiManualVariable', + }, + ], + __typename: 'CiManualVariableConnection', + }, + webPath: '/Commit451/lab-coat/-/jobs/401', + __typename: 'CiJob', + }, + errors: [], + __typename: 'JobRetryPayload', + }, + }, +}; diff --git a/spec/frontend/jobs/components/job/sidebar_header_spec.js b/spec/frontend/jobs/components/job/sidebar_header_spec.js index cb32ca9d3dc..422e2f6207c 100644 --- a/spec/frontend/jobs/components/job/sidebar_header_spec.js +++ b/spec/frontend/jobs/components/job/sidebar_header_spec.js @@ -1,91 +1,101 @@ -import { shallowMount } from '@vue/test-utils'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import { createLocalVue } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import SidebarHeader from '~/jobs/components/job/sidebar/sidebar_header.vue'; import JobRetryButton from '~/jobs/components/job/sidebar/job_sidebar_retry_button.vue'; -import LegacySidebarHeader from '~/jobs/components/job/sidebar/legacy_sidebar_header.vue'; -import createStore from '~/jobs/store'; -import job from '../../mock_data'; +import getJobQuery from '~/jobs/components/job/graphql/queries/get_job.query.graphql'; +import { mockFullPath, mockId, mockJobResponse } from './mock_data'; -describe('Legacy Sidebar Header', () => { - let store; - let wrapper; +const localVue = createLocalVue(); +localVue.use(VueApollo); - const findCancelButton = () => wrapper.findByTestId('cancel-button'); - const findRetryButton = () => wrapper.findComponent(JobRetryButton); - const findEraseLink = () => wrapper.findByTestId('job-log-erase-link'); - - const createWrapper = (props) => { - store = createStore(); - - wrapper = extendedWrapper( - shallowMount(LegacySidebarHeader, { - propsData: { - job, - ...props, - }, - store, - }), - ); +const defaultProvide = { + projectPath: mockFullPath, +}; + +describe('Sidebar Header', () => { + let wrapper; + let mockApollo; + let getJobQueryResponse; + + const createComponent = ({ options = {}, props = {}, restJob = {} } = {}) => { + wrapper = shallowMountExtended(SidebarHeader, { + propsData: { + ...props, + jobId: mockId, + restJob, + }, + provide: { + ...defaultProvide, + }, + ...options, + }); }; - afterEach(() => { - wrapper.destroy(); - }); + const createComponentWithApollo = async ({ props = {}, restJob = {} } = {}) => { + const requestHandlers = [[getJobQuery, getJobQueryResponse]]; - describe('when job log is erasable', () => { - const path = '/root/ci-project/-/jobs/1447/erase'; + mockApollo = createMockApollo(requestHandlers); - beforeEach(() => { - createWrapper({ - erasePath: path, - }); - }); + const options = { + localVue, + apolloProvider: mockApollo, + }; - it('renders erase job link', () => { - expect(findEraseLink().exists()).toBe(true); + createComponent({ + props, + restJob, + options, }); - it('erase job link has correct path', () => { - expect(findEraseLink().attributes('href')).toBe(path); - }); - }); + return waitForPromises(); + }; - describe('when job log is not erasable', () => { - beforeEach(() => { - createWrapper(); - }); + const findCancelButton = () => wrapper.findByTestId('cancel-button'); + const findEraseButton = () => wrapper.findByTestId('job-log-erase-link'); + const findJobName = () => wrapper.findByTestId('job-name'); + const findRetryButton = () => wrapper.findComponent(JobRetryButton); - it('does not render erase button', () => { - expect(findEraseLink().exists()).toBe(false); - }); + beforeEach(async () => { + getJobQueryResponse = jest.fn(); }); - describe('when the job is retryable', () => { - beforeEach(() => { - createWrapper(); - }); + afterEach(() => { + wrapper.destroy(); + }); - it('should render the retry button', () => { - expect(findRetryButton().props('href')).toBe(job.retry_path); + describe('when rendering contents', () => { + beforeEach(async () => { + getJobQueryResponse.mockResolvedValue(mockJobResponse); }); - }); - describe('when there is no retry path', () => { - it('should not render a retry button', async () => { - const copy = { ...job, retry_path: null }; - createWrapper({ job: copy }); + it('renders the correct job name', async () => { + await createComponentWithApollo(); + expect(findJobName().text()).toBe(mockJobResponse.data.project.job.name); + }); + it('does not render buttons with no paths', async () => { + await createComponentWithApollo(); + expect(findCancelButton().exists()).toBe(false); + expect(findEraseButton().exists()).toBe(false); expect(findRetryButton().exists()).toBe(false); }); - }); - describe('when the job is cancelable', () => { - beforeEach(() => { - createWrapper(); + it('renders a retry button with a path', async () => { + await createComponentWithApollo({ restJob: { retry_path: 'retry/path' } }); + expect(findRetryButton().exists()).toBe(true); + }); + + it('renders a cancel button with a path', async () => { + await createComponentWithApollo({ restJob: { cancel_path: 'cancel/path' } }); + expect(findCancelButton().exists()).toBe(true); }); - it('should render link to cancel job', () => { - expect(findCancelButton().props('icon')).toBe('cancel'); - expect(findCancelButton().attributes('href')).toBe(job.cancel_path); + it('renders an erase button with a path', async () => { + await createComponentWithApollo({ restJob: { erase_path: 'erase/path' } }); + expect(findEraseButton().exists()).toBe(true); }); }); }); diff --git a/spec/frontend/lib/dompurify_spec.js b/spec/frontend/lib/dompurify_spec.js index 412408ce377..f767a673553 100644 --- a/spec/frontend/lib/dompurify_spec.js +++ b/spec/frontend/lib/dompurify_spec.js @@ -94,6 +94,11 @@ describe('~/lib/dompurify', () => { expect(sanitize('<link rel="stylesheet" href="styles.css">')).toBe(''); }); + it("doesn't allow form tags", () => { + expect(sanitize('<form>')).toBe(''); + expect(sanitize('<form method="post" action="path"></form>')).toBe(''); + }); + describe.each` type | gon ${'root'} | ${rootGon} diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index 78c0d0a2b11..a46f8c13f00 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -483,7 +483,18 @@ RSpec.describe DiffHelper do end describe '#conflicts' do - let(:merge_request) { instance_double(MergeRequest, cannot_be_merged?: true) } + let(:merge_request) do + instance_double( + MergeRequest, + cannot_be_merged?: cannot_be_merged?, + source_branch_exists?: source_branch_exists?, + target_branch_exists?: target_branch_exists? + ) + end + + let(:cannot_be_merged?) { true } + let(:source_branch_exists?) { true } + let(:target_branch_exists?) { true } let(:can_be_resolved_in_ui?) { true } let(:allow_tree_conflicts) { false } let(:files) { [instance_double(Gitlab::Conflict::File, path: 'a')] } @@ -508,7 +519,23 @@ RSpec.describe DiffHelper do end context 'when merge request can be merged' do - let(:merge_request) { instance_double(MergeRequest, cannot_be_merged?: false) } + let(:cannot_be_merged?) { false } + + it 'returns nil' do + expect(helper.conflicts).to be_nil + end + end + + context 'when source branch does not exist' do + let(:source_branch_exists?) { false } + + it 'returns nil' do + expect(helper.conflicts).to be_nil + end + end + + context 'when target branch does not exist' do + let(:target_branch_exists?) { false } it 'returns nil' do expect(helper.conflicts).to be_nil diff --git a/spec/helpers/x509_helper_spec.rb b/spec/helpers/x509_helper_spec.rb index 4e3e8c8d3f6..dfe9259bd0f 100644 --- a/spec/helpers/x509_helper_spec.rb +++ b/spec/helpers/x509_helper_spec.rb @@ -57,22 +57,4 @@ RSpec.describe X509Helper do end end end - - describe '#x509_signature?' do - let(:x509_signature) { create(:x509_commit_signature) } - let(:gpg_signature) { create(:gpg_signature) } - - it 'detects a x509 signed commit' do - signature = Gitlab::X509::Signature.new( - X509Helpers::User1.signed_commit_signature, - X509Helpers::User1.signed_commit_base_data, - X509Helpers::User1.certificate_email, - X509Helpers::User1.signed_commit_time - ) - - expect(x509_signature?(x509_signature)).to be_truthy - expect(x509_signature?(signature)).to be_truthy - expect(x509_signature?(gpg_signature)).to be_falsey - end - end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index a00df3a7dda..93b4d1bf105 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1352,7 +1352,7 @@ RSpec.describe Gitlab::Git::Repository do it "returns the number of commits in the whole repository" do options = { all: true } - expect(repository.count_commits(options)).to eq(314) + expect(repository.count_commits(options)).to eq(315) end end diff --git a/spec/lib/gitlab/x509/signature_spec.rb b/spec/lib/gitlab/x509/signature_spec.rb index 31f66232f38..32b22c0accd 100644 --- a/spec/lib/gitlab/x509/signature_spec.rb +++ b/spec/lib/gitlab/x509/signature_spec.rb @@ -11,6 +11,17 @@ RSpec.describe Gitlab::X509::Signature do } end + it_behaves_like 'signature with type checking', :x509 do + subject(:signature) do + described_class.new( + X509Helpers::User1.signed_commit_signature, + X509Helpers::User1.signed_commit_base_data, + X509Helpers::User1.certificate_email, + X509Helpers::User1.signed_commit_time + ) + end + end + shared_examples "a verified signature" do let!(:user) { create(:user, email: X509Helpers::User1.certificate_email) } diff --git a/spec/models/commit_signatures/gpg_signature_spec.rb b/spec/models/commit_signatures/gpg_signature_spec.rb index 1ffaaeba396..75cc5d448df 100644 --- a/spec/models/commit_signatures/gpg_signature_spec.rb +++ b/spec/models/commit_signatures/gpg_signature_spec.rb @@ -23,6 +23,7 @@ RSpec.describe CommitSignatures::GpgSignature do it_behaves_like 'having unique enum values' it_behaves_like 'commit signature' + it_behaves_like 'signature with type checking', :gpg describe 'associations' do it { is_expected.to belong_to(:gpg_key) } @@ -86,9 +87,9 @@ RSpec.describe CommitSignatures::GpgSignature do end end - describe '#user' do + describe '#signed_by_user' do it 'retrieves the gpg_key user' do - expect(signature.user).to eq(gpg_key.user) + expect(signature.signed_by_user).to eq(gpg_key.user) end end end diff --git a/spec/models/commit_signatures/ssh_signature_spec.rb b/spec/models/commit_signatures/ssh_signature_spec.rb index 08530bf6964..629d9c5ec53 100644 --- a/spec/models/commit_signatures/ssh_signature_spec.rb +++ b/spec/models/commit_signatures/ssh_signature_spec.rb @@ -22,6 +22,7 @@ RSpec.describe CommitSignatures::SshSignature do it_behaves_like 'having unique enum values' it_behaves_like 'commit signature' + it_behaves_like 'signature with type checking', :ssh describe 'associations' do it { is_expected.to belong_to(:key).optional } @@ -37,4 +38,10 @@ RSpec.describe CommitSignatures::SshSignature do ).to contain_exactly(signature, another_signature) end end + + describe '#signed_by_user' do + it 'returns the user associated with the SSH key' do + expect(signature.signed_by_user).to eq(ssh_key.user) + end + end end diff --git a/spec/models/commit_signatures/x509_commit_signature_spec.rb b/spec/models/commit_signatures/x509_commit_signature_spec.rb index b971fd078e2..cceb96ec70d 100644 --- a/spec/models/commit_signatures/x509_commit_signature_spec.rb +++ b/spec/models/commit_signatures/x509_commit_signature_spec.rb @@ -23,6 +23,7 @@ RSpec.describe CommitSignatures::X509CommitSignature do it_behaves_like 'having unique enum values' it_behaves_like 'commit signature' + it_behaves_like 'signature with type checking', :x509 describe 'validation' do it { is_expected.to validate_presence_of(:x509_certificate_id) } @@ -37,12 +38,12 @@ RSpec.describe CommitSignatures::X509CommitSignature do let!(:user) { create(:user, email: X509Helpers::User1.certificate_email) } it 'returns user' do - expect(described_class.safe_create!(attributes).user).to eq(user) + expect(described_class.safe_create!(attributes).signed_by_user).to eq(user) end end it 'if email is not assigned to a user, return nil' do - expect(described_class.safe_create!(attributes).user).to be_nil + expect(described_class.safe_create!(attributes).signed_by_user).to be_nil end end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index bab6247d4f9..4b5aabe745b 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -828,12 +828,14 @@ eos describe 'signed commits' do let(:gpg_signed_commit) { project.commit_by(oid: '0b4bc9a49b562e85de7cc9e834518ea6828729b9') } let(:x509_signed_commit) { project.commit_by(oid: '189a6c924013fc3fe40d6f1ec1dc20214183bc97') } + let(:ssh_signed_commit) { project.commit_by(oid: '7b5160f9bb23a3d58a0accdbe89da13b96b1ece9') } let(:unsigned_commit) { project.commit_by(oid: '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51') } let!(:commit) { create(:commit, project: project) } it 'returns signature_type properly' do expect(gpg_signed_commit.signature_type).to eq(:PGP) expect(x509_signed_commit.signature_type).to eq(:X509) + expect(ssh_signed_commit.signature_type).to eq(:SSH) expect(unsigned_commit.signature_type).to eq(:NONE) expect(commit.signature_type).to eq(:NONE) end @@ -841,9 +843,24 @@ eos it 'returns has_signature? properly' do expect(gpg_signed_commit.has_signature?).to be_truthy expect(x509_signed_commit.has_signature?).to be_truthy + expect(ssh_signed_commit.has_signature?).to be_truthy expect(unsigned_commit.has_signature?).to be_falsey expect(commit.has_signature?).to be_falsey end + + context 'when feature flag "ssh_commit_signatures" is disabled' do + before do + stub_feature_flags(ssh_commit_signatures: false) + end + + it 'reports no signature' do + expect(ssh_signed_commit).not_to have_signature + end + + it 'does not return signature data' do + expect(ssh_signed_commit.signature).to be_nil + end + end end describe '#has_been_reverted?' do diff --git a/spec/models/concerns/commit_signature_spec.rb b/spec/models/concerns/commit_signature_spec.rb new file mode 100644 index 00000000000..4bba5a6ee41 --- /dev/null +++ b/spec/models/concerns/commit_signature_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe CommitSignature do + describe '#signed_by_user' do + context 'when class does not define the signed_by_user method' do + subject(:implementation) do + Class.new(ActiveRecord::Base) do + self.table_name = 'ssh_signatures' + end.include(described_class).new + end + + it 'raises a NoMethodError with custom message' do + expect do + implementation.signed_by_user + end.to raise_error(NoMethodError, 'must implement `signed_by_user` method') + end + end + end +end diff --git a/spec/models/concerns/signature_type_spec.rb b/spec/models/concerns/signature_type_spec.rb new file mode 100644 index 00000000000..d8e2b617e0e --- /dev/null +++ b/spec/models/concerns/signature_type_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SignatureType do + describe '#type' do + context 'when class does not define a type method' do + subject(:implementation) { Class.new.include(described_class).new } + + it 'raises a NoMethodError with custom message' do + expect { implementation.type }.to raise_error(NoMethodError, 'must implement `type` method') + end + end + end +end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 8a08d5203fd..acb6c323e13 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -2206,7 +2206,7 @@ RSpec.describe API::Commits do end describe 'GET /projects/:id/repository/commits/:sha/signature' do - let!(:project) { create(:project, :repository, :public) } + let_it_be(:project) { create(:project, :repository, :public) } let(:project_id) { project.id } let(:commit_id) { project.repository.commit.id } let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}/signature" } @@ -2228,7 +2228,7 @@ RSpec.describe API::Commits do end context 'gpg signed commit' do - let(:commit) { project.repository.commit(GpgHelpers::SIGNED_COMMIT_SHA) } + let!(:commit) { project.commit(GpgHelpers::SIGNED_COMMIT_SHA) } let(:commit_id) { commit.id } it 'returns correct JSON' do @@ -2244,8 +2244,8 @@ RSpec.describe API::Commits do end context 'x509 signed commit' do - let(:commit) { project.repository.commit_by(oid: '189a6c924013fc3fe40d6f1ec1dc20214183bc97') } - let(:commit_id) { commit.id } + let(:commit_id) { '189a6c924013fc3fe40d6f1ec1dc20214183bc97' } + let!(:commit) { project.commit(commit_id) } it 'returns correct JSON' do get api(route, current_user) @@ -2276,5 +2276,59 @@ RSpec.describe API::Commits do end end end + + context 'with ssh signed commit' do + let(:commit_id) { '7b5160f9bb23a3d58a0accdbe89da13b96b1ece9' } + let!(:commit) { project.commit(commit_id) } + + context 'when key belonging to author does not exist' do + it 'returns data without key' do + get api(route, current_user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['signature_type']).to eq('SSH') + expect(json_response['verification_status']).to eq(commit.signature.verification_status) + expect(json_response['key']).to be_nil + expect(json_response['commit_source']).to eq('gitaly') + end + end + + context 'when key belonging to author exists' do + let(:user) { create(:user, email: commit.committer_email) } + let!(:key) { create(:key, user: user, key: extract_public_key_from_commit(commit), expires_at: 2.days.from_now) } + + def extract_public_key_from_commit(commit) + ssh_commit = Gitlab::Ssh::Commit.new(commit) + signature_data = ::SSHData::Signature.parse_pem(ssh_commit.signature_text) + signature_data.public_key.openssh + end + + it 'returns data including key' do + get api(route, current_user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['signature_type']).to eq('SSH') + expect(json_response['verification_status']).to eq(commit.signature.verification_status) + expect(json_response['key']['id']).to eq(key.id) + expect(json_response['key']['title']).to eq(key.title) + expect(json_response['key']['key']).to eq(key.publishable_key) + expect(Time.parse(json_response['key']['created_at'])).to be_like_time(key.created_at) + expect(Time.parse(json_response['key']['expires_at'])).to be_like_time(key.expires_at) + expect(json_response['commit_source']).to eq('gitaly') + end + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(ssh_commit_signatures: false) + end + + it 'returns 404' do + get api(route, current_user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end end end diff --git a/spec/rubocop/cop/filename_length_spec.rb b/spec/rubocop/cop/filename_length_spec.rb index 1ea368d282f..a5bdce9a339 100644 --- a/spec/rubocop/cop/filename_length_spec.rb +++ b/spec/rubocop/cop/filename_length_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'rubocop_spec_helper' -require 'rubocop/rspec/support' require_relative '../../../rubocop/cop/filename_length' RSpec.describe RuboCop::Cop::FilenameLength do diff --git a/spec/rubocop/cop/gitlab/feature_available_usage_spec.rb b/spec/rubocop/cop/gitlab/feature_available_usage_spec.rb index 30edd33a318..b15c298099d 100644 --- a/spec/rubocop/cop/gitlab/feature_available_usage_spec.rb +++ b/spec/rubocop/cop/gitlab/feature_available_usage_spec.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true require 'rubocop_spec_helper' -require 'rubocop' -require 'rubocop/rspec/support' require_relative '../../../../rubocop/cop/gitlab/feature_available_usage' RSpec.describe RuboCop::Cop::Gitlab::FeatureAvailableUsage do diff --git a/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb b/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb index 6e60889f737..bfc0cebe203 100644 --- a/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb +++ b/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true require 'rubocop_spec_helper' -require 'rubocop' -require 'rubocop/rspec/support' require_relative '../../../../rubocop/cop/gitlab/mark_used_feature_flags' RSpec.describe RuboCop::Cop::Gitlab::MarkUsedFeatureFlags do diff --git a/spec/rubocop/cop/user_admin_spec.rb b/spec/rubocop/cop/user_admin_spec.rb index 99e87d619c0..21bf027324b 100644 --- a/spec/rubocop/cop/user_admin_spec.rb +++ b/spec/rubocop/cop/user_admin_spec.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true require 'rubocop_spec_helper' - -require 'rubocop' require_relative '../../../rubocop/cop/user_admin' RSpec.describe RuboCop::Cop::UserAdmin do diff --git a/spec/rubocop/formatter/graceful_formatter_spec.rb b/spec/rubocop/formatter/graceful_formatter_spec.rb index 1ed8533ac16..d76e566e2b4 100644 --- a/spec/rubocop/formatter/graceful_formatter_spec.rb +++ b/spec/rubocop/formatter/graceful_formatter_spec.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true -require 'fast_spec_helper' +require 'rubocop_spec_helper' require 'rspec-parameterized' -require 'rubocop' -require 'rubocop/rspec/support' require 'stringio' require_relative '../../../rubocop/formatter/graceful_formatter' diff --git a/spec/rubocop/support_workaround.rb b/spec/rubocop/support_workaround.rb new file mode 100644 index 00000000000..d83aa8a7232 --- /dev/null +++ b/spec/rubocop/support_workaround.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +# This replicates `require 'rubocop/rspec/support'` to workaround the issue +# in https://gitlab.com/gitlab-org/gitlab/-/issues/382452. +# +# All helpers are only included in rubocop specs (type: :rubocop/:rubocop_rspec). + +require 'rubocop/rspec/cop_helper' +require 'rubocop/rspec/host_environment_simulation_helper' +require 'rubocop/rspec/shared_contexts' +require 'rubocop/rspec/expect_offense' +require 'rubocop/rspec/parallel_formatter' + +RSpec.configure do |config| + config.include CopHelper, type: :rubocop + config.include CopHelper, type: :rubocop_rspec + config.include HostEnvironmentSimulatorHelper, type: :rubocop + config.include HostEnvironmentSimulatorHelper, type: :rubocop_rspec + config.include_context 'config', :config + config.include_context 'isolated environment', :isolated_environment + config.include_context 'maintain registry', :restore_registry + config.include_context 'ruby 2.0', :ruby20 + config.include_context 'ruby 2.1', :ruby21 + config.include_context 'ruby 2.2', :ruby22 + config.include_context 'ruby 2.3', :ruby23 + config.include_context 'ruby 2.4', :ruby24 + config.include_context 'ruby 2.5', :ruby25 + config.include_context 'ruby 2.6', :ruby26 + config.include_context 'ruby 2.7', :ruby27 + config.include_context 'ruby 3.0', :ruby30 + config.include_context 'ruby 3.1', :ruby31 + config.include_context 'ruby 3.2', :ruby32 +end diff --git a/spec/rubocop_spec_helper.rb b/spec/rubocop_spec_helper.rb index 6c6e588d42f..9884cdd0272 100644 --- a/spec/rubocop_spec_helper.rb +++ b/spec/rubocop_spec_helper.rb @@ -6,9 +6,10 @@ require 'fast_spec_helper' # To prevent load order issues we need to require `rubocop` first. # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47008 require 'rubocop' -require 'rubocop/rspec/support' require 'rubocop/rspec/shared_contexts/default_rspec_language_config_context' +require_relative 'rubocop/support_workaround' + RSpec.configure do |config| config.define_derived_metadata(file_path: %r{spec/rubocop}) do |metadata| metadata[:type] = :rubocop diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index e1b461cf37e..6292cf83297 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -91,7 +91,8 @@ module TestEnv 'utf-16' => 'f05a987', 'gitaly-rename-test' => '94bb47c', 'smime-signed-commits' => 'ed775cc', - 'Ääh-test-utf-8' => '7975be0' + 'Ääh-test-utf-8' => '7975be0', + 'ssh-signed-commit' => '7b5160f' }.freeze # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily diff --git a/spec/support/shared_examples/models/concerns/signature_type_shared_examples.rb b/spec/support/shared_examples/models/concerns/signature_type_shared_examples.rb new file mode 100644 index 00000000000..728855b74f8 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/signature_type_shared_examples.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +METHODS = %i[ + gpg? + ssh? + x509? +].freeze + +RSpec.shared_examples 'signature with type checking' do |type| + describe 'signature type checkers' do + where(:method, :expected) do + METHODS.map do |method| + [method, method == "#{type}?".to_sym] + end + end + + with_them do + specify { expect(subject.public_send(method)).to eq(expected) } + end + end +end |