diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-11 12:10:29 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-11 12:10:29 +0300 |
commit | 871b886a1794e5baefd6b2f96caf2ac4ce5da6ca (patch) | |
tree | a92b04af5c5704314c31981ac4bb92c1489c46fd /spec | |
parent | c0496e1078f8612b0c468430a79bd03c8102e2b9 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
40 files changed, 780 insertions, 1873 deletions
diff --git a/spec/config/settings_spec.rb b/spec/config/settings_spec.rb index 55e675d5107..4639e533922 100644 --- a/spec/config/settings_spec.rb +++ b/spec/config/settings_spec.rb @@ -170,12 +170,12 @@ RSpec.describe Settings, feature_category: :system_access do it 'defaults to using the encrypted_settings_key_base for the key' do expect(Gitlab::EncryptedConfiguration).to receive(:new).with(hash_including(base_key: Gitlab::Application.secrets.encrypted_settings_key_base)) - Settings.encrypted('tmp/tests/test.enc') + described_class.encrypted('tmp/tests/test.enc') end it 'returns empty encrypted config when a key has not been set' do allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(nil) - expect(Settings.encrypted('tmp/tests/test.enc').read).to be_empty + expect(described_class.encrypted('tmp/tests/test.enc').read).to be_empty end end diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb index 537424093fb..60343c822af 100644 --- a/spec/controllers/admin/application_settings_controller_spec.rb +++ b/spec/controllers/admin/application_settings_controller_spec.rb @@ -487,6 +487,43 @@ RSpec.describe Admin::ApplicationSettingsController, :do_not_mock_admin_mode_set end end + describe 'GET #slack_app_manifest_download', feature_category: :integrations do + before do + sign_in(admin) + end + + subject { get :slack_app_manifest_download } + + it 'downloads the GitLab for Slack app manifest' do + allow(Slack::Manifest).to receive(:to_h).and_return({ foo: 'bar' }) + + subject + + expect(response.body).to eq('{"foo":"bar"}') + expect(response.headers['Content-Disposition']).to eq( + 'attachment; filename="slack_manifest.json"; filename*=UTF-8\'\'slack_manifest.json' + ) + end + end + + describe 'GET #slack_app_manifest_share', feature_category: :integrations do + before do + sign_in(admin) + end + + subject { get :slack_app_manifest_share } + + it 'redirects the user to the Slack Manifest share URL' do + allow(Slack::Manifest).to receive(:to_h).and_return({ foo: 'bar' }) + + subject + + expect(response).to redirect_to( + "https://api.slack.com/apps?new_app=1&manifest_json=%7B%22foo%22%3A%22bar%22%7D" + ) + end + end + describe 'GET #service_usage_data', feature_category: :service_ping do before do stub_usage_data_connections diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb index 276bd9b65b9..88af7d1fe45 100644 --- a/spec/controllers/repositories/git_http_controller_spec.rb +++ b/spec/controllers/repositories/git_http_controller_spec.rb @@ -79,7 +79,7 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m end context 'when repository container is a project' do - it_behaves_like Repositories::GitHttpController do + it_behaves_like described_class do let(:container) { project } let(:user) { project.first_owner } let(:access_checker_class) { Gitlab::GitAccess } @@ -133,7 +133,7 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m end context 'when the user is a deploy token' do - it_behaves_like Repositories::GitHttpController do + it_behaves_like described_class do let(:container) { project } let(:user) { create(:deploy_token, :project, projects: [project]) } let(:access_checker_class) { Gitlab::GitAccess } @@ -144,7 +144,7 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m end context 'when repository container is a project wiki' do - it_behaves_like Repositories::GitHttpController do + it_behaves_like described_class do let(:container) { create(:project_wiki, :empty_repo, project: project) } let(:user) { project.first_owner } let(:access_checker_class) { Gitlab::GitAccessWiki } @@ -155,7 +155,7 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m end context 'when repository container is a personal snippet' do - it_behaves_like Repositories::GitHttpController do + it_behaves_like described_class do let(:container) { personal_snippet } let(:user) { personal_snippet.author } let(:access_checker_class) { Gitlab::GitAccessSnippet } @@ -167,7 +167,7 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m end context 'when repository container is a project snippet' do - it_behaves_like Repositories::GitHttpController do + it_behaves_like described_class do let(:container) { project_snippet } let(:user) { project_snippet.author } let(:access_checker_class) { Gitlab::GitAccessSnippet } diff --git a/spec/experiments/application_experiment_spec.rb b/spec/experiments/application_experiment_spec.rb index ef8f8cbce3b..461a6390a33 100644 --- a/spec/experiments/application_experiment_spec.rb +++ b/spec/experiments/application_experiment_spec.rb @@ -36,7 +36,7 @@ RSpec.describe ApplicationExperiment, :experiment, feature_category: :experiment # _published_experiments.html.haml partial. application_experiment.publish - expect(ApplicationExperiment.published_experiments['namespaced/stub']).to include( + expect(described_class.published_experiments['namespaced/stub']).to include( experiment: 'namespaced/stub', excluded: false, key: anything, diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index 411ea8ea12e..55d3ad3aca3 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -8,11 +8,9 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do include UsageDataHelpers let_it_be(:admin) { create(:admin) } - let(:dot_com?) { false } context 'application setting :admin_mode is enabled', :request_store do before do - allow(Gitlab).to receive(:com?).and_return(dot_com?) stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') sign_in(admin) gitlab_enable_admin_mode_sign_in(admin) @@ -147,9 +145,7 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do end context 'Dormant users', feature_category: :user_management do - context 'when Gitlab.com' do - let(:dot_com?) { true } - + context 'when Gitlab.com', :saas do it 'does not expose the setting section' do # NOTE: not_to have_content may have false positives for content # that might not load instantly, so before checking that @@ -163,8 +159,6 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do end context 'when not Gitlab.com' do - let(:dot_com?) { false } - it 'exposes the setting section' do expect(page).to have_content('Dormant users') expect(page).to have_field('Deactivate dormant users after a period of inactivity') @@ -366,9 +360,46 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do end context 'GitLab for Slack app settings', feature_category: :integrations do + let(:create_heading) { 'Create your GitLab for Slack app' } + let(:configure_heading) { 'Configure the app settings' } + let(:update_heading) { 'Update your Slack app' } + + it 'has all sections' do + page.within('.as-slack') do + expect(page).to have_content(create_heading) + expect(page).to have_content(configure_heading) + expect(page).to have_content(update_heading) + end + end + + context 'when GitLab.com', :saas do + it 'only has the configure section' do + page.within('.as-slack') do + expect(page).to have_content(configure_heading) + + expect(page).not_to have_content(create_heading) + expect(page).not_to have_content(update_heading) + end + end + end + + context 'when the `slack_app_self_managed` flag is disabled' do + before do + stub_feature_flags(slack_app_self_managed: false) + visit general_admin_application_settings_path + end + + it 'does not display any sections' do + expect(page).not_to have_selector('.as-slack') + expect(page).not_to have_content(configure_heading) + expect(page).not_to have_content(create_heading) + expect(page).not_to have_content(update_heading) + end + end + it 'changes the settings' do page.within('.as-slack') do - check 'Enable Slack application' + check 'Enable GitLab for Slack app' fill_in 'Client ID', with: 'slack_app_id' fill_in 'Client secret', with: 'slack_app_secret' fill_in 'Signing secret', with: 'slack_app_signing_secret' diff --git a/spec/fixtures/api/schemas/slack/manifest.json b/spec/fixtures/api/schemas/slack/manifest.json new file mode 100644 index 00000000000..236c5df46bc --- /dev/null +++ b/spec/fixtures/api/schemas/slack/manifest.json @@ -0,0 +1,164 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "display_information": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 35 + }, + "description": { + "type": "string", + "maxLength": 140 + }, + "background_color": { + "type": "string", + "pattern": "^#[0-9A-F]{6}$" + }, + "long_description": { + "type": "string", + "maxLength": 4000 + } + }, + "required": [ + "name" + ] + }, + "features": { + "type": "object", + "properties": { + "app_home": { + "type": "object", + "properties": { + "home_tab_enabled": { + "type": "boolean" + }, + "messages_tab_enabled": { + "type": "boolean" + }, + "messages_tab_read_only_enabled": { + "type": "boolean" + } + } + }, + "bot_user": { + "type": "object", + "properties": { + "display_name": { + "type": "string", + "maxLength": 80 + }, + "always_online": { + "type": "boolean" + } + } + }, + "slash_commands": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "command": { + "type": "string", + "maxLength": 32 + }, + "url": { + "type": "string" + }, + "description": { + "type": "string", + "maxLength": 2000 + }, + "usage_hint": { + "type": "string", + "maxLength": 1000 + }, + "should_escape": { + "type": "boolean" + } + }, + "required": [ + "command", + "description" + ] + } + ] + } + } + }, + "oauth_config": { + "type": "object", + "properties": { + "redirect_urls": { + "type": "array", + "maxContains": 1000, + "items": [ + { + "type": "string" + } + ] + }, + "scopes": { + "type": "object", + "properties": { + "bot": { + "type": "array", + "maxContains": 255 + } + } + } + } + }, + "settings": { + "type": "object", + "properties": { + "event_subscriptions": { + "type": "object", + "properties": { + "request_url": { + "type": "string" + }, + "bot_events": { + "type": "array", + "maxContains": 100, + "items": [ + { + "type": "string" + } + ] + } + } + }, + "interactivity": { + "type": "object", + "properties": { + "is_enabled": { + "type": "boolean" + }, + "request_url": { + "type": "string" + }, + "message_menu_options_url": { + "type": "string" + } + } + }, + "org_deploy_enabled": { + "type": "boolean" + }, + "socket_mode_enabled": { + "type": "boolean" + }, + "token_rotation_enabled": { + "type": "boolean" + } + } + } + }, + "required": [ + "display_information" + ] +} diff --git a/spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap b/spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap deleted file mode 100644 index 311a67a3e31..00000000000 --- a/spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap +++ /dev/null @@ -1,26 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Grouped Issues List renders a smart virtual list with the correct props 1`] = ` -Object { - "length": 4, - "remain": 20, - "rtag": "div", - "size": 32, - "wclass": "report-block-list", - "wtag": "ul", -} -`; - -exports[`Grouped Issues List with data renders a report item with the correct props 1`] = ` -Object { - "component": "CodequalityIssueBody", - "iconComponent": "IssueStatusIcon", - "isNew": false, - "issue": Object { - "name": "foo", - }, - "showReportSectionStatusIcon": false, - "status": "none", - "statusIconSize": 24, -} -`; diff --git a/spec/frontend/ci/reports/components/grouped_issues_list_spec.js b/spec/frontend/ci/reports/components/grouped_issues_list_spec.js deleted file mode 100644 index 8beec220802..00000000000 --- a/spec/frontend/ci/reports/components/grouped_issues_list_spec.js +++ /dev/null @@ -1,83 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import GroupedIssuesList from '~/ci/reports/components/grouped_issues_list.vue'; -import ReportItem from '~/ci/reports/components/report_item.vue'; -import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue'; - -describe('Grouped Issues List', () => { - let wrapper; - - const createComponent = ({ propsData = {}, stubs = {} } = {}) => { - wrapper = shallowMount(GroupedIssuesList, { - propsData, - stubs, - }); - }; - - const findHeading = (groupName) => wrapper.find(`[data-testid="${groupName}Heading"`); - - it('renders a smart virtual list with the correct props', () => { - createComponent({ - propsData: { - resolvedIssues: [{ name: 'foo' }], - unresolvedIssues: [{ name: 'bar' }], - }, - stubs: { - SmartVirtualList, - }, - }); - - expect(wrapper.findComponent(SmartVirtualList).props()).toMatchSnapshot(); - }); - - describe('without data', () => { - beforeEach(() => { - createComponent(); - }); - - it.each(['unresolved', 'resolved'])('does not a render a header for %s issues', (issueName) => { - expect(findHeading(issueName).exists()).toBe(false); - }); - - it.each(['resolved', 'unresolved'])('does not render report items for %s issues', () => { - expect(wrapper.findComponent(ReportItem).exists()).toBe(false); - }); - }); - - describe('with data', () => { - it.each` - givenIssues | givenHeading | groupName - ${[{ name: 'foo issue' }]} | ${'Foo Heading'} | ${'resolved'} - ${[{ name: 'bar issue' }]} | ${'Bar Heading'} | ${'unresolved'} - `('renders the heading for $groupName issues', ({ givenIssues, givenHeading, groupName }) => { - createComponent({ - propsData: { [`${groupName}Issues`]: givenIssues, [`${groupName}Heading`]: givenHeading }, - }); - - expect(findHeading(groupName).text()).toBe(givenHeading); - }); - - it.each(['resolved', 'unresolved'])('renders all %s issues', (issueName) => { - const issues = [{ name: 'foo' }, { name: 'bar' }]; - - createComponent({ - propsData: { [`${issueName}Issues`]: issues }, - }); - - expect(wrapper.findAllComponents(ReportItem)).toHaveLength(issues.length); - }); - - it('renders a report item with the correct props', () => { - createComponent({ - propsData: { - resolvedIssues: [{ name: 'foo' }], - component: 'CodequalityIssueBody', - }, - stubs: { - ReportItem, - }, - }); - - expect(wrapper.findComponent(ReportItem).props()).toMatchSnapshot(); - }); - }); -}); diff --git a/spec/frontend/ci/reports/components/summary_row_spec.js b/spec/frontend/ci/reports/components/summary_row_spec.js deleted file mode 100644 index d4d2ac29fb1..00000000000 --- a/spec/frontend/ci/reports/components/summary_row_spec.js +++ /dev/null @@ -1,63 +0,0 @@ -import { mount } from '@vue/test-utils'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import HelpPopover from '~/vue_shared/components/help_popover.vue'; -import SummaryRow from '~/ci/reports/components/summary_row.vue'; - -describe('Summary row', () => { - let wrapper; - - const summary = 'SAST detected 1 new vulnerability and 1 fixed vulnerability'; - const popoverOptions = { - title: 'Static Application Security Testing (SAST)', - content: '<a>Learn more about SAST</a>', - }; - const statusIcon = 'warning'; - - const createComponent = ({ props = {}, slots = {} } = {}) => { - wrapper = extendedWrapper( - mount(SummaryRow, { - propsData: { - summary, - popoverOptions, - statusIcon, - ...props, - }, - slots, - }), - ); - }; - - const findSummary = () => wrapper.findByTestId('summary-row-description'); - const findStatusIcon = () => wrapper.findByTestId('summary-row-icon'); - const findHelpPopover = () => wrapper.findComponent(HelpPopover); - - it('renders provided summary', () => { - createComponent(); - expect(findSummary().text()).toContain(summary); - }); - - it('renders provided icon', () => { - createComponent(); - expect(findStatusIcon().find('[data-testid="status_warning-icon"]').exists()).toBe(true); - }); - - it('renders help popover if popoverOptions are provided', () => { - createComponent(); - expect(findHelpPopover().props('options')).toEqual(popoverOptions); - }); - - it('does not render help popover if popoverOptions are not provided', () => { - createComponent({ props: { popoverOptions: null } }); - expect(findHelpPopover().exists()).toBe(false); - }); - - describe('summary slot', () => { - it('replaces the summary prop', () => { - const summarySlotContent = 'Summary slot content'; - createComponent({ slots: { summary: summarySlotContent } }); - - expect(wrapper.text()).not.toContain(summary); - expect(findSummary().text()).toContain(summarySlotContent); - }); - }); -}); diff --git a/spec/frontend/fixtures/timezones.rb b/spec/frontend/fixtures/timezones.rb index 2393f4e797d..f04e647c8eb 100644 --- a/spec/frontend/fixtures/timezones.rb +++ b/spec/frontend/fixtures/timezones.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe TimeZoneHelper, '(JavaScript fixtures)' do include JavaScriptFixturesHelpers - include TimeZoneHelper + include described_class let(:response) { @timezones.sort_by! { |tz| tz[:name] }.to_json } diff --git a/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap b/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap deleted file mode 100644 index 66d27b5d605..00000000000 --- a/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap +++ /dev/null @@ -1,144 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SecuritySummary component given the message {"countMessage": "%{criticalStart}0 Critical%{criticalEnd} %{highStart}1 High%{highEnd} and %{otherStart}0 Others%{otherEnd}", "critical": 0, "high": 1, "message": "Security scanning detected %{totalStart}1%{totalEnd} potential vulnerability", "other": 0, "status": "", "total": 1} interpolates correctly 1`] = ` -<span> - Security scanning detected - <strong> - 1 - </strong> - potential vulnerability - <span - class="gl-font-sm" - > - <span> - <span - class="gl-pl-4" - > - - 0 Critical - - </span> - </span> - - <span> - <strong - class="gl-text-red-600 gl-px-2" - > - - 1 High - - </strong> - </span> - and - <span> - <span - class="gl-px-2" - > - - 0 Others - - </span> - </span> - </span> -</span> -`; - -exports[`SecuritySummary component given the message {"countMessage": "%{criticalStart}1 Critical%{criticalEnd} %{highStart}0 High%{highEnd} and %{otherStart}0 Others%{otherEnd}", "critical": 1, "high": 0, "message": "Security scanning detected %{totalStart}1%{totalEnd} potential vulnerability", "other": 0, "status": "", "total": 1} interpolates correctly 1`] = ` -<span> - Security scanning detected - <strong> - 1 - </strong> - potential vulnerability - <span - class="gl-font-sm" - > - <span> - <strong - class="gl-text-red-800 gl-pl-4" - > - - 1 Critical - - </strong> - </span> - - <span> - <span - class="gl-px-2" - > - - 0 High - - </span> - </span> - and - <span> - <span - class="gl-px-2" - > - - 0 Others - - </span> - </span> - </span> -</span> -`; - -exports[`SecuritySummary component given the message {"countMessage": "%{criticalStart}1 Critical%{criticalEnd} %{highStart}2 High%{highEnd} and %{otherStart}0 Others%{otherEnd}", "critical": 1, "high": 2, "message": "Security scanning detected %{totalStart}3%{totalEnd} potential vulnerabilities", "other": 0, "status": "", "total": 3} interpolates correctly 1`] = ` -<span> - Security scanning detected - <strong> - 3 - </strong> - potential vulnerabilities - <span - class="gl-font-sm" - > - <span> - <strong - class="gl-text-red-800 gl-pl-4" - > - - 1 Critical - - </strong> - </span> - - <span> - <strong - class="gl-text-red-600 gl-px-2" - > - - 2 High - - </strong> - </span> - and - <span> - <span - class="gl-px-2" - > - - 0 Others - - </span> - </span> - </span> -</span> -`; - -exports[`SecuritySummary component given the message {"message": ""} interpolates correctly 1`] = ` -<span> - - <!----> -</span> -`; - -exports[`SecuritySummary component given the message {"message": "foo"} interpolates correctly 1`] = ` -<span> - foo - <!----> -</span> -`; diff --git a/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js b/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js deleted file mode 100644 index 6eebd129beb..00000000000 --- a/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js +++ /dev/null @@ -1,104 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import { - expectedDownloadDropdownPropsWithTitle, - securityReportMergeRequestDownloadPathsQueryResponse, -} from 'jest/vue_shared/security_reports/mock_data'; -import { createAlert } from '~/alert'; -import Component from '~/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue'; -import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue'; -import { - REPORT_TYPE_SAST, - REPORT_TYPE_SECRET_DETECTION, -} from '~/vue_shared/security_reports/constants'; -import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql'; - -jest.mock('~/alert'); - -describe('Merge request artifact Download', () => { - let wrapper; - - const defaultProps = { - reportTypes: [REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION], - targetProjectFullPath: '/path', - mrIid: 123, - }; - - const createWrapper = ({ propsData, options }) => { - wrapper = shallowMount(Component, { - stubs: { - SecurityReportDownloadDropdown, - }, - propsData: { - ...defaultProps, - ...propsData, - }, - ...options, - }); - }; - - const pendingHandler = () => new Promise(() => {}); - const successHandler = () => - Promise.resolve({ data: securityReportMergeRequestDownloadPathsQueryResponse }); - const failureHandler = () => Promise.resolve({ errors: [{ message: 'some error' }] }); - const createMockApolloProvider = (handler) => { - Vue.use(VueApollo); - const requestHandlers = [[securityReportMergeRequestDownloadPathsQuery, handler]]; - - return createMockApollo(requestHandlers); - }; - - const findDownloadDropdown = () => wrapper.findComponent(SecurityReportDownloadDropdown); - - describe('given the query is loading', () => { - beforeEach(() => { - createWrapper({ - options: { - apolloProvider: createMockApolloProvider(pendingHandler), - }, - }); - }); - - it('loading is true', () => { - expect(findDownloadDropdown().props('loading')).toBe(true); - }); - }); - - describe('given the query loads successfully', () => { - beforeEach(() => { - createWrapper({ - options: { - apolloProvider: createMockApolloProvider(successHandler), - }, - }); - }); - - it('renders the download dropdown', () => { - expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithTitle); - }); - }); - - describe('given the query fails', () => { - beforeEach(() => { - createWrapper({ - options: { - apolloProvider: createMockApolloProvider(failureHandler), - }, - }); - }); - - it('calls createAlert correctly', () => { - expect(createAlert).toHaveBeenCalledWith({ - message: Component.i18n.apiError, - captureError: true, - error: expect.any(Error), - }); - }); - - it('renders nothing', () => { - expect(findDownloadDropdown().props('artifacts')).toEqual([]); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js b/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js deleted file mode 100644 index 2f6e633fb34..00000000000 --- a/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js +++ /dev/null @@ -1,63 +0,0 @@ -import { GlLink, GlPopover } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import HelpIcon from '~/vue_shared/security_reports/components/help_icon.vue'; - -const helpPath = '/docs'; -const discoverProjectSecurityPath = '/discoverProjectSecurityPath'; - -describe('HelpIcon component', () => { - let wrapper; - - const createWrapper = (props) => { - wrapper = shallowMount(HelpIcon, { - propsData: { - helpPath, - ...props, - }, - }); - }; - - const findLink = () => wrapper.findComponent(GlLink); - const findPopover = () => wrapper.findComponent(GlPopover); - const findPopoverTarget = () => wrapper.findComponent({ ref: 'discoverProjectSecurity' }); - - describe('given a help path only', () => { - beforeEach(() => { - createWrapper(); - }); - - it('does not render a popover', () => { - expect(findPopover().exists()).toBe(false); - }); - - it('renders a help link', () => { - expect(findLink().attributes()).toMatchObject({ - href: helpPath, - target: '_blank', - }); - }); - }); - - describe('given a help path and discover project security path', () => { - beforeEach(() => { - createWrapper({ discoverProjectSecurityPath }); - }); - - it('renders a popover', () => { - const popover = findPopover(); - expect(popover.props('target')()).toBe(findPopoverTarget().element); - expect(popover.attributes()).toMatchObject({ - title: HelpIcon.i18n.upgradeToManageVulnerabilities, - triggers: 'click blur', - }); - expect(popover.text()).toContain(HelpIcon.i18n.upgradeToInteract); - }); - - it('renders a link to the discover path', () => { - expect(findLink().attributes()).toMatchObject({ - href: discoverProjectSecurityPath, - target: '_blank', - }); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/security_reports/security_summary_spec.js b/spec/frontend/vue_shared/components/security_reports/security_summary_spec.js deleted file mode 100644 index 61cdc329220..00000000000 --- a/spec/frontend/vue_shared/components/security_reports/security_summary_spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import { GlSprintf } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import SecuritySummary from '~/vue_shared/security_reports/components/security_summary.vue'; -import { groupedTextBuilder } from '~/vue_shared/security_reports/store/utils'; - -describe('SecuritySummary component', () => { - let wrapper; - - const createWrapper = (message) => { - wrapper = shallowMount(SecuritySummary, { - propsData: { message }, - stubs: { - GlSprintf, - }, - }); - }; - - describe.each([ - { message: '' }, - { message: 'foo' }, - groupedTextBuilder({ reportType: 'Security scanning', critical: 1, high: 0, total: 1 }), - groupedTextBuilder({ reportType: 'Security scanning', critical: 0, high: 1, total: 1 }), - groupedTextBuilder({ reportType: 'Security scanning', critical: 1, high: 2, total: 3 }), - ])('given the message %p', (message) => { - beforeEach(() => { - createWrapper(message); - }); - - it('interpolates correctly', () => { - expect(wrapper.element).toMatchSnapshot(); - }); - }); -}); diff --git a/spec/frontend/vue_shared/security_reports/mock_data.js b/spec/frontend/vue_shared/security_reports/mock_data.js index a9ad675e538..533d312a4de 100644 --- a/spec/frontend/vue_shared/security_reports/mock_data.js +++ b/spec/frontend/vue_shared/security_reports/mock_data.js @@ -341,120 +341,6 @@ export const securityReportMergeRequestDownloadPathsQueryNoArtifactsResponse = { }, }; -export const securityReportMergeRequestDownloadPathsQueryResponse = { - project: { - id: '1', - mergeRequest: { - id: 'mr-1', - headPipeline: { - id: 'gid://gitlab/Ci::Pipeline/176', - jobs: { - nodes: [ - { - id: 'job-1', - name: 'secret_detection', - artifacts: { - nodes: [ - { - downloadPath: - '/gitlab-org/secrets-detection-test/-/jobs/1399/artifacts/download?file_type=trace', - fileType: 'TRACE', - __typename: 'CiJobArtifact', - }, - { - downloadPath: - '/gitlab-org/secrets-detection-test/-/jobs/1399/artifacts/download?file_type=secret_detection', - fileType: 'SECRET_DETECTION', - __typename: 'CiJobArtifact', - }, - ], - __typename: 'CiJobArtifactConnection', - }, - __typename: 'CiJob', - }, - { - id: 'job-2', - name: 'bandit-sast', - artifacts: { - nodes: [ - { - downloadPath: - '/gitlab-org/secrets-detection-test/-/jobs/1400/artifacts/download?file_type=trace', - fileType: 'TRACE', - __typename: 'CiJobArtifact', - }, - { - downloadPath: - '/gitlab-org/secrets-detection-test/-/jobs/1400/artifacts/download?file_type=sast', - fileType: 'SAST', - __typename: 'CiJobArtifact', - }, - ], - __typename: 'CiJobArtifactConnection', - }, - __typename: 'CiJob', - }, - { - id: 'job-3', - name: 'eslint-sast', - artifacts: { - nodes: [ - { - downloadPath: - '/gitlab-org/secrets-detection-test/-/jobs/1401/artifacts/download?file_type=trace', - fileType: 'TRACE', - __typename: 'CiJobArtifact', - }, - { - downloadPath: - '/gitlab-org/secrets-detection-test/-/jobs/1401/artifacts/download?file_type=sast', - fileType: 'SAST', - __typename: 'CiJobArtifact', - }, - ], - __typename: 'CiJobArtifactConnection', - }, - __typename: 'CiJob', - }, - { - id: 'job-4', - name: 'all_artifacts', - artifacts: { - nodes: [ - { - downloadPath: - '/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=archive', - fileType: 'ARCHIVE', - __typename: 'CiJobArtifact', - }, - { - downloadPath: - '/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=trace', - fileType: 'TRACE', - __typename: 'CiJobArtifact', - }, - { - downloadPath: - '/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=metadata', - fileType: 'METADATA', - __typename: 'CiJobArtifact', - }, - ], - __typename: 'CiJobArtifactConnection', - }, - __typename: 'CiJob', - }, - ], - __typename: 'CiJobConnection', - }, - __typename: 'Pipeline', - }, - __typename: 'MergeRequest', - }, - __typename: 'Project', - }, -}; - export const securityReportPipelineDownloadPathsQueryResponse = { project: { id: 'project-1', @@ -566,9 +452,6 @@ export const securityReportPipelineDownloadPathsQueryResponse = { __typename: 'Project', }; -/** - * These correspond to SAST jobs in the securityReportMergeRequestDownloadPathsQueryResponse above. - */ export const sastArtifacts = [ { name: 'bandit-sast', @@ -582,9 +465,6 @@ export const sastArtifacts = [ }, ]; -/** - * These correspond to Secret Detection jobs in the securityReportMergeRequestDownloadPathsQueryResponse above. - */ export const secretDetectionArtifacts = [ { name: 'secret_detection', @@ -594,13 +474,6 @@ export const secretDetectionArtifacts = [ }, ]; -export const expectedDownloadDropdownPropsWithTitle = { - loading: false, - artifacts: [...secretDetectionArtifacts, ...sastArtifacts], - text: '', - title: 'Download results', -}; - export const expectedDownloadDropdownPropsWithText = { loading: false, artifacts: [...secretDetectionArtifacts, ...sastArtifacts], @@ -608,9 +481,6 @@ export const expectedDownloadDropdownPropsWithText = { text: 'Download results', }; -/** - * These correspond to any jobs with zip archives in the securityReportMergeRequestDownloadPathsQueryResponse above. - */ export const archiveArtifacts = [ { name: 'all_artifacts Archive', @@ -619,9 +489,6 @@ export const archiveArtifacts = [ }, ]; -/** - * These correspond to any jobs with trace data in the securityReportMergeRequestDownloadPathsQueryResponse above. - */ export const traceArtifacts = [ { name: 'secret_detection Trace', @@ -645,9 +512,6 @@ export const traceArtifacts = [ }, ]; -/** - * These correspond to any jobs with metadata data in the securityReportMergeRequestDownloadPathsQueryResponse above. - */ export const metadataArtifacts = [ { name: 'all_artifacts Metadata', diff --git a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js deleted file mode 100644 index 257f59612e8..00000000000 --- a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js +++ /dev/null @@ -1,267 +0,0 @@ -import { mount } from '@vue/test-utils'; -import MockAdapter from 'axios-mock-adapter'; -import { merge } from 'lodash'; -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import Vuex from 'vuex'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import { trimText } from 'helpers/text_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import { - expectedDownloadDropdownPropsWithText, - securityReportMergeRequestDownloadPathsQueryNoArtifactsResponse, - securityReportMergeRequestDownloadPathsQueryResponse, - sastDiffSuccessMock, - secretDetectionDiffSuccessMock, -} from 'jest/vue_shared/security_reports/mock_data'; -import { createAlert } from '~/alert'; -import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; -import HelpIcon from '~/vue_shared/security_reports/components/help_icon.vue'; -import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue'; -import { - REPORT_TYPE_SAST, - REPORT_TYPE_SECRET_DETECTION, -} from '~/vue_shared/security_reports/constants'; -import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql'; -import SecurityReportsApp from '~/vue_shared/security_reports/security_reports_app.vue'; - -jest.mock('~/alert'); - -Vue.use(VueApollo); -Vue.use(Vuex); - -const SAST_COMPARISON_PATH = '/sast.json'; -const SECRET_DETECTION_COMPARISON_PATH = '/secret_detection.json'; - -describe('Security reports app', () => { - let wrapper; - - const props = { - pipelineId: 123, - projectId: 456, - securityReportsDocsPath: '/docs', - discoverProjectSecurityPath: '/discoverProjectSecurityPath', - }; - - const createComponent = (options) => { - wrapper = mount( - SecurityReportsApp, - merge( - { - propsData: { ...props }, - stubs: { - HelpIcon: true, - }, - }, - options, - ), - ); - }; - - const pendingHandler = () => new Promise(() => {}); - const successHandler = () => - Promise.resolve({ data: securityReportMergeRequestDownloadPathsQueryResponse }); - const successEmptyHandler = () => - Promise.resolve({ data: securityReportMergeRequestDownloadPathsQueryNoArtifactsResponse }); - const failureHandler = () => Promise.resolve({ errors: [{ message: 'some error' }] }); - const createMockApolloProvider = (handler) => { - const requestHandlers = [[securityReportMergeRequestDownloadPathsQuery, handler]]; - - return createMockApollo(requestHandlers); - }; - - const findDownloadDropdown = () => wrapper.findComponent(SecurityReportDownloadDropdown); - const findHelpIconComponent = () => wrapper.findComponent(HelpIcon); - - describe('given the artifacts query is loading', () => { - beforeEach(() => { - createComponent({ - apolloProvider: createMockApolloProvider(pendingHandler), - }); - }); - - // TODO: Remove this assertion as part of - // https://gitlab.com/gitlab-org/gitlab/-/issues/273431 - it('initially renders nothing', () => { - expect(wrapper.html()).toBe(''); - }); - }); - - describe('given the artifacts query loads successfully', () => { - beforeEach(() => { - createComponent({ - apolloProvider: createMockApolloProvider(successHandler), - }); - }); - - it('renders the download dropdown', () => { - expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithText); - }); - - it('renders the expected message', () => { - expect(wrapper.text()).toContain(SecurityReportsApp.i18n.scansHaveRun); - }); - - it('renders a help link', () => { - expect(findHelpIconComponent().props()).toEqual({ - helpPath: props.securityReportsDocsPath, - discoverProjectSecurityPath: props.discoverProjectSecurityPath, - }); - }); - }); - - describe('given the artifacts query loads successfully with no artifacts', () => { - beforeEach(() => { - createComponent({ - apolloProvider: createMockApolloProvider(successEmptyHandler), - }); - }); - - // TODO: Remove this assertion as part of - // https://gitlab.com/gitlab-org/gitlab/-/issues/273431 - it('initially renders nothing', () => { - expect(wrapper.html()).toBe(''); - }); - }); - - describe('given the artifacts query fails', () => { - beforeEach(() => { - createComponent({ - apolloProvider: createMockApolloProvider(failureHandler), - }); - }); - - it('calls createAlert correctly', () => { - expect(createAlert).toHaveBeenCalledWith({ - message: SecurityReportsApp.i18n.apiError, - captureError: true, - error: expect.any(Error), - }); - }); - - // TODO: Remove this assertion as part of - // https://gitlab.com/gitlab-org/gitlab/-/issues/273431 - it('renders nothing', () => { - expect(wrapper.html()).toBe(''); - }); - }); - - describe('given the coreSecurityMrWidgetCounts feature flag is enabled', () => { - let mock; - - const createComponentWithFlagEnabled = (options) => - createComponent( - merge(options, { - provide: { - glFeatures: { - coreSecurityMrWidgetCounts: true, - }, - }, - apolloProvider: createMockApolloProvider(successHandler), - }), - ); - - beforeEach(() => { - mock = new MockAdapter(axios); - }); - - afterEach(() => { - mock.restore(); - }); - - const SAST_SUCCESS_MESSAGE = - 'Security scanning detected 1 potential vulnerability 1 Critical 0 High and 0 Others'; - const SECRET_DETECTION_SUCCESS_MESSAGE = - 'Security scanning detected 2 potential vulnerabilities 1 Critical 1 High and 0 Others'; - describe.each` - reportType | pathProp | path | successResponse | successMessage - ${REPORT_TYPE_SAST} | ${'sastComparisonPath'} | ${SAST_COMPARISON_PATH} | ${sastDiffSuccessMock} | ${SAST_SUCCESS_MESSAGE} - ${REPORT_TYPE_SECRET_DETECTION} | ${'secretDetectionComparisonPath'} | ${SECRET_DETECTION_COMPARISON_PATH} | ${secretDetectionDiffSuccessMock} | ${SECRET_DETECTION_SUCCESS_MESSAGE} - `( - 'given a $pathProp and $reportType artifact', - ({ pathProp, path, successResponse, successMessage }) => { - describe('when loading', () => { - beforeEach(() => { - mock = new MockAdapter(axios, { delayResponse: 1 }); - mock.onGet(path).replyOnce(HTTP_STATUS_OK, successResponse); - - createComponentWithFlagEnabled({ - propsData: { - [pathProp]: path, - }, - }); - - return waitForPromises(); - }); - - it('should have loading message', () => { - expect(wrapper.text()).toContain('Security scanning is loading'); - }); - - it('renders the download dropdown', () => { - expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithText); - }); - }); - - describe('when successfully loaded', () => { - beforeEach(() => { - mock.onGet(path).replyOnce(HTTP_STATUS_OK, successResponse); - - createComponentWithFlagEnabled({ - propsData: { - [pathProp]: path, - }, - }); - - return waitForPromises(); - }); - - it('should show counts', () => { - expect(trimText(wrapper.text())).toContain(successMessage); - }); - - it('renders the download dropdown', () => { - expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithText); - }); - }); - - describe('when an error occurs', () => { - beforeEach(() => { - mock.onGet(path).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR); - - createComponentWithFlagEnabled({ - propsData: { - [pathProp]: path, - }, - }); - - return waitForPromises(); - }); - - it('should show error message', () => { - expect(trimText(wrapper.text())).toContain('Loading resulted in an error'); - }); - - it('renders the download dropdown', () => { - expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithText); - }); - }); - - describe('when the comparison endpoint is not provided', () => { - beforeEach(() => { - mock.onGet(path).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR); - - createComponentWithFlagEnabled(); - - return waitForPromises(); - }); - - it('renders the basic scansHaveRun message', () => { - expect(wrapper.text()).toContain(SecurityReportsApp.i18n.scansHaveRun); - }); - }); - }, - ); - }); -}); diff --git a/spec/frontend/vue_shared/security_reports/store/getters_spec.js b/spec/frontend/vue_shared/security_reports/store/getters_spec.js deleted file mode 100644 index bcc8955ba02..00000000000 --- a/spec/frontend/vue_shared/security_reports/store/getters_spec.js +++ /dev/null @@ -1,182 +0,0 @@ -import { - groupedSummaryText, - allReportsHaveError, - areReportsLoading, - anyReportHasError, - areAllReportsLoading, - anyReportHasIssues, - summaryCounts, -} from '~/vue_shared/security_reports/store/getters'; -import createSastState from '~/vue_shared/security_reports/store/modules/sast/state'; -import createSecretDetectionState from '~/vue_shared/security_reports/store/modules/secret_detection/state'; -import createState from '~/vue_shared/security_reports/store/state'; -import { groupedTextBuilder } from '~/vue_shared/security_reports/store/utils'; -import { CRITICAL, HIGH, LOW } from '~/vulnerabilities/constants'; - -const generateVuln = (severity) => ({ severity }); - -describe('Security reports getters', () => { - let state; - - beforeEach(() => { - state = createState(); - state.sast = createSastState(); - state.secretDetection = createSecretDetectionState(); - }); - - describe('summaryCounts', () => { - it('returns 0 count for empty state', () => { - expect(summaryCounts(state)).toEqual({ - critical: 0, - high: 0, - other: 0, - }); - }); - - describe('combines all reports', () => { - it('of the same severity', () => { - state.sast.newIssues = [generateVuln(CRITICAL)]; - state.secretDetection.newIssues = [generateVuln(CRITICAL)]; - - expect(summaryCounts(state)).toEqual({ - critical: 2, - high: 0, - other: 0, - }); - }); - - it('of different severities', () => { - state.sast.newIssues = [generateVuln(CRITICAL)]; - state.secretDetection.newIssues = [generateVuln(HIGH), generateVuln(LOW)]; - - expect(summaryCounts(state)).toEqual({ - critical: 1, - high: 1, - other: 1, - }); - }); - }); - }); - - describe('groupedSummaryText', () => { - it('returns failed text', () => { - expect( - groupedSummaryText(state, { - allReportsHaveError: true, - areReportsLoading: false, - summaryCounts: {}, - }), - ).toEqual({ message: 'Security scanning failed loading any results' }); - }); - - it('returns `is loading` as status text', () => { - expect( - groupedSummaryText(state, { - allReportsHaveError: false, - areReportsLoading: true, - summaryCounts: {}, - }), - ).toEqual( - groupedTextBuilder({ - reportType: 'Security scanning', - critical: 0, - high: 0, - other: 0, - status: 'is loading', - }), - ); - }); - - it('returns no new status text if there are existing ones', () => { - expect( - groupedSummaryText(state, { - allReportsHaveError: false, - areReportsLoading: false, - summaryCounts: {}, - }), - ).toEqual( - groupedTextBuilder({ - reportType: 'Security scanning', - critical: 0, - high: 0, - other: 0, - status: '', - }), - ); - }); - }); - - describe('areReportsLoading', () => { - it('returns true when any report is loading', () => { - state.sast.isLoading = true; - - expect(areReportsLoading(state)).toEqual(true); - }); - - it('returns false when none of the reports are loading', () => { - expect(areReportsLoading(state)).toEqual(false); - }); - }); - - describe('areAllReportsLoading', () => { - it('returns true when all reports are loading', () => { - state.sast.isLoading = true; - state.secretDetection.isLoading = true; - - expect(areAllReportsLoading(state)).toEqual(true); - }); - - it('returns false when some of the reports are loading', () => { - state.sast.isLoading = true; - - expect(areAllReportsLoading(state)).toEqual(false); - }); - - it('returns false when none of the reports are loading', () => { - expect(areAllReportsLoading(state)).toEqual(false); - }); - }); - - describe('allReportsHaveError', () => { - it('returns true when all reports have error', () => { - state.sast.hasError = true; - state.secretDetection.hasError = true; - - expect(allReportsHaveError(state)).toEqual(true); - }); - - it('returns false when none of the reports have error', () => { - expect(allReportsHaveError(state)).toEqual(false); - }); - - it('returns false when one of the reports does not have error', () => { - state.secretDetection.hasError = true; - - expect(allReportsHaveError(state)).toEqual(false); - }); - }); - - describe('anyReportHasError', () => { - it('returns true when any of the reports has error', () => { - state.sast.hasError = true; - - expect(anyReportHasError(state)).toEqual(true); - }); - - it('returns false when none of the reports has error', () => { - expect(anyReportHasError(state)).toEqual(false); - }); - }); - - describe('anyReportHasIssues', () => { - it('returns true when any of the reports has new issues', () => { - state.sast.newIssues.push(generateVuln(LOW)); - - expect(anyReportHasIssues(state)).toEqual(true); - }); - - it('returns false when none of the reports has error', () => { - expect(anyReportHasIssues(state)).toEqual(false); - }); - }); -}); diff --git a/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js deleted file mode 100644 index 0cab950cb77..00000000000 --- a/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js +++ /dev/null @@ -1,197 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import testAction from 'helpers/vuex_action_helper'; - -import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status'; -import * as actions from '~/vue_shared/security_reports/store/modules/sast/actions'; -import * as types from '~/vue_shared/security_reports/store/modules/sast/mutation_types'; -import createState from '~/vue_shared/security_reports/store/modules/sast/state'; - -const diffEndpoint = 'diff-endpoint.json'; -const blobPath = 'blob-path.json'; -const reports = { - base: 'base', - head: 'head', - enrichData: 'enrichData', - diff: 'diff', -}; -const error = 'Something went wrong'; -const vulnerabilityFeedbackPath = 'vulnerability-feedback-path'; -const rootState = { vulnerabilityFeedbackPath, blobPath }; - -let state; - -describe('sast report actions', () => { - beforeEach(() => { - state = createState(); - }); - - describe('setDiffEndpoint', () => { - it(`should commit ${types.SET_DIFF_ENDPOINT} with the correct path`, () => { - return testAction( - actions.setDiffEndpoint, - diffEndpoint, - state, - [ - { - type: types.SET_DIFF_ENDPOINT, - payload: diffEndpoint, - }, - ], - [], - ); - }); - }); - - describe('requestDiff', () => { - it(`should commit ${types.REQUEST_DIFF}`, () => { - return testAction(actions.requestDiff, {}, state, [{ type: types.REQUEST_DIFF }], []); - }); - }); - - describe('receiveDiffSuccess', () => { - it(`should commit ${types.RECEIVE_DIFF_SUCCESS} with the correct response`, () => { - return testAction( - actions.receiveDiffSuccess, - reports, - state, - [ - { - type: types.RECEIVE_DIFF_SUCCESS, - payload: reports, - }, - ], - [], - ); - }); - }); - - describe('receiveDiffError', () => { - it(`should commit ${types.RECEIVE_DIFF_ERROR} with the correct response`, () => { - return testAction( - actions.receiveDiffError, - error, - state, - [ - { - type: types.RECEIVE_DIFF_ERROR, - payload: error, - }, - ], - [], - ); - }); - }); - - describe('fetchDiff', () => { - let mock; - - beforeEach(() => { - mock = new MockAdapter(axios); - state.paths.diffEndpoint = diffEndpoint; - rootState.canReadVulnerabilityFeedback = true; - }); - - afterEach(() => { - mock.restore(); - }); - - describe('when diff and vulnerability feedback endpoints respond successfully', () => { - beforeEach(() => { - mock - .onGet(diffEndpoint) - .replyOnce(HTTP_STATUS_OK, reports.diff) - .onGet(vulnerabilityFeedbackPath) - .replyOnce(HTTP_STATUS_OK, reports.enrichData); - }); - - it('should dispatch the `receiveDiffSuccess` action', () => { - const { diff, enrichData } = reports; - return testAction( - actions.fetchDiff, - {}, - { ...rootState, ...state }, - [], - [ - { type: 'requestDiff' }, - { - type: 'receiveDiffSuccess', - payload: { - diff, - enrichData, - }, - }, - ], - ); - }); - }); - - describe('when diff endpoint responds successfully and fetching vulnerability feedback is not authorized', () => { - beforeEach(() => { - rootState.canReadVulnerabilityFeedback = false; - mock.onGet(diffEndpoint).replyOnce(HTTP_STATUS_OK, reports.diff); - }); - - it('should dispatch the `receiveDiffSuccess` action with empty enrich data', () => { - const { diff } = reports; - const enrichData = []; - return testAction( - actions.fetchDiff, - {}, - { ...rootState, ...state }, - [], - [ - { type: 'requestDiff' }, - { - type: 'receiveDiffSuccess', - payload: { - diff, - enrichData, - }, - }, - ], - ); - }); - }); - - describe('when the vulnerability feedback endpoint fails', () => { - beforeEach(() => { - mock - .onGet(diffEndpoint) - .replyOnce(HTTP_STATUS_OK, reports.diff) - .onGet(vulnerabilityFeedbackPath) - .replyOnce(HTTP_STATUS_NOT_FOUND); - }); - - it('should dispatch the `receiveError` action', () => { - return testAction( - actions.fetchDiff, - {}, - { ...rootState, ...state }, - [], - [{ type: 'requestDiff' }, { type: 'receiveDiffError' }], - ); - }); - }); - - describe('when the diff endpoint fails', () => { - beforeEach(() => { - mock - .onGet(diffEndpoint) - .replyOnce(HTTP_STATUS_NOT_FOUND) - .onGet(vulnerabilityFeedbackPath) - .replyOnce(HTTP_STATUS_OK, reports.enrichData); - }); - - it('should dispatch the `receiveDiffError` action', () => { - return testAction( - actions.fetchDiff, - {}, - { ...rootState, ...state }, - [], - [{ type: 'requestDiff' }, { type: 'receiveDiffError' }], - ); - }); - }); - }); -}); diff --git a/spec/frontend/vue_shared/security_reports/store/modules/sast/mutations_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/sast/mutations_spec.js deleted file mode 100644 index d6119f44619..00000000000 --- a/spec/frontend/vue_shared/security_reports/store/modules/sast/mutations_spec.js +++ /dev/null @@ -1,84 +0,0 @@ -import * as types from '~/vue_shared/security_reports/store/modules/sast/mutation_types'; -import mutations from '~/vue_shared/security_reports/store/modules/sast/mutations'; -import createState from '~/vue_shared/security_reports/store/modules/sast/state'; - -const createIssue = ({ ...config }) => ({ changed: false, ...config }); - -describe('sast module mutations', () => { - const path = 'path'; - let state; - - beforeEach(() => { - state = createState(); - }); - - describe(types.SET_DIFF_ENDPOINT, () => { - it('should set the SAST diff endpoint', () => { - mutations[types.SET_DIFF_ENDPOINT](state, path); - - expect(state.paths.diffEndpoint).toBe(path); - }); - }); - - describe(types.REQUEST_DIFF, () => { - it('should set the `isLoading` status to `true`', () => { - mutations[types.REQUEST_DIFF](state); - - expect(state.isLoading).toBe(true); - }); - }); - - describe(types.RECEIVE_DIFF_SUCCESS, () => { - beforeEach(() => { - const reports = { - diff: { - added: [ - createIssue({ cve: 'CVE-1' }), - createIssue({ cve: 'CVE-2' }), - createIssue({ cve: 'CVE-3' }), - ], - fixed: [createIssue({ cve: 'CVE-4' }), createIssue({ cve: 'CVE-5' })], - existing: [createIssue({ cve: 'CVE-6' })], - base_report_out_of_date: true, - }, - }; - state.isLoading = true; - mutations[types.RECEIVE_DIFF_SUCCESS](state, reports); - }); - - it('should set the `isLoading` status to `false`', () => { - expect(state.isLoading).toBe(false); - }); - - it('should set the `baseReportOutofDate` status to `false`', () => { - expect(state.baseReportOutofDate).toBe(true); - }); - - it('should have the relevant `new` issues', () => { - expect(state.newIssues).toHaveLength(3); - }); - - it('should have the relevant `resolved` issues', () => { - expect(state.resolvedIssues).toHaveLength(2); - }); - - it('should have the relevant `all` issues', () => { - expect(state.allIssues).toHaveLength(1); - }); - }); - - describe(types.RECEIVE_DIFF_ERROR, () => { - beforeEach(() => { - state.isLoading = true; - mutations[types.RECEIVE_DIFF_ERROR](state); - }); - - it('should set the `isLoading` status to `false`', () => { - expect(state.isLoading).toBe(false); - }); - - it('should set the `hasError` status to `true`', () => { - expect(state.hasError).toBe(true); - }); - }); -}); diff --git a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js deleted file mode 100644 index 7197784c3e8..00000000000 --- a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js +++ /dev/null @@ -1,198 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import testAction from 'helpers/vuex_action_helper'; - -import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status'; -import * as actions from '~/vue_shared/security_reports/store/modules/secret_detection/actions'; -import * as types from '~/vue_shared/security_reports/store/modules/secret_detection/mutation_types'; -import createState from '~/vue_shared/security_reports/store/modules/secret_detection/state'; - -const diffEndpoint = 'diff-endpoint.json'; -const blobPath = 'blob-path.json'; -const reports = { - base: 'base', - head: 'head', - enrichData: 'enrichData', - diff: 'diff', -}; -const error = 'Something went wrong'; -const vulnerabilityFeedbackPath = 'vulnerability-feedback-path'; -const rootState = { vulnerabilityFeedbackPath, blobPath }; - -let state; - -describe('secret detection report actions', () => { - beforeEach(() => { - state = createState(); - }); - - describe('setDiffEndpoint', () => { - it(`should commit ${types.SET_DIFF_ENDPOINT} with the correct path`, () => { - return testAction( - actions.setDiffEndpoint, - diffEndpoint, - state, - [ - { - type: types.SET_DIFF_ENDPOINT, - payload: diffEndpoint, - }, - ], - [], - ); - }); - }); - - describe('requestDiff', () => { - it(`should commit ${types.REQUEST_DIFF}`, () => { - return testAction(actions.requestDiff, {}, state, [{ type: types.REQUEST_DIFF }], []); - }); - }); - - describe('receiveDiffSuccess', () => { - it(`should commit ${types.RECEIVE_DIFF_SUCCESS} with the correct response`, () => { - return testAction( - actions.receiveDiffSuccess, - reports, - state, - [ - { - type: types.RECEIVE_DIFF_SUCCESS, - payload: reports, - }, - ], - [], - ); - }); - }); - - describe('receiveDiffError', () => { - it(`should commit ${types.RECEIVE_DIFF_ERROR} with the correct response`, () => { - return testAction( - actions.receiveDiffError, - error, - state, - [ - { - type: types.RECEIVE_DIFF_ERROR, - payload: error, - }, - ], - [], - ); - }); - }); - - describe('fetchDiff', () => { - let mock; - - beforeEach(() => { - mock = new MockAdapter(axios); - state.paths.diffEndpoint = diffEndpoint; - rootState.canReadVulnerabilityFeedback = true; - }); - - afterEach(() => { - mock.restore(); - }); - - describe('when diff and vulnerability feedback endpoints respond successfully', () => { - beforeEach(() => { - mock - .onGet(diffEndpoint) - .replyOnce(HTTP_STATUS_OK, reports.diff) - .onGet(vulnerabilityFeedbackPath) - .replyOnce(HTTP_STATUS_OK, reports.enrichData); - }); - - it('should dispatch the `receiveDiffSuccess` action', () => { - const { diff, enrichData } = reports; - - return testAction( - actions.fetchDiff, - {}, - { ...rootState, ...state }, - [], - [ - { type: 'requestDiff' }, - { - type: 'receiveDiffSuccess', - payload: { - diff, - enrichData, - }, - }, - ], - ); - }); - }); - - describe('when diff endpoint responds successfully and fetching vulnerability feedback is not authorized', () => { - beforeEach(() => { - rootState.canReadVulnerabilityFeedback = false; - mock.onGet(diffEndpoint).replyOnce(HTTP_STATUS_OK, reports.diff); - }); - - it('should dispatch the `receiveDiffSuccess` action with empty enrich data', () => { - const { diff } = reports; - const enrichData = []; - return testAction( - actions.fetchDiff, - {}, - { ...rootState, ...state }, - [], - [ - { type: 'requestDiff' }, - { - type: 'receiveDiffSuccess', - payload: { - diff, - enrichData, - }, - }, - ], - ); - }); - }); - - describe('when the vulnerability feedback endpoint fails', () => { - beforeEach(() => { - mock - .onGet(diffEndpoint) - .replyOnce(HTTP_STATUS_OK, reports.diff) - .onGet(vulnerabilityFeedbackPath) - .replyOnce(HTTP_STATUS_NOT_FOUND); - }); - - it('should dispatch the `receiveDiffError` action', () => { - return testAction( - actions.fetchDiff, - {}, - { ...rootState, ...state }, - [], - [{ type: 'requestDiff' }, { type: 'receiveDiffError' }], - ); - }); - }); - - describe('when the diff endpoint fails', () => { - beforeEach(() => { - mock - .onGet(diffEndpoint) - .replyOnce(HTTP_STATUS_NOT_FOUND) - .onGet(vulnerabilityFeedbackPath) - .replyOnce(HTTP_STATUS_OK, reports.enrichData); - }); - - it('should dispatch the `receiveDiffError` action', () => { - return testAction( - actions.fetchDiff, - {}, - { ...rootState, ...state }, - [], - [{ type: 'requestDiff' }, { type: 'receiveDiffError' }], - ); - }); - }); - }); -}); diff --git a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/mutations_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/mutations_spec.js deleted file mode 100644 index 42da7476a40..00000000000 --- a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/mutations_spec.js +++ /dev/null @@ -1,84 +0,0 @@ -import * as types from '~/vue_shared/security_reports/store/modules/secret_detection/mutation_types'; -import mutations from '~/vue_shared/security_reports/store/modules/secret_detection/mutations'; -import createState from '~/vue_shared/security_reports/store/modules/secret_detection/state'; - -const createIssue = ({ ...config }) => ({ changed: false, ...config }); - -describe('secret detection module mutations', () => { - const path = 'path'; - let state; - - beforeEach(() => { - state = createState(); - }); - - describe(types.SET_DIFF_ENDPOINT, () => { - it('should set the secret detection diff endpoint', () => { - mutations[types.SET_DIFF_ENDPOINT](state, path); - - expect(state.paths.diffEndpoint).toBe(path); - }); - }); - - describe(types.REQUEST_DIFF, () => { - it('should set the `isLoading` status to `true`', () => { - mutations[types.REQUEST_DIFF](state); - - expect(state.isLoading).toBe(true); - }); - }); - - describe(types.RECEIVE_DIFF_SUCCESS, () => { - beforeEach(() => { - const reports = { - diff: { - added: [ - createIssue({ cve: 'CVE-1' }), - createIssue({ cve: 'CVE-2' }), - createIssue({ cve: 'CVE-3' }), - ], - fixed: [createIssue({ cve: 'CVE-4' }), createIssue({ cve: 'CVE-5' })], - existing: [createIssue({ cve: 'CVE-6' })], - base_report_out_of_date: true, - }, - }; - state.isLoading = true; - mutations[types.RECEIVE_DIFF_SUCCESS](state, reports); - }); - - it('should set the `isLoading` status to `false`', () => { - expect(state.isLoading).toBe(false); - }); - - it('should set the `baseReportOutofDate` status to `true`', () => { - expect(state.baseReportOutofDate).toBe(true); - }); - - it('should have the relevant `new` issues', () => { - expect(state.newIssues).toHaveLength(3); - }); - - it('should have the relevant `resolved` issues', () => { - expect(state.resolvedIssues).toHaveLength(2); - }); - - it('should have the relevant `all` issues', () => { - expect(state.allIssues).toHaveLength(1); - }); - }); - - describe(types.RECEIVE_DIFF_ERROR, () => { - beforeEach(() => { - state.isLoading = true; - mutations[types.RECEIVE_DIFF_ERROR](state); - }); - - it('should set the `isLoading` status to `false`', () => { - expect(state.isLoading).toBe(false); - }); - - it('should set the `hasError` status to `true`', () => { - expect(state.hasError).toBe(true); - }); - }); -}); diff --git a/spec/frontend/vue_shared/security_reports/store/utils_spec.js b/spec/frontend/vue_shared/security_reports/store/utils_spec.js deleted file mode 100644 index c8750cd58a0..00000000000 --- a/spec/frontend/vue_shared/security_reports/store/utils_spec.js +++ /dev/null @@ -1,63 +0,0 @@ -import { enrichVulnerabilityWithFeedback } from '~/vue_shared/security_reports/store/utils'; -import { - FEEDBACK_TYPE_DISMISSAL, - FEEDBACK_TYPE_ISSUE, - FEEDBACK_TYPE_MERGE_REQUEST, -} from '~/vue_shared/security_reports/constants'; - -describe('security reports store utils', () => { - const vulnerability = { uuid: 1 }; - - describe('enrichVulnerabilityWithFeedback', () => { - const dismissalFeedback = { - feedback_type: FEEDBACK_TYPE_DISMISSAL, - finding_uuid: vulnerability.uuid, - }; - const dismissalVuln = { ...vulnerability, isDismissed: true, dismissalFeedback }; - - const issueFeedback = { - feedback_type: FEEDBACK_TYPE_ISSUE, - issue_iid: 1, - finding_uuid: vulnerability.uuid, - }; - const issueVuln = { ...vulnerability, hasIssue: true, issue_feedback: issueFeedback }; - const mrFeedback = { - feedback_type: FEEDBACK_TYPE_MERGE_REQUEST, - merge_request_iid: 1, - finding_uuid: vulnerability.uuid, - }; - const mrVuln = { - ...vulnerability, - hasMergeRequest: true, - merge_request_feedback: mrFeedback, - }; - - it.each` - feedbacks | expected - ${[dismissalFeedback]} | ${dismissalVuln} - ${[{ ...issueFeedback, issue_iid: null }]} | ${vulnerability} - ${[issueFeedback]} | ${issueVuln} - ${[{ ...mrFeedback, merge_request_iid: null }]} | ${vulnerability} - ${[mrFeedback]} | ${mrVuln} - ${[dismissalFeedback, issueFeedback, mrFeedback]} | ${{ ...dismissalVuln, ...issueVuln, ...mrVuln }} - `('returns expected enriched vulnerability: $expected', ({ feedbacks, expected }) => { - const enrichedVulnerability = enrichVulnerabilityWithFeedback(vulnerability, feedbacks); - - expect(enrichedVulnerability).toEqual(expected); - }); - - it('matches correct feedback objects to vulnerability', () => { - const feedbacks = [ - dismissalFeedback, - issueFeedback, - mrFeedback, - { ...dismissalFeedback, finding_uuid: 2 }, - { ...issueFeedback, finding_uuid: 2 }, - { ...mrFeedback, finding_uuid: 2 }, - ]; - const enrichedVulnerability = enrichVulnerabilityWithFeedback(vulnerability, feedbacks); - - expect(enrichedVulnerability).toEqual({ ...dismissalVuln, ...issueVuln, ...mrVuln }); - }); - }); -}); diff --git a/spec/frontend/vue_shared/security_reports/utils_spec.js b/spec/frontend/vue_shared/security_reports/utils_spec.js deleted file mode 100644 index b7129ece698..00000000000 --- a/spec/frontend/vue_shared/security_reports/utils_spec.js +++ /dev/null @@ -1,48 +0,0 @@ -import { - REPORT_TYPE_SAST, - REPORT_TYPE_SECRET_DETECTION, - REPORT_FILE_TYPES, -} from '~/vue_shared/security_reports/constants'; -import { - extractSecurityReportArtifactsFromMergeRequest, - extractSecurityReportArtifactsFromPipeline, -} from '~/vue_shared/security_reports/utils'; -import { - securityReportMergeRequestDownloadPathsQueryResponse, - securityReportPipelineDownloadPathsQueryResponse, - sastArtifacts, - secretDetectionArtifacts, - archiveArtifacts, - traceArtifacts, - metadataArtifacts, -} from './mock_data'; - -describe.each([ - [ - 'extractSecurityReportArtifactsFromMergeRequest', - extractSecurityReportArtifactsFromMergeRequest, - securityReportMergeRequestDownloadPathsQueryResponse, - ], - [ - 'extractSecurityReportArtifactsFromPipelines', - extractSecurityReportArtifactsFromPipeline, - securityReportPipelineDownloadPathsQueryResponse, - ], -])('%s', (funcName, extractFunc, response) => { - it.each` - reportTypes | expectedArtifacts - ${[]} | ${[]} - ${['foo']} | ${[]} - ${[REPORT_TYPE_SAST]} | ${sastArtifacts} - ${[REPORT_TYPE_SECRET_DETECTION]} | ${secretDetectionArtifacts} - ${[REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION]} | ${[...secretDetectionArtifacts, ...sastArtifacts]} - ${[REPORT_FILE_TYPES.ARCHIVE]} | ${archiveArtifacts} - ${[REPORT_FILE_TYPES.TRACE]} | ${traceArtifacts} - ${[REPORT_FILE_TYPES.METADATA]} | ${metadataArtifacts} - `( - 'returns the expected artifacts given report types $reportTypes', - ({ reportTypes, expectedArtifacts }) => { - expect(extractFunc(reportTypes, response)).toEqual(expectedArtifacts); - }, - ); -}); diff --git a/spec/graphql/gitlab_schema_spec.rb b/spec/graphql/gitlab_schema_spec.rb index 2e0711fe18c..885bbc82ecc 100644 --- a/spec/graphql/gitlab_schema_spec.rb +++ b/spec/graphql/gitlab_schema_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe GitlabSchema do - let_it_be(:connections) { GitlabSchema.connections.all_wrappers } + let_it_be(:connections) { described_class.connections.all_wrappers } let_it_be(:tracers) { described_class.tracers } let(:user) { build :user } diff --git a/spec/graphql/graphql_triggers_spec.rb b/spec/graphql/graphql_triggers_spec.rb index 864818351a1..3f58f2678d8 100644 --- a/spec/graphql/graphql_triggers_spec.rb +++ b/spec/graphql/graphql_triggers_spec.rb @@ -20,7 +20,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do issuable ) - GraphqlTriggers.issuable_assignees_updated(issuable) + described_class.issuable_assignees_updated(issuable) end end @@ -32,7 +32,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do issuable ).and_call_original - GraphqlTriggers.issuable_title_updated(issuable) + described_class.issuable_title_updated(issuable) end end @@ -44,7 +44,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do issuable ).and_call_original - GraphqlTriggers.issuable_description_updated(issuable) + described_class.issuable_description_updated(issuable) end end @@ -62,7 +62,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do issuable ) - GraphqlTriggers.issuable_labels_updated(issuable) + described_class.issuable_labels_updated(issuable) end end @@ -74,7 +74,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do issuable ).and_call_original - GraphqlTriggers.issuable_dates_updated(issuable) + described_class.issuable_dates_updated(issuable) end end @@ -86,7 +86,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do issuable ).and_call_original - GraphqlTriggers.issuable_milestone_updated(issuable) + described_class.issuable_milestone_updated(issuable) end end @@ -100,7 +100,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do merge_request ).and_call_original - GraphqlTriggers.merge_request_reviewers_updated(merge_request) + described_class.merge_request_reviewers_updated(merge_request) end end @@ -114,7 +114,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do merge_request ).and_call_original - GraphqlTriggers.merge_request_merge_status_updated(merge_request) + described_class.merge_request_merge_status_updated(merge_request) end end @@ -128,7 +128,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do merge_request ).and_call_original - GraphqlTriggers.merge_request_approval_state_updated(merge_request) + described_class.merge_request_approval_state_updated(merge_request) end end @@ -140,7 +140,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do issuable ).and_call_original - GraphqlTriggers.work_item_updated(issuable) + described_class.work_item_updated(issuable) end context 'when triggered with an Issue' do @@ -154,7 +154,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do work_item ).and_call_original - GraphqlTriggers.work_item_updated(issue) + described_class.work_item_updated(issue) end end end diff --git a/spec/graphql/types/global_id_type_spec.rb b/spec/graphql/types/global_id_type_spec.rb index fa0b34113bc..8ce0bc2b70a 100644 --- a/spec/graphql/types/global_id_type_spec.rb +++ b/spec/graphql/types/global_id_type_spec.rb @@ -105,12 +105,12 @@ RSpec.describe Types::GlobalIDType do around do |example| # Unset all previously memoized GlobalIDTypes to allow us to define one # that will use the constants stubbed in the `before` block. - previous_id_types = Types::GlobalIDType.instance_variable_get(:@id_types) - Types::GlobalIDType.instance_variable_set(:@id_types, {}) + previous_id_types = described_class.instance_variable_get(:@id_types) + described_class.instance_variable_set(:@id_types, {}) example.run ensure - Types::GlobalIDType.instance_variable_set(:@id_types, previous_id_types) + described_class.instance_variable_set(:@id_types, previous_id_types) end before do diff --git a/spec/initializers/google_api_client_spec.rb b/spec/initializers/google_api_client_spec.rb index b3c4ac5e23b..cd3e3cf0328 100644 --- a/spec/initializers/google_api_client_spec.rb +++ b/spec/initializers/google_api_client_spec.rb @@ -8,7 +8,7 @@ require 'google/apis/core/base_service' RSpec.describe Google::Apis::Core::HttpCommand do # rubocop:disable RSpec/FilePath context('with a successful response') do let(:client) { Google::Apis::Core::BaseService.new('', '').client } - let(:command) { Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals') } + let(:command) { described_class.new(:get, 'https://www.googleapis.com/zoo/animals') } before do stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(body: %(Hello world)) diff --git a/spec/lib/gitlab/checks/file_size_check/any_oversized_blob_spec.rb b/spec/lib/gitlab/checks/file_size_check/any_oversized_blob_spec.rb new file mode 100644 index 00000000000..bf24d6b63c6 --- /dev/null +++ b/spec/lib/gitlab/checks/file_size_check/any_oversized_blob_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Checks::FileSizeCheck::AnyOversizedBlob, feature_category: :source_code_management do + let_it_be(:project) { create(:project, :public, :repository) } + let(:any_blob) do + described_class.new( + project: project, + changes: [{ newrev: 'bf12d2567099e26f59692896f73ac819bae45b00' }], + file_size_limit_megabytes: 1) + end + + describe '#find!' do + subject { any_blob.find! } + + # SHA of the 2-mb-file branch + let(:newrev) { 'bf12d2567099e26f59692896f73ac819bae45b00' } + let(:timeout) { nil } + + before do + # Delete branch so Repository#new_blobs can return results + project.repository.delete_branch('2-mb-file') + end + + it 'returns the blob exceeding the file size limit' do + blob = subject + expect(blob).to be_kind_of(Gitlab::Git::Blob) + expect(blob.path).to eq('file.bin') + end + end +end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index e5406be0f97..aa7d4b84fb5 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -143,6 +143,7 @@ milestone: - boards - milestone_releases - releases +- user_agent_detail snippets: - author - project diff --git a/spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb b/spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb index cc85c897019..28f6919e6bc 100644 --- a/spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb +++ b/spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb @@ -33,6 +33,18 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder ] end + let_it_be(:ignored_column_model) do + Class.new(ApplicationRecord) do + self.table_name = 'issues' + + include IgnorableColumns + + ignore_column :title, remove_with: '16.4', remove_after: '2023-08-22' + end + end + + let(:scope_model) { Issue } + let(:created_records) { issues } let(:iterator) do Gitlab::Pagination::Keyset::Iterator.new( scope: scope.limit(batch_size), @@ -79,6 +91,55 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder end end + context 'when the scope model has ignored columns' do + let(:scope) { ignored_column_model.order(id: :desc) } + let(:expected_order) { ignored_column_model.where(id: issues.map(&:id)).sort_by(&:id).reverse } + + let(:in_operator_optimization_options) do + { + array_scope: Project.where(namespace_id: top_level_group.self_and_descendants.select(:id)).select(:id), + array_mapping_scope: -> (id_expression) { ignored_column_model.where(ignored_column_model.arel_table[:project_id].eq(id_expression)) }, + finder_query: -> (id_expression) { ignored_column_model.where(ignored_column_model.arel_table[:id].eq(id_expression)) } + } + end + + context 'when iterating records one by one' do + let(:batch_size) { 1 } + + it_behaves_like 'correct ordering examples' + + context 'when scope selects only some columns' do + let(:scope) { ignored_column_model.order(id: :desc).select(:id) } + + it_behaves_like 'correct ordering examples' + end + end + + context 'when iterating records with LIMIT 3' do + let(:batch_size) { 3 } + + it_behaves_like 'correct ordering examples' + + context 'when scope selects only some columns' do + let(:scope) { ignored_column_model.order(id: :desc).select(:id) } + + it_behaves_like 'correct ordering examples' + end + end + + context 'when loading records at once' do + let(:batch_size) { issues.size + 1 } + + it_behaves_like 'correct ordering examples' + + context 'when scope selects only some columns' do + let(:scope) { ignored_column_model.order(id: :desc).select(:id) } + + it_behaves_like 'correct ordering examples' + end + end + end + context 'when ordering by issues.id DESC' do let(:scope) { Issue.order(id: :desc) } let(:expected_order) { issues.sort_by(&:id).reverse } @@ -95,6 +156,14 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder let(:batch_size) { 1 } it_behaves_like 'correct ordering examples' + + context 'when key_set_optimizer_ignored_columns feature flag is disabled' do + before do + stub_feature_flags(key_set_optimizer_ignored_columns: false) + end + + it_behaves_like 'correct ordering examples' + end end context 'when iterating records with LIMIT 3' do @@ -332,7 +401,7 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder end context 'when ordering by JOIN-ed columns' do - let(:scope) { cte_with_issues_and_projects.apply_to(Issue.where({})).reorder(order) } + let(:scope) { cte_with_issues_and_projects.apply_to(Issue.where({}).select(Arel.star)).reorder(order) } let(:cte_with_issues_and_projects) do cte_query = Issue.select('issues.id AS id', 'project_id', 'projects.id AS projects_id', 'projects.name AS projects_name').joins(:project) diff --git a/spec/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy_spec.rb b/spec/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy_spec.rb index 5180403b493..c20f3c96734 100644 --- a/spec/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy_spec.rb +++ b/spec/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy_spec.rb @@ -3,12 +3,12 @@ require 'spec_helper' RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::Strategies::RecordLoaderStrategy do - let(:finder_query) { -> (created_at_value, id_value) { Project.where(Project.arel_table[:id].eq(id_value)) } } + let(:finder_query) { -> (created_at_value, id_value) { model.where(model.arel_table[:id].eq(id_value)) } } let(:model) { Project } let(:keyset_scope) do scope, _ = Gitlab::Pagination::Keyset::SimpleOrderBuilder.build( - Project.order(:created_at, :id) + model.order(:created_at, :id) ) scope @@ -22,6 +22,16 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::Strategies::R Gitlab::Pagination::Keyset::InOperatorOptimization::OrderByColumns.new(keyset_order.column_definitions, model.arel_table) end + let_it_be(:ignored_column_model) do + Class.new(ApplicationRecord) do + self.table_name = 'projects' + + include IgnorableColumns + + ignore_column :name, remove_with: '16.4', remove_after: '2023-08-22' + end + end + subject(:strategy) { described_class.new(finder_query, model, order_by_columns) } describe '#initializer_columns' do @@ -57,4 +67,32 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::Strategies::R expect(strategy.columns).to eq([expected_loader_query.chomp]) end end + + describe '#final_projections' do + context 'when model does not have ignored columns' do + it 'does not specify the selected column names' do + expect(strategy.final_projections).to contain_exactly("(#{described_class::RECORDS_COLUMN}).*") + end + end + + context 'when model has ignored columns' do + let(:model) { ignored_column_model } + + it 'specifies the selected column names' do + expect(strategy.final_projections).to match_array( + model.default_select_columns.map { |column| "(#{described_class::RECORDS_COLUMN}).#{column.name}" } + ) + end + + context 'when the key_set_optimizer_ignored_columns feature flag is disabled' do + before do + stub_feature_flags(key_set_optimizer_ignored_columns: false) + end + + it 'does not specify the selected column names' do + expect(strategy.final_projections).to contain_exactly("(#{described_class::RECORDS_COLUMN}).*") + end + end + end + end end diff --git a/spec/lib/gitlab/pagination/keyset/order_spec.rb b/spec/lib/gitlab/pagination/keyset/order_spec.rb index dda68f6e5ae..a8bdafb1ce8 100644 --- a/spec/lib/gitlab/pagination/keyset/order_spec.rb +++ b/spec/lib/gitlab/pagination/keyset/order_spec.rb @@ -645,6 +645,16 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do let_it_be(:user_2) { create(:user, created_at: five_months_ago) } let_it_be(:user_3) { create(:user, created_at: 1.month.ago) } let_it_be(:user_4) { create(:user, created_at: 2.months.ago) } + let_it_be(:ignored_column_model) do + Class.new(ApplicationRecord) do + self.table_name = 'users' + + include IgnorableColumns + include FromUnion + + ignore_column :username, remove_with: '16.4', remove_after: '2023-08-22' + end + end let(:expected_results) { [user_3, user_4, user_2, user_1] } let(:scope) { User.order(created_at: :desc, id: :desc) } @@ -672,6 +682,36 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do iterator_options[:use_union_optimization] = true end + context 'when the scope model has ignored columns' do + let(:ignored_expected_results) { expected_results.map { |r| r.becomes(ignored_column_model) } } # rubocop:disable Cop/AvoidBecomes + + context 'when scope selects all columns' do + let(:scope) { ignored_column_model.order(created_at: :desc, id: :desc) } + + it 'returns items in the correct order' do + expect(items).to eq(ignored_expected_results) + end + end + + context 'when scope selects only specific columns' do + let(:scope) { ignored_column_model.order(created_at: :desc, id: :desc).select(:id, :created_at) } + + it 'returns items in the correct order' do + expect(items).to eq(ignored_expected_results) + end + end + end + + context 'when key_set_optimizer_ignored_columns feature flag is disabled' do + before do + stub_feature_flags(key_set_optimizer_ignored_columns: false) + end + + it 'returns items in the correct order' do + expect(items).to eq(expected_results) + end + end + it 'returns items in the correct order' do expect(items).to eq(expected_results) end diff --git a/spec/lib/slack/manifest_spec.rb b/spec/lib/slack/manifest_spec.rb new file mode 100644 index 00000000000..f602f05d260 --- /dev/null +++ b/spec/lib/slack/manifest_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Slack::Manifest, feature_category: :integrations do + describe '.to_h' do + it 'creates the correct manifest' do + expect(described_class.to_h).to eq({ + display_information: { + name: "GitLab (#{Gitlab.config.gitlab.host})", + description: s_('SlackIntegration|Interact with GitLab without leaving your Slack workspace!'), + background_color: '#171321', + long_description: "Generated for #{Gitlab.config.gitlab.host} by GitLab #{Gitlab::VERSION}.\r\n\r\n" \ + "- *Notifications:* Get notifications to your team's Slack channel about events " \ + "happening inside your GitLab projects.\r\n\r\n- *Slash commands:* Quickly open, " \ + 'access, or close issues from Slack using the `/gitlab` command. Streamline your ' \ + 'GitLab deployments with ChatOps.' + }, + features: { + app_home: { + home_tab_enabled: true, + messages_tab_enabled: false, + messages_tab_read_only_enabled: true + }, + bot_user: { + display_name: 'GitLab', + always_online: true + }, + slash_commands: [ + { + command: '/gitlab', + url: "#{Gitlab.config.gitlab.url}/api/v4/slack/trigger", + description: 'GitLab slash commands', + usage_hint: 'your-project-name-or-alias command', + should_escape: false + } + ] + }, + oauth_config: { + redirect_urls: [ + Gitlab.config.gitlab.url + ], + scopes: { + bot: %w[ + commands + chat:write + chat:write.public + ] + } + }, + settings: { + event_subscriptions: { + request_url: "#{Gitlab.config.gitlab.url}/api/v4/integrations/slack/events", + bot_events: %w[ + app_home_opened + ] + }, + interactivity: { + is_enabled: true, + request_url: "#{Gitlab.config.gitlab.url}/api/v4/integrations/slack/interactions", + message_menu_options_url: "#{Gitlab.config.gitlab.url}/api/v4/integrations/slack/options" + }, + org_deploy_enabled: false, + socket_mode_enabled: false, + token_rotation_enabled: false + } + }) + end + end + + describe '.to_json' do + subject(:to_json) { described_class.to_json } + + shared_examples 'a manifest that matches the JSON schema' do + it { is_expected.to match_schema('slack/manifest') } + end + + it_behaves_like 'a manifest that matches the JSON schema' + + context 'when the host name is very long' do + before do + allow(Gitlab.config.gitlab).to receive(:host).and_return('abc' * 20) + end + + it_behaves_like 'a manifest that matches the JSON schema' + end + end + + describe '.share_url' do + it 'URI encodes the manifest' do + allow(described_class).to receive(:to_h).and_return({ foo: 'bar' }) + + expect(described_class.share_url).to eq('https://api.slack.com/apps?new_app=1&manifest_json=%7B%22foo%22%3A%22bar%22%7D') + end + end +end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 76771360e1f..629dfdaf55e 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -109,8 +109,14 @@ RSpec.describe Notify do is_expected.to have_body_text issue.description end - it 'does not add a reason header' do - is_expected.not_to have_header('X-GitLab-NotificationReason', /.+/) + context 'when issue is confidential' do + before do + issue.update_attribute(:confidential, true) + end + + it 'has a confidential header set to true' do + is_expected.to have_header('X-GitLab-ConfidentialIssue', 'true') + end end context 'when sent with a reason' do @@ -819,6 +825,10 @@ RSpec.describe Notify do let_it_be(:second_note) { create(:discussion_note_on_issue, in_reply_to: first_note, project: project) } let_it_be(:third_note) { create(:discussion_note_on_issue, in_reply_to: second_note, project: project) } + before_all do + first_note.noteable.update_attribute(:confidential, "true") + end + subject { described_class.note_issue_email(recipient.id, third_note.id) } it_behaves_like 'an email sent to a user' @@ -840,17 +850,29 @@ RSpec.describe Notify do it 'has X-GitLab-Discussion-ID header' do expect(subject.header['X-GitLab-Discussion-ID'].value).to eq(third_note.discussion.id) end + + it 'has a confidential header set to true' do + is_expected.to have_header('X-GitLab-ConfidentialIssue', 'true') + end end context 'individual issue comments' do let_it_be(:note) { create(:note_on_issue, project: project) } + before_all do + note.noteable.update_attribute(:confidential, "true") + end + subject { described_class.note_issue_email(recipient.id, note.id) } it_behaves_like 'an email sent to a user' it_behaves_like 'appearance header and footer enabled' it_behaves_like 'appearance header and footer not enabled' + it 'has a confidential header set to true' do + expect(subject.header['X-GitLab-ConfidentialIssue'].value).to eq('true') + end + it 'has In-Reply-To header pointing to the issue' do expect(subject.header['In-Reply-To'].message_ids).to eq(["issue_#{note.noteable.id}@#{host}"]) end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 9d7744a0ffd..1f0f89fea60 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Milestone do +RSpec.describe Milestone, feature_category: :team_planning do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :public) } let_it_be(:group) { create(:group) } @@ -732,4 +732,44 @@ RSpec.describe Milestone do expect(milestone.lock_version).to be_present end end + + describe '#check_for_spam?' do + let_it_be(:milestone) { build_stubbed(:milestone, project: project) } + + subject { milestone.check_for_spam? } + + context 'when spammable attribute title has changed' do + before do + milestone.title = 'New title' + end + + it { is_expected.to eq(true) } + end + + context 'when spammable attribute description has changed' do + before do + milestone.description = 'New description' + end + + it { is_expected.to eq(true) } + end + + context 'when spammable attribute has changed but parent is private' do + before do + milestone.title = 'New title' + milestone.parent.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PRIVATE) + end + + it { is_expected.to eq(false) } + end + + context 'when no spammable attribute has changed' do + before do + milestone.title = milestone.title_was + milestone.description = milestone.description_was + end + + it { is_expected.to eq(false) } + end + end end diff --git a/spec/rubocop/cop/ignored_columns_spec.rb b/spec/rubocop/cop/ignored_columns_spec.rb index 8d2c6b92c70..c8f47f8aee9 100644 --- a/spec/rubocop/cop/ignored_columns_spec.rb +++ b/spec/rubocop/cop/ignored_columns_spec.rb @@ -4,20 +4,20 @@ require 'rubocop_spec_helper' require_relative '../../../rubocop/cop/ignored_columns' RSpec.describe RuboCop::Cop::IgnoredColumns, feature_category: :database do - it 'flags use of `self.ignored_columns +=` instead of the IgnoredColumns concern' do + it 'flags use of `self.ignored_columns +=` instead of the IgnorableColumns concern' do expect_offense(<<~RUBY) class Foo < ApplicationRecord self.ignored_columns += %i[id] - ^^^^^^^^^^^^^^^ Use `IgnoredColumns` concern instead of adding to `self.ignored_columns`. + ^^^^^^^^^^^^^^^ Use `IgnorableColumns` concern instead of adding to `self.ignored_columns`. end RUBY end - it 'flags use of `self.ignored_columns =` instead of the IgnoredColumns concern' do + it 'flags use of `self.ignored_columns =` instead of the IgnorableColumns concern' do expect_offense(<<~RUBY) class Foo < ApplicationRecord self.ignored_columns = %i[id] - ^^^^^^^^^^^^^^^ Use `IgnoredColumns` concern instead of setting `self.ignored_columns`. + ^^^^^^^^^^^^^^^ Use `IgnorableColumns` concern instead of setting `self.ignored_columns`. end RUBY end diff --git a/spec/serializers/prometheus_alert_entity_spec.rb b/spec/serializers/prometheus_alert_entity_spec.rb deleted file mode 100644 index 02da5a5bb88..00000000000 --- a/spec/serializers/prometheus_alert_entity_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe PrometheusAlertEntity do - let(:user) { build_stubbed(:user) } - let(:prometheus_alert) { build_stubbed(:prometheus_alert) } - let(:request) { double('prometheus_alert', current_user: user) } - let(:entity) { described_class.new(prometheus_alert, request: request) } - - subject { entity.as_json } - - context 'when user can read prometheus alerts' do - before do - prometheus_alert.project.add_maintainer(user) - end - - it 'exposes prometheus_alert attributes' do - expect(subject).to include(:id, :title, :query, :operator, :threshold, :runbook_url) - end - end -end diff --git a/spec/services/milestones/create_service_spec.rb b/spec/services/milestones/create_service_spec.rb index 78cb05532eb..70010d88fbd 100644 --- a/spec/services/milestones/create_service_spec.rb +++ b/spec/services/milestones/create_service_spec.rb @@ -3,24 +3,70 @@ require 'spec_helper' RSpec.describe Milestones::CreateService, feature_category: :team_planning do - let(:project) { create(:project) } - let(:user) { create(:user) } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:params) { { title: 'New Milestone', description: 'Description' } } + + subject(:create_milestone) { described_class.new(project, user, params) } describe '#execute' do - context "valid params" do + context 'when milestone is saved successfully' do + it 'creates a new milestone' do + expect { create_milestone.execute }.to change { Milestone.count }.by(1) + end + + it 'opens the milestone if it is a project milestone' do + expect_next_instance_of(EventCreateService) do |instance| + expect(instance).to receive(:open_milestone) + end + + create_milestone.execute + end + + it 'returns the created milestone' do + milestone = create_milestone.execute + expect(milestone).to be_a(Milestone) + expect(milestone.title).to eq('New Milestone') + expect(milestone.description).to eq('Description') + end + end + + context 'when milestone fails to save' do before do - project.add_maintainer(user) + allow_next_instance_of(Milestone) do |instance| + allow(instance).to receive(:save).and_return(false) + end + end + + it 'does not create a new milestone' do + expect { create_milestone.execute }.not_to change { Milestone.count } + end - opts = { - title: 'v2.1.9', - description: 'Patch release to fix security issue' - } + it 'does not open the milestone' do + expect(EventCreateService).not_to receive(:open_milestone) + + create_milestone.execute + end - @milestone = described_class.new(project, user, opts).execute + it 'returns the unsaved milestone' do + milestone = create_milestone.execute + expect(milestone).to be_a(Milestone) + expect(milestone.title).to eq('New Milestone') + expect(milestone.persisted?).to be_falsey end + end + + it 'calls before_create method' do + expect(create_milestone).to receive(:before_create) + create_milestone.execute + end + end - it { expect(@milestone).to be_valid } - it { expect(@milestone.title).to eq('v2.1.9') } + describe '#before_create' do + it 'checks for spam' do + milestone = build(:milestone) + expect(milestone).to receive(:check_for_spam).with(user: user, action: :create) + subject.send(:before_create, milestone) end end end diff --git a/spec/services/milestones/update_service_spec.rb b/spec/services/milestones/update_service_spec.rb index 76110af2514..44de49960d4 100644 --- a/spec/services/milestones/update_service_spec.rb +++ b/spec/services/milestones/update_service_spec.rb @@ -2,40 +2,86 @@ require 'spec_helper' RSpec.describe Milestones::UpdateService, feature_category: :team_planning do - let(:project) { create(:project) } - let(:user) { build(:user) } - let(:milestone) { create(:milestone, project: project) } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:milestone) { create(:milestone, project: project) } + let_it_be(:params) { { title: 'New Title' } } + + subject(:update_milestone) { described_class.new(project, user, params) } describe '#execute' do - context "valid params" do - let(:inner_service) { double(:service) } + context 'when state_event is "activate"' do + let(:params) { { state_event: 'activate' } } - before do - project.add_maintainer(user) + it 'calls Milestones::ReopenService' do + reopen_service = instance_double(Milestones::ReopenService) + expect(Milestones::ReopenService).to receive(:new).with(project, user, {}).and_return(reopen_service) + expect(reopen_service).to receive(:execute).with(milestone) + + update_milestone.execute(milestone) end + end - subject { described_class.new(project, user, { title: 'new_title' }).execute(milestone) } + context 'when state_event is "close"' do + let(:params) { { state_event: 'close' } } + + it 'calls Milestones::CloseService' do + close_service = instance_double(Milestones::CloseService) + expect(Milestones::CloseService).to receive(:new).with(project, user, {}).and_return(close_service) + expect(close_service).to receive(:execute).with(milestone) + + update_milestone.execute(milestone) + end + end - it { expect(subject).to be_valid } - it { expect(subject.title).to eq('new_title') } + context 'when params are present' do + it 'assigns the params to the milestone' do + expect(milestone).to receive(:assign_attributes).with(params.except(:state_event)) - context 'state_event is activate' do - it 'calls ReopenService' do - expect(Milestones::ReopenService).to receive(:new).with(project, user, {}).and_return(inner_service) - expect(inner_service).to receive(:execute).with(milestone) + update_milestone.execute(milestone) + end + end - described_class.new(project, user, { state_event: 'activate' }).execute(milestone) - end + context 'when milestone is changed' do + before do + allow(milestone).to receive(:changed?).and_return(true) end - context 'state_event is close' do - it 'calls ReopenService' do - expect(Milestones::CloseService).to receive(:new).with(project, user, {}).and_return(inner_service) - expect(inner_service).to receive(:execute).with(milestone) + it 'calls before_update' do + expect(update_milestone).to receive(:before_update).with(milestone) - described_class.new(project, user, { state_event: 'close' }).execute(milestone) - end + update_milestone.execute(milestone) end end + + context 'when milestone is not changed' do + before do + allow(milestone).to receive(:changed?).and_return(false) + end + + it 'does not call before_update' do + expect(update_milestone).not_to receive(:before_update) + + update_milestone.execute(milestone) + end + end + + it 'saves the milestone' do + expect(milestone).to receive(:save) + + update_milestone.execute(milestone) + end + + it 'returns the milestone' do + expect(update_milestone.execute(milestone)).to eq(milestone) + end + end + + describe '#before_update' do + it 'checks for spam' do + expect(milestone).to receive(:check_for_spam).with(user: user, action: :update) + + update_milestone.send(:before_update, milestone) + end end end diff --git a/spec/views/admin/application_settings/_slack.html.haml_spec.rb b/spec/views/admin/application_settings/_slack.html.haml_spec.rb new file mode 100644 index 00000000000..6f89d2b7de4 --- /dev/null +++ b/spec/views/admin/application_settings/_slack.html.haml_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'admin/application_settings/_slack.html.haml', feature_category: :integrations do + let(:app_settings) { build(:application_setting) } + + before do + assign(:application_setting, app_settings) + end + + it 'renders the form correctly', :aggregate_failures do + render + + expect(rendered).to have_field('Client ID', type: 'text') + expect(rendered).to have_field('Client secret', type: 'text') + expect(rendered).to have_field('Signing secret', type: 'text') + expect(rendered).to have_field('Verification token', type: 'text') + expect(rendered).to have_link( + 'Create Slack app', + href: slack_app_manifest_share_admin_application_settings_path + ) + expect(rendered).to have_link( + 'Download latest manifest file', + href: slack_app_manifest_download_admin_application_settings_path + ) + end + + context 'when GitLab.com', :saas do + it 'renders the form correctly', :aggregate_failures do + render + + expect(rendered).to have_field('Client ID', type: 'text') + expect(rendered).to have_field('Client secret', type: 'text') + expect(rendered).to have_field('Signing secret', type: 'text') + expect(rendered).to have_field('Verification token', type: 'text') + + expect(rendered).not_to have_link('Create Slack app') + expect(rendered).not_to have_link('Download latest manifest file') + end + end +end |