Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/features/callouts/registration_enabled_spec.rb2
-rw-r--r--spec/features/clusters/cluster_detail_page_spec.rb2
-rw-r--r--spec/features/clusters/cluster_health_dashboard_spec.rb3
-rw-r--r--spec/features/clusters/create_agent_spec.rb2
-rw-r--r--spec/features/commits/user_uses_quick_actions_spec.rb2
-rw-r--r--spec/features/commits/user_view_commits_spec.rb2
-rw-r--r--spec/features/projects/jobs_spec.rb4
-rw-r--r--spec/finders/branches_finder_spec.rb63
-rw-r--r--spec/finders/tags_finder_spec.rb13
-rw-r--r--spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap2
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js92
-rw-r--r--spec/frontend/jobs/components/job/empty_state_spec.js2
-rw-r--r--spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js1
-rw-r--r--spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js17
-rw-r--r--spec/frontend/jobs/components/job/manual_variables_form_spec.js231
-rw-r--r--spec/frontend/jobs/components/job/mock_data.js76
-rw-r--r--spec/frontend/jobs/components/job/sidebar_header_spec.js140
-rw-r--r--spec/frontend/lib/dompurify_spec.js5
-rw-r--r--spec/helpers/diff_helper_spec.rb31
-rw-r--r--spec/helpers/x509_helper_spec.rb18
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb2
-rw-r--r--spec/lib/gitlab/x509/signature_spec.rb11
-rw-r--r--spec/models/commit_signatures/gpg_signature_spec.rb5
-rw-r--r--spec/models/commit_signatures/ssh_signature_spec.rb7
-rw-r--r--spec/models/commit_signatures/x509_commit_signature_spec.rb5
-rw-r--r--spec/models/commit_spec.rb17
-rw-r--r--spec/models/concerns/commit_signature_spec.rb21
-rw-r--r--spec/models/concerns/signature_type_spec.rb15
-rw-r--r--spec/requests/api/commits_spec.rb62
-rw-r--r--spec/rubocop/cop/filename_length_spec.rb1
-rw-r--r--spec/rubocop/cop/gitlab/feature_available_usage_spec.rb2
-rw-r--r--spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb2
-rw-r--r--spec/rubocop/cop/user_admin_spec.rb2
-rw-r--r--spec/rubocop/formatter/graceful_formatter_spec.rb4
-rw-r--r--spec/rubocop/support_workaround.rb33
-rw-r--r--spec/rubocop_spec_helper.rb3
-rw-r--r--spec/support/helpers/test_env.rb3
-rw-r--r--spec/support/shared_examples/models/concerns/signature_type_shared_examples.rb21
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