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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-05-24 21:09:14 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-24 21:09:14 +0300
commitca386bfc0cf083e0ccb477995378061fc2a15b66 (patch)
treef53d4a2f288ba64e9f440080817f14d62965398c /spec
parent61ebd5753018a1f4b6032122f6ea625dc4e4fc8e (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/factories/service_desk/custom_email_credential.rb2
-rw-r--r--spec/features/commits_spec.rb7
-rw-r--r--spec/features/merge_request/user_sees_merge_request_file_tree_sidebar_spec.rb62
-rw-r--r--spec/fixtures/emails/service_desk_custom_email_address_verification.eml31
-rw-r--r--spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js39
-rw-r--r--spec/frontend/admin/abuse_report/components/report_actions_spec.js158
-rw-r--r--spec/frontend/admin/abuse_report/components/report_header_spec.js49
-rw-r--r--spec/frontend/admin/abuse_report/mock_data.js10
-rw-r--r--spec/frontend/admin/abuse_reports/components/abuse_report_actions_spec.js202
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js3
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js2
-rw-r--r--spec/frontend/content_editor/extensions/description_item_spec.js121
-rw-r--r--spec/frontend/content_editor/extensions/description_list_spec.js36
-rw-r--r--spec/frontend/content_editor/extensions/details_content_spec.js20
-rw-r--r--spec/frontend/content_editor/extensions/details_spec.js23
-rw-r--r--spec/frontend/content_editor/test_utils.js9
-rw-r--r--spec/frontend/diffs/components/tree_list_spec.js2
-rw-r--r--spec/frontend/notes/components/notes_app_spec.js6
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js2
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js30
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_template_dropdown_spec.js1
-rw-r--r--spec/helpers/admin/abuse_reports_helper_spec.rb2
-rw-r--r--spec/helpers/resource_events/abuse_report_events_helper_spec.rb17
-rw-r--r--spec/mailers/notify_spec.rb7
-rw-r--r--spec/models/resource_events/abuse_report_event_spec.rb8
-rw-r--r--spec/requests/admin/abuse_reports_controller_spec.rb37
-rw-r--r--spec/requests/api/internal/kubernetes_spec.rb75
-rw-r--r--spec/serializers/admin/abuse_report_details_entity_spec.rb25
-rw-r--r--spec/serializers/admin/abuse_report_details_serializer_spec.rb3
-rw-r--r--spec/services/admin/abuse_report_update_service_spec.rb13
-rw-r--r--spec/services/service_desk/custom_email_verifications/create_service_spec.rb139
-rw-r--r--spec/services/service_desk/custom_email_verifications/update_service_spec.rb151
-rw-r--r--spec/support/helpers/merge_request_diff_helpers.rb12
33 files changed, 1028 insertions, 276 deletions
diff --git a/spec/factories/service_desk/custom_email_credential.rb b/spec/factories/service_desk/custom_email_credential.rb
index da131dd8250..f1da12327a2 100644
--- a/spec/factories/service_desk/custom_email_credential.rb
+++ b/spec/factories/service_desk/custom_email_credential.rb
@@ -4,7 +4,7 @@ FactoryBot.define do
factory :service_desk_custom_email_credential, class: '::ServiceDesk::CustomEmailCredential' do
project
smtp_address { "smtp.example.com" }
- smtp_username { "text@example.com" }
+ smtp_username { "user@example.com" }
smtp_port { 587 }
smtp_password { "supersecret" }
end
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index a84f469750d..fd09a7f7343 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -187,6 +187,13 @@ RSpec.describe 'Commits', feature_category: :source_code_management do
visit project_commits_path(project, branch_name)
end
+ it 'includes a date on which the commits were authored' do
+ commits = project.repository.commits(branch_name, limit: 40)
+ commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, _daily_commits|
+ expect(page).to have_content(day.strftime("%b %d, %Y"))
+ end
+ end
+
it 'includes the committed_date for each commit' do
commits = project.repository.commits(branch_name, limit: 40)
diff --git a/spec/features/merge_request/user_sees_merge_request_file_tree_sidebar_spec.rb b/spec/features/merge_request/user_sees_merge_request_file_tree_sidebar_spec.rb
new file mode 100644
index 00000000000..c385def6762
--- /dev/null
+++ b/spec/features/merge_request/user_sees_merge_request_file_tree_sidebar_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Merge request > User sees merge request file tree sidebar', :js, feature_category: :code_review_workflow do
+ include MergeRequestDiffHelpers
+
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
+ let(:user) { project.creator }
+ let(:sidebar) { find('.diff-tree-list') }
+ let(:sidebar_scroller) { sidebar.find('.vue-recycle-scroller') }
+
+ before do
+ sign_in(user)
+ visit diffs_project_merge_request_path(project, merge_request)
+ wait_for_requests
+ scroll_into_view
+ end
+
+ it 'sees file tree sidebar' do
+ expect(page).to have_selector('.file-row[role=button]')
+ end
+
+ # TODO: fix this test
+ # For some reason the browser in CI doesn't update the file tree sidebar when review bar is shown
+ # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118378#note_1403906356
+ #
+ # it 'has last entry visible with discussions enabled' do
+ # add_diff_line_draft_comment('foo', find('.line_holder', match: :first))
+ # scroll_into_view
+ # scroll_to_end
+ # button = find_all('.file-row[role=button]').last
+ # expect(button.obscured?).to be_falsy
+ # end
+
+ shared_examples 'shows last visible file in sidebar' do
+ it 'shows last file' do
+ scroll_to_end
+ button = find_all('.file-row[role=button]').last
+ title = button.find('[data-testid=file-row-name-container]')[:title]
+ button.click
+ expect(page).to have_selector(".file-title-name[title*=\"#{title}\"]")
+ end
+ end
+
+ it_behaves_like 'shows last visible file in sidebar'
+
+ context 'when viewing using file-by-file mode' do
+ let(:user) { create(:user, view_diffs_file_by_file: true) }
+
+ it_behaves_like 'shows last visible file in sidebar'
+ end
+
+ def scroll_into_view
+ sidebar.execute_script("this.scrollIntoView({ block: 'end' })")
+ end
+
+ def scroll_to_end
+ sidebar_scroller.execute_script('this.scrollBy(0,99999)')
+ end
+end
diff --git a/spec/fixtures/emails/service_desk_custom_email_address_verification.eml b/spec/fixtures/emails/service_desk_custom_email_address_verification.eml
new file mode 100644
index 00000000000..a5a17589a34
--- /dev/null
+++ b/spec/fixtures/emails/service_desk_custom_email_address_verification.eml
@@ -0,0 +1,31 @@
+Delivered-To: support+project_slug-project_id-issue-@example.com
+Received: by 2002:a05:7022:aa3:b0:5d:66:2e64 with SMTP id dd35csp3394266dlb; Mon, 23 Jan 2023 08:50:49 -0800 (PST)
+X-Received: by 2002:a19:a40e:0:b0:4c8:d65:da81 with SMTP id q14-20020a19a40e000000b004c80d65da81mr9022372lfc.60.1674492649184; Mon, 23 Jan 2023 08:50:49 -0800 (PST)
+Received: from mail-sor-f41.google.com (mail-sor-f41.google.com. [209.85.220.41]) by mx.google.com with SMTPS id t20-20020a195f14000000b00499004f4b1asor10121263lfb.188.2023.01.23.08.50.48 for <support+project_slug-project_id-issue-@example.com> (Google Transport Security); Mon, 23 Jan 2023 08:50:49 -0800 (PST)
+X-Received: by 2002:a05:6512:224c:b0:4cc:7937:fa04 with SMTP id i12-20020a056512224c00b004cc7937fa04mr1421048lfu.378.1674492648772; Mon, 23 Jan 2023 08:50:48 -0800 (PST)
+X-Forwarded-To: support+project_slug-project_id-issue-@example.com
+X-Forwarded-For: custom-support-email@example.com support+project_slug-project_id-issue-@example.com
+Return-Path: <custom-support-email@example.com>
+Received: from gmail.com ([94.31.107.53]) by smtp.gmail.com with ESMTPSA id t13-20020a1c770d000000b003db0ee277b2sm11097876wmi.5.2023.01.23.08.50.47 for <fatjuiceofficial+verify@gmail.com> (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 23 Jan 2023 08:50:47 -0800 (PST)
+From: Flight Support <custom-support-email@example.com>
+X-Google-Original-From: Flight Support <example@example.com>
+Date: Mon, 23 Jan 2023 17:50:46 +0100
+Reply-To: GitLab <noreply@example.com>
+To: custom-support-email+verify@example.com
+Message-ID: <63d927a0e407c_5f8f3ac0267d@mail.gmail.com>
+Subject: Verify custom email address custom-support-email@example.com for Flight
+Mime-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 7bit
+Auto-Submitted: no
+X-Auto-Response-Suppress: All
+
+
+
+This email is auto-generated. It verifies the ownership of the entered Service Desk custom email address and
+correct functionality of email forwarding.
+
+Verification token: ZROT4ZZXA-Y6
+--
+
+You're receiving this email because of your account on 127.0.0.1.
diff --git a/spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js b/spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js
index cabbb5e1591..e519684bbc5 100644
--- a/spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js
+++ b/spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js
@@ -1,14 +1,17 @@
import { shallowMount } from '@vue/test-utils';
+import { GlAlert } from '@gitlab/ui';
import AbuseReportApp from '~/admin/abuse_report/components/abuse_report_app.vue';
import ReportHeader from '~/admin/abuse_report/components/report_header.vue';
import UserDetails from '~/admin/abuse_report/components/user_details.vue';
import ReportedContent from '~/admin/abuse_report/components/reported_content.vue';
import HistoryItems from '~/admin/abuse_report/components/history_items.vue';
+import { SUCCESS_ALERT } from '~/admin/abuse_report/constants';
import { mockAbuseReport } from '../mock_data';
describe('AbuseReportApp', () => {
let wrapper;
+ const findAlert = () => wrapper.findComponent(GlAlert);
const findReportHeader = () => wrapper.findComponent(ReportHeader);
const findUserDetails = () => wrapper.findComponent(UserDetails);
const findReportedContent = () => wrapper.findComponent(ReportedContent);
@@ -27,10 +30,44 @@ describe('AbuseReportApp', () => {
createComponent();
});
+ it('does not show the alert by default', () => {
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ describe('when emitting the showAlert event from the report header', () => {
+ const message = 'alert message';
+
+ beforeEach(() => {
+ findReportHeader().vm.$emit('showAlert', SUCCESS_ALERT, message);
+ });
+
+ it('shows the alert', () => {
+ expect(findAlert().exists()).toBe(true);
+ });
+
+ it('displays the message', () => {
+ expect(findAlert().text()).toBe(message);
+ });
+
+ it('sets the variant property', () => {
+ expect(findAlert().props('variant')).toBe(SUCCESS_ALERT);
+ });
+
+ describe('when dismissing the alert', () => {
+ beforeEach(() => {
+ findAlert().vm.$emit('dismiss');
+ });
+
+ it('hides the alert', () => {
+ expect(findAlert().exists()).toBe(false);
+ });
+ });
+ });
+
describe('ReportHeader', () => {
it('renders ReportHeader', () => {
expect(findReportHeader().props('user')).toBe(mockAbuseReport.user);
- expect(findReportHeader().props('actions')).toBe(mockAbuseReport.actions);
+ expect(findReportHeader().props('report')).toBe(mockAbuseReport.report);
});
describe('when no user is present', () => {
diff --git a/spec/frontend/admin/abuse_report/components/report_actions_spec.js b/spec/frontend/admin/abuse_report/components/report_actions_spec.js
new file mode 100644
index 00000000000..a1a78902b58
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/report_actions_spec.js
@@ -0,0 +1,158 @@
+import MockAdapter from 'axios-mock-adapter';
+import { GlDrawer } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import axios from '~/lib/utils/axios_utils';
+import {
+ HTTP_STATUS_OK,
+ HTTP_STATUS_UNPROCESSABLE_ENTITY,
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+} from '~/lib/utils/http_status';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import ReportActions from '~/admin/abuse_report/components/report_actions.vue';
+import {
+ ACTIONS_I18N,
+ SUCCESS_ALERT,
+ FAILED_ALERT,
+ ERROR_MESSAGE,
+} from '~/admin/abuse_report/constants';
+import { mockAbuseReport } from '../mock_data';
+
+describe('ReportActions', () => {
+ let wrapper;
+ let axiosMock;
+
+ const params = {
+ user_action: 'ban_user',
+ close: true,
+ comment: 'my comment',
+ reason: 'spam',
+ };
+
+ const { report } = mockAbuseReport;
+
+ const clickActionsButton = () => wrapper.findByTestId('actions-button').vm.$emit('click');
+ const isDrawerOpen = () => wrapper.findComponent(GlDrawer).props('open');
+ const findErrorFor = (id) => wrapper.findByTestId(id).find('.d-block.invalid-feedback');
+ const setCloseReport = (close) => wrapper.findByTestId('close').find('input').setChecked(close);
+ const setSelectOption = (id, value) =>
+ wrapper.findByTestId(`${id}-select`).find(`option[value=${value}]`).setSelected();
+ const selectAction = (action) => setSelectOption('action', action);
+ const selectReason = (reason) => setSelectOption('reason', reason);
+ const setComment = (comment) => wrapper.findByTestId('comment').find('input').setValue(comment);
+ const submitForm = () => wrapper.findByTestId('submit-button').vm.$emit('click');
+
+ const createComponent = (props = {}) => {
+ wrapper = mountExtended(ReportActions, {
+ propsData: {
+ report,
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ axiosMock = new MockAdapter(axios);
+ createComponent();
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+ });
+
+ it('initially hides the drawer', () => {
+ expect(isDrawerOpen()).toBe(false);
+ });
+
+ describe('when clicking the actions button', () => {
+ beforeEach(() => {
+ clickActionsButton();
+ });
+
+ it('shows the drawer', () => {
+ expect(isDrawerOpen()).toBe(true);
+ });
+
+ describe.each`
+ input | errorFor | messageShown
+ ${null} | ${'action'} | ${true}
+ ${null} | ${'reason'} | ${true}
+ ${'close'} | ${'action'} | ${false}
+ ${'action'} | ${'action'} | ${false}
+ ${'reason'} | ${'reason'} | ${false}
+ `('when submitting an invalid form', ({ input, errorFor, messageShown }) => {
+ describe(`when ${
+ input ? `providing a value for the ${input} field` : 'not providing any values'
+ }`, () => {
+ beforeEach(() => {
+ submitForm();
+
+ if (input === 'close') {
+ setCloseReport(params.close);
+ } else if (input === 'action') {
+ selectAction(params.user_action);
+ } else if (input === 'reason') {
+ selectReason(params.reason);
+ }
+ });
+
+ it(`${messageShown ? 'shows' : 'hides'} ${errorFor} error message`, () => {
+ if (messageShown) {
+ expect(findErrorFor(errorFor).text()).toBe(ACTIONS_I18N.requiredFieldFeedback);
+ } else {
+ expect(findErrorFor(errorFor).exists()).toBe(false);
+ }
+ });
+ });
+ });
+
+ describe('when submitting a valid form', () => {
+ describe.each`
+ response | success | responseStatus | responseData | alertType | alertMessage
+ ${'successful'} | ${true} | ${HTTP_STATUS_OK} | ${{ message: 'success!' }} | ${SUCCESS_ALERT} | ${'success!'}
+ ${'custom failure'} | ${false} | ${HTTP_STATUS_UNPROCESSABLE_ENTITY} | ${{ message: 'fail!' }} | ${FAILED_ALERT} | ${'fail!'}
+ ${'generic failure'} | ${false} | ${HTTP_STATUS_INTERNAL_SERVER_ERROR} | ${{}} | ${FAILED_ALERT} | ${ERROR_MESSAGE}
+ `(
+ 'when the server responds with a $response response',
+ ({ success, responseStatus, responseData, alertType, alertMessage }) => {
+ beforeEach(async () => {
+ jest.spyOn(axios, 'put');
+
+ axiosMock.onPut(report.updatePath).replyOnce(responseStatus, responseData);
+
+ selectAction(params.user_action);
+ setCloseReport(params.close);
+ selectReason(params.reason);
+ setComment(params.comment);
+
+ await nextTick();
+
+ submitForm();
+
+ await waitForPromises();
+ });
+
+ it('does a put call with the right data', () => {
+ expect(axios.put).toHaveBeenCalledWith(report.updatePath, params);
+ });
+
+ it('closes the drawer', () => {
+ expect(isDrawerOpen()).toBe(false);
+ });
+
+ it('emits the showAlert event', () => {
+ expect(wrapper.emitted('showAlert')).toStrictEqual([[alertType, alertMessage]]);
+ });
+
+ it(`${success ? 'does' : 'does not'} emit the closeReport event`, () => {
+ if (success) {
+ expect(wrapper.emitted('closeReport')).toBeDefined();
+ } else {
+ expect(wrapper.emitted('closeReport')).toBeUndefined();
+ }
+ });
+ },
+ );
+ });
+ });
+});
diff --git a/spec/frontend/admin/abuse_report/components/report_header_spec.js b/spec/frontend/admin/abuse_report/components/report_header_spec.js
index d584cab05b3..f22f3af091f 100644
--- a/spec/frontend/admin/abuse_report/components/report_header_spec.js
+++ b/spec/frontend/admin/abuse_report/components/report_header_spec.js
@@ -1,25 +1,27 @@
-import { GlAvatar, GlLink, GlButton } from '@gitlab/ui';
+import { GlBadge, GlIcon, GlAvatar, GlLink, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import ReportHeader from '~/admin/abuse_report/components/report_header.vue';
-import AbuseReportActions from '~/admin/abuse_reports/components/abuse_report_actions.vue';
-import { REPORT_HEADER_I18N } from '~/admin/abuse_report/constants';
+import ReportActions from '~/admin/abuse_report/components/report_actions.vue';
+import { REPORT_HEADER_I18N, STATUS_OPEN, STATUS_CLOSED } from '~/admin/abuse_report/constants';
import { mockAbuseReport } from '../mock_data';
describe('ReportHeader', () => {
let wrapper;
- const { user, actions } = mockAbuseReport;
+ const { user, report } = mockAbuseReport;
+ const findBadge = () => wrapper.findComponent(GlBadge);
+ const findIcon = () => wrapper.findComponent(GlIcon);
const findAvatar = () => wrapper.findComponent(GlAvatar);
const findLink = () => wrapper.findComponent(GlLink);
const findButton = () => wrapper.findComponent(GlButton);
- const findActions = () => wrapper.findComponent(AbuseReportActions);
+ const findActions = () => wrapper.findComponent(ReportActions);
const createComponent = (props = {}) => {
wrapper = shallowMount(ReportHeader, {
propsData: {
user,
- actions,
+ report,
...props,
},
});
@@ -51,9 +53,42 @@ describe('ReportHeader', () => {
expect(button.text()).toBe(REPORT_HEADER_I18N.adminProfile);
});
+ describe.each`
+ status | text | variant | className | badgeIcon
+ ${STATUS_OPEN} | ${REPORT_HEADER_I18N[STATUS_OPEN]} | ${'success'} | ${'issuable-status-badge-open'} | ${'issues'}
+ ${STATUS_CLOSED} | ${REPORT_HEADER_I18N[STATUS_CLOSED]} | ${'info'} | ${'issuable-status-badge-closed'} | ${'issue-closed'}
+ `(
+ 'rendering the report $status status badge',
+ ({ status, text, variant, className, badgeIcon }) => {
+ beforeEach(() => {
+ createComponent({ report: { ...report, status } });
+ });
+
+ it(`indicates the ${status} status`, () => {
+ expect(findBadge().text()).toBe(text);
+ });
+
+ it(`with the ${variant} variant`, () => {
+ expect(findBadge().props('variant')).toBe(variant);
+ });
+
+ it(`with the text '${text}' as 'aria-label'`, () => {
+ expect(findBadge().attributes('aria-label')).toBe(text);
+ });
+
+ it(`contains the ${className} class`, () => {
+ expect(findBadge().element.classList).toContain(className);
+ });
+
+ it(`has an icon with the ${badgeIcon} name`, () => {
+ expect(findIcon().props('name')).toBe(badgeIcon);
+ });
+ },
+ );
+
it('renders the actions', () => {
const actionsComponent = findActions();
- expect(actionsComponent.props('report')).toMatchObject(actions);
+ expect(actionsComponent.props('report')).toMatchObject(report);
});
});
diff --git a/spec/frontend/admin/abuse_report/mock_data.js b/spec/frontend/admin/abuse_report/mock_data.js
index ee0f0967735..8c0ae223c87 100644
--- a/spec/frontend/admin/abuse_report/mock_data.js
+++ b/spec/frontend/admin/abuse_report/mock_data.js
@@ -40,6 +40,7 @@ export const mockAbuseReport = {
path: '/reporter',
},
report: {
+ status: 'open',
message: 'This is obvious spam',
reportedAt: '2023-03-29T09:39:50.502Z',
category: 'spam',
@@ -49,13 +50,6 @@ export const mockAbuseReport = {
url: 'http://localhost:3000/spamuser417/project/-/merge_requests/1#note_1375',
screenshot:
'/uploads/-/system/abuse_report/screenshot/27/Screenshot_2023-03-30_at_16.56.37.png',
- },
- actions: {
- reportedUser: { name: 'Sp4m User', createdAt: '2023-03-29T09:30:23.885Z' },
- userBlocked: false,
- blockUserPath: '/admin/users/spamuser417/block',
- removeReportPath: '/admin/abuse_reports/27',
- removeUserAndReportPath: '/admin/abuse_reports/27?remove_user=true',
- redirectPath: '/admin/abuse_reports',
+ updatePath: '/admin/abuse_reports/27',
},
};
diff --git a/spec/frontend/admin/abuse_reports/components/abuse_report_actions_spec.js b/spec/frontend/admin/abuse_reports/components/abuse_report_actions_spec.js
deleted file mode 100644
index bc648e52fad..00000000000
--- a/spec/frontend/admin/abuse_reports/components/abuse_report_actions_spec.js
+++ /dev/null
@@ -1,202 +0,0 @@
-import { nextTick } from 'vue';
-import axios from 'axios';
-import MockAdapter from 'axios-mock-adapter';
-import { GlDisclosureDropdown, GlDisclosureDropdownItem, GlModal } from '@gitlab/ui';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-import AbuseReportActions from '~/admin/abuse_reports/components/abuse_report_actions.vue';
-import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
-import { redirectTo, refreshCurrentPage } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
-import { createAlert, VARIANT_SUCCESS } from '~/alert';
-import { sprintf } from '~/locale';
-import { ACTIONS_I18N } from '~/admin/abuse_reports/constants';
-import { mockAbuseReports } from '../mock_data';
-
-jest.mock('~/alert');
-jest.mock('~/lib/utils/url_utility');
-
-describe('AbuseReportActions', () => {
- let wrapper;
-
- const findRemoveUserAndReportButton = () => wrapper.findByText('Remove user & report');
- const findBlockUserButton = () => wrapper.findByTestId('block-user-button');
- const findRemoveReportButton = () => wrapper.findByText('Remove report');
- const findConfirmationModal = () => wrapper.findComponent(GlModal);
-
- const report = mockAbuseReports[0];
-
- const createComponent = (props = {}) => {
- wrapper = mountExtended(AbuseReportActions, {
- propsData: {
- report,
- ...props,
- },
- stubs: {
- GlDisclosureDropdown,
- GlDisclosureDropdownItem,
- },
- });
- };
-
- describe('default', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('displays "Block user", "Remove user & report", and "Remove report" buttons', () => {
- expect(findRemoveUserAndReportButton().text()).toBe(ACTIONS_I18N.removeUserAndReport);
-
- const blockButton = findBlockUserButton();
- expect(blockButton.text()).toBe(ACTIONS_I18N.blockUser);
- expect(blockButton.attributes('disabled')).toBeUndefined();
-
- expect(findRemoveReportButton().text()).toBe(ACTIONS_I18N.removeReport);
- });
-
- it('does not show the confirmation modal initially', () => {
- expect(findConfirmationModal().props('visible')).toBe(false);
- });
- });
-
- describe('block button when user is already blocked', () => {
- it('is disabled and has the correct text', () => {
- createComponent({ report: { ...report, userBlocked: true } });
-
- const button = findBlockUserButton();
- expect(button.text()).toBe(ACTIONS_I18N.alreadyBlocked);
- expect(button.attributes('disabled')).toBeDefined();
- });
- });
-
- describe('actions', () => {
- let axiosMock;
-
- beforeEach(() => {
- axiosMock = new MockAdapter(axios);
-
- createComponent();
- });
-
- afterEach(() => {
- axiosMock.restore();
- createAlert.mockClear();
- });
-
- describe('on remove user and report', () => {
- it('shows confirmation modal and reloads the page on success', async () => {
- findRemoveUserAndReportButton().trigger('click');
- await nextTick();
-
- expect(findConfirmationModal().props()).toMatchObject({
- visible: true,
- title: sprintf(ACTIONS_I18N.removeUserAndReportConfirm, {
- user: report.reportedUser.name,
- }),
- });
-
- axiosMock.onDelete(report.removeUserAndReportPath).reply(HTTP_STATUS_OK);
-
- findConfirmationModal().vm.$emit('primary');
- await axios.waitForAll();
-
- expect(refreshCurrentPage).toHaveBeenCalled();
- });
-
- describe('when a redirect path is present', () => {
- beforeEach(() => {
- createComponent({ report: { ...report, redirectPath: '/redirect_path' } });
- });
-
- it('redirects to the given path', async () => {
- findRemoveUserAndReportButton().trigger('click');
- await nextTick();
-
- axiosMock.onDelete(report.removeUserAndReportPath).reply(HTTP_STATUS_OK);
-
- findConfirmationModal().vm.$emit('primary');
- await axios.waitForAll();
-
- expect(redirectTo).toHaveBeenCalledWith('/redirect_path'); // eslint-disable-line import/no-deprecated
- });
- });
- });
-
- describe('on block user', () => {
- beforeEach(async () => {
- findBlockUserButton().trigger('click');
- await nextTick();
- });
-
- it('shows confirmation modal', () => {
- expect(findConfirmationModal().props()).toMatchObject({
- visible: true,
- title: ACTIONS_I18N.blockUserConfirm,
- });
- });
-
- describe.each([
- {
- responseData: { notice: 'Notice' },
- createAlertArgs: { message: 'Notice', variant: VARIANT_SUCCESS },
- blockButtonText: ACTIONS_I18N.alreadyBlocked,
- blockButtonDisabled: 'disabled',
- },
- {
- responseData: { error: 'Error' },
- createAlertArgs: { message: 'Error' },
- blockButtonText: ACTIONS_I18N.blockUser,
- blockButtonDisabled: undefined,
- },
- ])(
- 'when response JSON is $responseData',
- ({ responseData, createAlertArgs, blockButtonText, blockButtonDisabled }) => {
- beforeEach(async () => {
- axiosMock.onPut(report.blockUserPath).reply(HTTP_STATUS_OK, responseData);
-
- findConfirmationModal().vm.$emit('primary');
- await axios.waitForAll();
- });
-
- it('updates the block button correctly', () => {
- const button = findBlockUserButton();
- expect(button.text()).toBe(blockButtonText);
- expect(button.attributes('disabled')).toBe(blockButtonDisabled);
- });
-
- it('displays the returned message', () => {
- expect(createAlert).toHaveBeenCalledWith(createAlertArgs);
- });
- },
- );
- });
-
- describe('on remove report', () => {
- it('reloads the page on success', async () => {
- axiosMock.onDelete(report.removeReportPath).reply(HTTP_STATUS_OK);
-
- findRemoveReportButton().trigger('click');
-
- expect(findConfirmationModal().props('visible')).toBe(false);
-
- await axios.waitForAll();
-
- expect(refreshCurrentPage).toHaveBeenCalled();
- });
-
- describe('when a redirect path is present', () => {
- beforeEach(() => {
- createComponent({ report: { ...report, redirectPath: '/redirect_path' } });
- });
-
- it('redirects to the given path', async () => {
- axiosMock.onDelete(report.removeReportPath).reply(HTTP_STATUS_OK);
-
- findRemoveReportButton().trigger('click');
-
- await axios.waitForAll();
-
- expect(redirectTo).toHaveBeenCalledWith('/redirect_path'); // eslint-disable-line import/no-deprecated
- });
- });
- });
- });
-});
diff --git a/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js
index 309e5f76b9c..85eafa9e85c 100644
--- a/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js
@@ -64,13 +64,12 @@ describe('content_editor/components/bubble_menus/bubble_menu', () => {
tippyOptions: expect.objectContaining({
onHidden: expect.any(Function),
onShow: expect.any(Function),
- appendTo: expect.any(Function),
+ strategy: 'fixed',
maxWidth: 'auto',
...tippyOptions,
}),
});
- expect(BubbleMenuPlugin.mock.calls[0][0].tippyOptions.appendTo()).toBe(document.body);
expect(tiptapEditor.registerPlugin).toHaveBeenCalledWith(pluginInitializationResult);
});
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index 44dd328025a..59b46e95c45 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -95,7 +95,7 @@ describe('ContentEditor', () => {
it('renders footer containing quick actions help text if quick actions docs path is defined', () => {
createWrapper({ quickActionsDocsPath: '/foo/bar' });
- expect(findEditorElement().text()).toContain('For quick actions, type /');
+ expect(wrapper.text()).toContain('For quick actions, type /');
expect(wrapper.findComponent(GlLink).attributes('href')).toBe('/foo/bar');
});
diff --git a/spec/frontend/content_editor/extensions/description_item_spec.js b/spec/frontend/content_editor/extensions/description_item_spec.js
new file mode 100644
index 00000000000..02b80d93886
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/description_item_spec.js
@@ -0,0 +1,121 @@
+import DescriptionList from '~/content_editor/extensions/description_list';
+import DescriptionItem from '~/content_editor/extensions/description_item';
+import { createTestEditor, createDocBuilder, triggerKeyboardInput } from '../test_utils';
+
+describe('content_editor/extensions/description_item', () => {
+ let tiptapEditor;
+ let doc;
+ let p;
+ let descriptionList;
+ let descriptionItem;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [DescriptionList, DescriptionItem] });
+
+ ({
+ builders: { doc, p, descriptionList, descriptionItem },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ descriptionList: { nodeType: DescriptionList.name },
+ descriptionItem: { nodeType: DescriptionItem.name },
+ },
+ }));
+ });
+
+ describe('shortcut: Enter', () => {
+ it('splits a description item into two items', () => {
+ const initialDoc = doc(descriptionList(descriptionItem(p('Description item'))));
+ const expectedDoc = doc(
+ descriptionList(descriptionItem(p('Descrip')), descriptionItem(p('tion item'))),
+ );
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+ tiptapEditor.commands.setTextSelection(10);
+ tiptapEditor.commands.keyboardShortcut('Enter');
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
+
+ describe('shortcut: Tab', () => {
+ it('converts a description term into a description details', () => {
+ const initialDoc = doc(descriptionList(descriptionItem(p('Description item'))));
+ const expectedDoc = doc(
+ descriptionList(descriptionItem({ isTerm: false }, p('Description item'))),
+ );
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+ tiptapEditor.commands.setTextSelection(10);
+ tiptapEditor.commands.keyboardShortcut('Tab');
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+
+ it('has no effect on a description details', () => {
+ const initialDoc = doc(
+ descriptionList(descriptionItem({ isTerm: false }, p('Description item'))),
+ );
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+ tiptapEditor.commands.setTextSelection(10);
+ tiptapEditor.commands.keyboardShortcut('Tab');
+
+ expect(tiptapEditor.getJSON()).toEqual(initialDoc.toJSON());
+ });
+ });
+
+ describe('shortcut: Shift-Tab', () => {
+ it('converts a description details into a description term', () => {
+ const initialDoc = doc(
+ descriptionList(
+ descriptionItem({ isTerm: false }, p('Description item')),
+ descriptionItem(p('Description item')),
+ descriptionItem(p('Description item')),
+ ),
+ );
+ const expectedDoc = doc(
+ descriptionList(
+ descriptionItem(p('Description item')),
+ descriptionItem(p('Description item')),
+ descriptionItem(p('Description item')),
+ ),
+ );
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+ tiptapEditor.commands.setTextSelection(10);
+ tiptapEditor.commands.keyboardShortcut('Shift-Tab');
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+
+ it('lifts a description term', () => {
+ const initialDoc = doc(descriptionList(descriptionItem(p('Description item'))));
+ const expectedDoc = doc(p('Description item'));
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+ tiptapEditor.commands.setTextSelection(10);
+ tiptapEditor.commands.keyboardShortcut('Shift-Tab');
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
+
+ describe('capturing keyboard events', () => {
+ it.each`
+ key | shiftKey | nodeActive | captured | description
+ ${'Tab'} | ${false} | ${true} | ${true} | ${'captures Tab key when cursor is inside a description item'}
+ ${'Tab'} | ${false} | ${false} | ${false} | ${'does not capture Tab key when cursor is not inside a description item'}
+ ${'Tab'} | ${true} | ${true} | ${true} | ${'captures Shift-Tab key when cursor is inside a description item'}
+ ${'Tab'} | ${true} | ${false} | ${false} | ${'does not capture Shift-Tab key when cursor is not inside a description item'}
+ `('$description', ({ key, shiftKey, nodeActive, captured }) => {
+ const initialDoc = doc(descriptionList(descriptionItem(p('Text content'))));
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+
+ jest.spyOn(tiptapEditor, 'isActive').mockReturnValue(nodeActive);
+
+ expect(triggerKeyboardInput({ tiptapEditor, key, shiftKey })).toBe(captured);
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/description_list_spec.js b/spec/frontend/content_editor/extensions/description_list_spec.js
new file mode 100644
index 00000000000..e46680956ec
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/description_list_spec.js
@@ -0,0 +1,36 @@
+import DescriptionList from '~/content_editor/extensions/description_list';
+import DescriptionItem from '~/content_editor/extensions/description_item';
+import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
+
+describe('content_editor/extensions/description_list', () => {
+ let tiptapEditor;
+ let doc;
+ let p;
+ let descriptionList;
+ let descriptionItem;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [DescriptionList, DescriptionItem] });
+
+ ({
+ builders: { doc, p, descriptionList, descriptionItem },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ descriptionList: { nodeType: DescriptionList.name },
+ descriptionItem: { nodeType: DescriptionItem.name },
+ },
+ }));
+ });
+
+ it.each`
+ inputRuleText | insertedNode | insertedNodeType
+ ${'<dl>'} | ${() => descriptionList(descriptionItem(p()))} | ${'descriptionList'}
+ ${'<dl'} | ${() => p()} | ${'paragraph'}
+ ${'dl>'} | ${() => p()} | ${'paragraph'}
+ `('with input=$input, it inserts a $insertedNodeType node', ({ inputRuleText, insertedNode }) => {
+ triggerNodeInputRule({ tiptapEditor, inputRuleText });
+
+ expect(tiptapEditor.getJSON()).toEqual(doc(insertedNode()).toJSON());
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/details_content_spec.js b/spec/frontend/content_editor/extensions/details_content_spec.js
index 575f3bf65e4..02e2b51366a 100644
--- a/spec/frontend/content_editor/extensions/details_content_spec.js
+++ b/spec/frontend/content_editor/extensions/details_content_spec.js
@@ -1,6 +1,6 @@
import Details from '~/content_editor/extensions/details';
import DetailsContent from '~/content_editor/extensions/details_content';
-import { createTestEditor, createDocBuilder } from '../test_utils';
+import { createTestEditor, createDocBuilder, triggerKeyboardInput } from '../test_utils';
describe('content_editor/extensions/details_content', () => {
let tiptapEditor;
@@ -42,7 +42,6 @@ describe('content_editor/extensions/details_content', () => {
);
tiptapEditor.commands.setContent(initialDoc.toJSON());
-
tiptapEditor.commands.setTextSelection(10);
tiptapEditor.commands.keyboardShortcut('Enter');
@@ -66,11 +65,26 @@ describe('content_editor/extensions/details_content', () => {
);
tiptapEditor.commands.setContent(initialDoc.toJSON());
-
tiptapEditor.commands.setTextSelection(20);
tiptapEditor.commands.keyboardShortcut('Shift-Tab');
expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
});
});
+
+ describe('capturing keyboard events', () => {
+ it.each`
+ key | shiftKey | nodeActive | captured | description
+ ${'Tab'} | ${true} | ${true} | ${true} | ${'captures Shift-Tab key when cursor is inside a details content'}
+ ${'Tab'} | ${true} | ${false} | ${false} | ${'does not capture Shift-Tab key when cursor is not inside a details content'}
+ `('$description', ({ key, shiftKey, nodeActive, captured }) => {
+ const initialDoc = doc(details(detailsContent(p('Text content'))));
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+
+ jest.spyOn(tiptapEditor, 'isActive').mockReturnValue(nodeActive);
+
+ expect(triggerKeyboardInput({ tiptapEditor, key, shiftKey })).toBe(captured);
+ });
+ });
});
diff --git a/spec/frontend/content_editor/extensions/details_spec.js b/spec/frontend/content_editor/extensions/details_spec.js
index cd59943982f..ce97444ec19 100644
--- a/spec/frontend/content_editor/extensions/details_spec.js
+++ b/spec/frontend/content_editor/extensions/details_spec.js
@@ -1,6 +1,6 @@
import Details from '~/content_editor/extensions/details';
import DetailsContent from '~/content_editor/extensions/details_content';
-import { createTestEditor, createDocBuilder } from '../test_utils';
+import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
describe('content_editor/extensions/details', () => {
let tiptapEditor;
@@ -75,18 +75,13 @@ describe('content_editor/extensions/details', () => {
});
it.each`
- input | insertedNode
- ${'<details>'} | ${(...args) => details(detailsContent(p(...args)))}
- ${'<details'} | ${(...args) => p(...args)}
- ${'details>'} | ${(...args) => p(...args)}
- `('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
- const { view } = tiptapEditor;
- const { selection } = view.state;
- const expectedDoc = doc(insertedNode());
-
- // Triggers the event handler that input rules listen to
- view.someProp('handleTextInput', (f) => f(view, selection.from, selection.to, input));
-
- expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ inputRuleText | insertedNode | insertedNodeType
+ ${'<details>'} | ${() => details(detailsContent(p()))} | ${'details'}
+ ${'<details'} | ${() => p()} | ${'paragraph'}
+ ${'details>'} | ${() => p()} | ${'paragraph'}
+ `('with input=$input, it inserts a $insertedNodeType node', ({ inputRuleText, insertedNode }) => {
+ triggerNodeInputRule({ tiptapEditor, inputRuleText });
+
+ expect(tiptapEditor.getJSON()).toEqual(doc(insertedNode()).toJSON());
});
});
diff --git a/spec/frontend/content_editor/test_utils.js b/spec/frontend/content_editor/test_utils.js
index 802ea49631f..9357381c053 100644
--- a/spec/frontend/content_editor/test_utils.js
+++ b/spec/frontend/content_editor/test_utils.js
@@ -192,6 +192,15 @@ export const triggerMarkInputRule = ({ tiptapEditor, inputRuleText }) => {
);
};
+export const triggerKeyboardInput = ({ tiptapEditor, key, shiftKey = false }) => {
+ let isCaptured = false;
+ tiptapEditor.view.someProp('handleKeyDown', (f) => {
+ isCaptured = f(tiptapEditor.view, new KeyboardEvent('keydown', { key, shiftKey }));
+ return isCaptured;
+ });
+ return isCaptured;
+};
+
/**
* Executes an action that triggers a transaction in the
* tiptap Editor. Returns a promise that resolves
diff --git a/spec/frontend/diffs/components/tree_list_spec.js b/spec/frontend/diffs/components/tree_list_spec.js
index 87c638d065a..1ec8547d325 100644
--- a/spec/frontend/diffs/components/tree_list_spec.js
+++ b/spec/frontend/diffs/components/tree_list_spec.js
@@ -3,6 +3,7 @@ import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import TreeList from '~/diffs/components/tree_list.vue';
import createStore from '~/diffs/store/modules';
+import batchComments from '~/batch_comments/stores/modules/batch_comments';
import DiffFileRow from '~/diffs/components//diff_file_row.vue';
import { stubComponent } from 'helpers/stub_component';
@@ -38,6 +39,7 @@ describe('Diffs tree list component', () => {
store = new Vuex.Store({
modules: {
diffs: createStore(),
+ batchComments: batchComments(),
},
});
diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js
index cdfe8b02b48..0f70b264326 100644
--- a/spec/frontend/notes/components/notes_app_spec.js
+++ b/spec/frontend/notes/components/notes_app_spec.js
@@ -334,14 +334,12 @@ describe('note_app', () => {
});
it('should listen hashchange event', () => {
- const notesApp = wrapper.findComponent(NotesApp);
const hash = 'some dummy hash';
jest.spyOn(urlUtility, 'getLocationHash').mockReturnValue(hash);
- const setTargetNoteHash = jest.spyOn(notesApp.vm, 'setTargetNoteHash');
-
+ const dispatchMock = jest.spyOn(store, 'dispatch');
window.dispatchEvent(new Event('hashchange'), hash);
- expect(setTargetNoteHash).toHaveBeenCalled();
+ expect(dispatchMock).toHaveBeenCalledWith('setTargetNoteHash', 'some dummy hash');
});
});
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
index 86e4e88e3cf..7f6ecbac748 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
@@ -18,6 +18,7 @@ describe('ServiceDeskRoot', () => {
endpoint: '/gitlab-org/gitlab-test/service_desk',
initialIncomingEmail: 'servicedeskaddress@example.com',
initialIsEnabled: true,
+ isIssueTrackerEnabled: true,
outgoingName: 'GitLab Support Bot',
projectKey: 'key',
selectedTemplate: 'Bug',
@@ -59,6 +60,7 @@ describe('ServiceDeskRoot', () => {
initialSelectedTemplate: provideData.selectedTemplate,
initialSelectedFileTemplateProjectId: provideData.selectedFileTemplateProjectId,
isEnabled: provideData.initialIsEnabled,
+ isIssueTrackerEnabled: provideData.isIssueTrackerEnabled,
isTemplateSaving: false,
templates: provideData.templates,
});
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
index 84eafc3d0f3..5631927cc2f 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
@@ -1,7 +1,8 @@
-import { GlButton, GlDropdown, GlLoadingIcon, GlToggle } from '@gitlab/ui';
+import { GlButton, GlDropdown, GlLoadingIcon, GlToggle, GlAlert } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { helpPagePath } from '~/helpers/help_page_helper';
import ServiceDeskSetting from '~/projects/settings_service_desk/components/service_desk_setting.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
@@ -16,17 +17,44 @@ describe('ServiceDeskSetting', () => {
const findTemplateDropdown = () => wrapper.findComponent(GlDropdown);
const findToggle = () => wrapper.findComponent(GlToggle);
const findSuffixFormGroup = () => wrapper.findByTestId('suffix-form-group');
+ const findIssueTrackerInfo = () => wrapper.findComponent(GlAlert);
+ const findIssueHelpLink = () => wrapper.findByTestId('issue-help-page');
const createComponent = ({ props = {} } = {}) =>
extendedWrapper(
mount(ServiceDeskSetting, {
propsData: {
isEnabled: true,
+ isIssueTrackerEnabled: true,
...props,
},
}),
);
+ describe('with issue tracker', () => {
+ it('does not show the info notice when enabled', () => {
+ wrapper = createComponent();
+
+ expect(findIssueTrackerInfo().exists()).toBe(false);
+ });
+
+ it('shows info notice when disabled with help page link', () => {
+ wrapper = createComponent({
+ props: {
+ isIssueTrackerEnabled: false,
+ },
+ });
+
+ expect(findIssueTrackerInfo().exists()).toBe(true);
+ expect(findIssueHelpLink().text()).toEqual('activate the issue tracker');
+ expect(findIssueHelpLink().attributes('href')).toBe(
+ helpPagePath('user/project/settings/index.md', {
+ anchor: 'configure-project-visibility-features-and-permissions',
+ }),
+ );
+ });
+ });
+
describe('when isEnabled=true', () => {
describe('only isEnabled', () => {
describe('as project admin', () => {
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_template_dropdown_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_template_dropdown_spec.js
index 7090db5cad7..1a76e7d1ec6 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_template_dropdown_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_template_dropdown_spec.js
@@ -14,6 +14,7 @@ describe('ServiceDeskTemplateDropdown', () => {
mount(ServiceDeskTemplateDropdown, {
propsData: {
isEnabled: true,
+ isIssueTrackerEnabled: true,
...props,
},
}),
diff --git a/spec/helpers/admin/abuse_reports_helper_spec.rb b/spec/helpers/admin/abuse_reports_helper_spec.rb
index 496b7361b6e..6a7630dc76a 100644
--- a/spec/helpers/admin/abuse_reports_helper_spec.rb
+++ b/spec/helpers/admin/abuse_reports_helper_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Admin::AbuseReportsHelper, feature_category: :insider_threat do
subject(:data) { helper.abuse_report_data(report)[:abuse_report_data] }
it 'has the expected attributes' do
- expect(data).to include('user', 'reporter', 'report', 'actions')
+ expect(data).to include('user', 'reporter', 'report')
end
end
end
diff --git a/spec/helpers/resource_events/abuse_report_events_helper_spec.rb b/spec/helpers/resource_events/abuse_report_events_helper_spec.rb
new file mode 100644
index 00000000000..f711fb6773c
--- /dev/null
+++ b/spec/helpers/resource_events/abuse_report_events_helper_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ResourceEvents::AbuseReportEventsHelper, feature_category: :instance_resiliency do
+ describe '#success_message_for_action' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:action, :action_value) do
+ ResourceEvents::AbuseReportEvent.actions.to_a
+ end
+
+ with_them do
+ it { expect(helper.success_message_for_action(action)).not_to be_nil }
+ end
+ end
+end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index c2c32abbdc4..372808b64d3 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -1571,12 +1571,7 @@ RSpec.describe Notify do
end
context 'when custom email is enabled' do
- let_it_be(:credentials) do
- create(
- :service_desk_custom_email_credential,
- project: project
- )
- end
+ let_it_be(:credentials) { create(:service_desk_custom_email_credential, project: project) }
let_it_be(:settings) do
create(
diff --git a/spec/models/resource_events/abuse_report_event_spec.rb b/spec/models/resource_events/abuse_report_event_spec.rb
index 1c709ae4f21..d454632c906 100644
--- a/spec/models/resource_events/abuse_report_event_spec.rb
+++ b/spec/models/resource_events/abuse_report_event_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe ResourceEvents::AbuseReportEvent, feature_category: :instance_resiliency, type: :model do
+ include ResourceEvents::AbuseReportEventsHelper
+
subject(:event) { build(:abuse_report_event) }
describe 'associations' do
@@ -14,4 +16,10 @@ RSpec.describe ResourceEvents::AbuseReportEvent, feature_category: :instance_res
it { is_expected.to be_valid }
it { is_expected.to validate_presence_of(:action) }
end
+
+ describe '#success_message' do
+ it 'returns a success message for the action' do
+ expect(event.success_message).to eq(success_message_for_action(event.action))
+ end
+ end
end
diff --git a/spec/requests/admin/abuse_reports_controller_spec.rb b/spec/requests/admin/abuse_reports_controller_spec.rb
index 0b5aaabaa61..8d033a2e147 100644
--- a/spec/requests/admin/abuse_reports_controller_spec.rb
+++ b/spec/requests/admin/abuse_reports_controller_spec.rb
@@ -57,13 +57,46 @@ RSpec.describe Admin::AbuseReportsController, type: :request, feature_category:
let(:report) { create(:abuse_report) }
let(:params) { { user_action: 'block_user', close: 'true', reason: 'spam', comment: 'obvious spam' } }
let(:expected_params) { ActionController::Parameters.new(params).permit! }
+ let(:message) { 'Service response' }
+
+ subject(:request) { put admin_abuse_report_path(report, params) }
it 'invokes the Admin::AbuseReportUpdateService' do
expect_next_instance_of(Admin::AbuseReportUpdateService, report, admin, expected_params) do |service|
- expect(service).to receive(:execute)
+ expect(service).to receive(:execute).and_call_original
end
- put admin_abuse_report_path(report, params)
+ request
+ end
+
+ context 'when the service response is a success' do
+ before do
+ allow_next_instance_of(Admin::AbuseReportUpdateService, report, admin, expected_params) do |service|
+ allow(service).to receive(:execute).and_return(ServiceResponse.success(message: message))
+ end
+
+ request
+ end
+
+ it 'returns the service response message with a success status' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['message']).to eq(message)
+ end
+ end
+
+ context 'when the service response is an error' do
+ before do
+ allow_next_instance_of(Admin::AbuseReportUpdateService, report, admin, expected_params) do |service|
+ allow(service).to receive(:execute).and_return(ServiceResponse.error(message: message))
+ end
+
+ request
+ end
+
+ it 'returns the service response message with a failed status' do
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(json_response['message']).to eq(message)
+ end
end
end
diff --git a/spec/requests/api/internal/kubernetes_spec.rb b/spec/requests/api/internal/kubernetes_spec.rb
index c07382a6e04..cd2c21fda29 100644
--- a/spec/requests/api/internal/kubernetes_spec.rb
+++ b/spec/requests/api/internal/kubernetes_spec.rb
@@ -337,6 +337,81 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :deployment_manageme
end
end
+ describe 'GET /internal/kubernetes/verify_project_access' do
+ def send_request(headers: {}, params: {})
+ get api("/internal/kubernetes/verify_project_access"), params: params, headers: headers.reverse_merge(jwt_auth_headers)
+ end
+
+ include_examples 'authorization'
+ include_examples 'agent authentication'
+ include_examples 'error handling'
+
+ shared_examples 'access is granted' do
+ it 'returns success response' do
+ send_request(params: { id: project_id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" })
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
+
+ shared_examples 'access is denied' do
+ it 'returns 404' do
+ send_request(params: { id: project_id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" })
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'an agent is found' do
+ let_it_be(:agent_token) { create(:cluster_agent_token) }
+ let(:project_id) { project.id }
+
+ include_examples 'agent token tracking'
+
+ context 'project is public' do
+ let(:project) { create(:project, :public) }
+
+ it_behaves_like 'access is granted'
+
+ context 'repository is for project members only' do
+ let(:project) { create(:project, :public, :repository_private) }
+
+ it_behaves_like 'access is denied'
+ end
+ end
+
+ context 'project is private' do
+ let(:project) { create(:project, :private) }
+
+ it_behaves_like 'access is denied'
+
+ context 'and agent belongs to project' do
+ let(:agent_token) { create(:cluster_agent_token, agent: create(:cluster_agent, project: project)) }
+
+ it_behaves_like 'access is granted'
+ end
+ end
+
+ context 'project is internal' do
+ let(:project) { create(:project, :internal) }
+
+ it_behaves_like 'access is denied'
+
+ context 'and agent belongs to project' do
+ let(:agent_token) { create(:cluster_agent_token, agent: create(:cluster_agent, project: project)) }
+
+ it_behaves_like 'access is granted'
+ end
+ end
+
+ context 'project does not exist' do
+ let(:project_id) { non_existing_record_id }
+
+ it_behaves_like 'access is denied'
+ end
+ end
+ end
+
describe 'POST /internal/kubernetes/authorize_proxy_user', :clean_gitlab_redis_sessions do
include SessionHelpers
diff --git a/spec/serializers/admin/abuse_report_details_entity_spec.rb b/spec/serializers/admin/abuse_report_details_entity_spec.rb
index 0e5e6a62ce1..08bfa57b062 100644
--- a/spec/serializers/admin/abuse_report_details_entity_spec.rb
+++ b/spec/serializers/admin/abuse_report_details_entity_spec.rb
@@ -21,8 +21,7 @@ RSpec.describe Admin::AbuseReportDetailsEntity, feature_category: :insider_threa
expect(entity_hash.keys).to include(
:user,
:reporter,
- :report,
- :actions
+ :report
)
end
@@ -127,31 +126,15 @@ RSpec.describe Admin::AbuseReportDetailsEntity, feature_category: :insider_threa
report_hash = entity_hash[:report]
expect(report_hash.keys).to match_array([
+ :status,
:message,
:reported_at,
:category,
:type,
:content,
:url,
- :screenshot
- ])
- end
-
- it 'correctly exposes `actions`', :aggregate_failures do
- actions_hash = entity_hash[:actions]
-
- expect(actions_hash.keys).to match_array([
- :user_blocked,
- :block_user_path,
- :remove_user_and_report_path,
- :remove_report_path,
- :reported_user,
- :redirect_path
- ])
-
- expect(actions_hash[:reported_user].keys).to match_array([
- :name,
- :created_at
+ :screenshot,
+ :update_path
])
end
end
diff --git a/spec/serializers/admin/abuse_report_details_serializer_spec.rb b/spec/serializers/admin/abuse_report_details_serializer_spec.rb
index f22d92a1763..a42c56c0921 100644
--- a/spec/serializers/admin/abuse_report_details_serializer_spec.rb
+++ b/spec/serializers/admin/abuse_report_details_serializer_spec.rb
@@ -12,8 +12,7 @@ RSpec.describe Admin::AbuseReportDetailsSerializer, feature_category: :insider_t
is_expected.to include(
:user,
:reporter,
- :report,
- :actions
+ :report
)
end
end
diff --git a/spec/services/admin/abuse_report_update_service_spec.rb b/spec/services/admin/abuse_report_update_service_spec.rb
index e85b516b87f..7069d8ee5c1 100644
--- a/spec/services/admin/abuse_report_update_service_spec.rb
+++ b/spec/services/admin/abuse_report_update_service_spec.rb
@@ -52,6 +52,10 @@ RSpec.describe Admin::AbuseReportUpdateService, feature_category: :instance_resi
comment: params[:comment]
)
end
+
+ it 'returns the event success message' do
+ expect(subject.message).to eq(abuse_report.events.last.success_message)
+ end
end
context 'when invalid parameters are given' do
@@ -194,6 +198,15 @@ RSpec.describe Admin::AbuseReportUpdateService, feature_category: :instance_resi
it_behaves_like 'closes the report'
it_behaves_like 'records an event', action: 'close_report'
+
+ context 'when report is already closed' do
+ before do
+ abuse_report.closed!
+ end
+
+ it_behaves_like 'returns an error response', 'Report already closed'
+ it_behaves_like 'does not record an event'
+ end
end
end
end
diff --git a/spec/services/service_desk/custom_email_verifications/create_service_spec.rb b/spec/services/service_desk/custom_email_verifications/create_service_spec.rb
new file mode 100644
index 00000000000..fceb6fc78b4
--- /dev/null
+++ b/spec/services/service_desk/custom_email_verifications/create_service_spec.rb
@@ -0,0 +1,139 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ServiceDesk::CustomEmailVerifications::CreateService, feature_category: :service_desk do
+ describe '#execute' do
+ let_it_be_with_reload(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ let!(:credential) { create(:service_desk_custom_email_credential, project: project) }
+
+ let(:message_delivery) { instance_double(ActionMailer::MessageDelivery) }
+ let(:message) { instance_double(Mail::Message) }
+
+ let(:service) { described_class.new(project: project, current_user: user) }
+
+ before do
+ allow(message_delivery).to receive(:deliver_later)
+ allow(Notify).to receive(:service_desk_verification_triggered_email).and_return(message_delivery)
+
+ # We send verification email directly
+ allow(message).to receive(:deliver)
+ allow(Notify).to receive(:service_desk_custom_email_verification_email).and_return(message)
+ end
+
+ shared_examples 'a verification process that exits early' do
+ it 'aborts verification process and exits early', :aggregate_failures do
+ # Because we exit early it should not send any verification or notification emails
+ expect(service).to receive(:setup_and_deliver_verification_email).exactly(0).times
+ expect(Notify).to receive(:service_desk_verification_triggered_email).exactly(0).times
+
+ response = service.execute
+
+ expect(response).to be_error
+ end
+ end
+
+ shared_examples 'a verification process with ramp up error' do |error, error_identifier|
+ it 'aborts verification process', :aggregate_failures do
+ allow(message).to receive(:deliver).and_raise(error)
+
+ # Creates one verification email
+ expect(Notify).to receive(:service_desk_custom_email_verification_email).once
+
+ # Correct amount of notification emails were sent
+ expect(Notify).to receive(:service_desk_verification_triggered_email).exactly(project.owners.size + 1).times
+
+ # Correct amount of result notification emails were sent
+ expect(Notify).to receive(:service_desk_verification_result_email).exactly(project.owners.size + 1).times
+
+ response = service.execute
+
+ expect(response).to be_error
+ expect(response.reason).to eq error_identifier
+
+ expect(settings).not_to be_custom_email_enabled
+ expect(settings.custom_email_verification.triggered_at).not_to be_nil
+ expect(settings.custom_email_verification).to have_attributes(
+ token: nil,
+ triggerer: user,
+ error: error_identifier,
+ state: 'failed'
+ )
+ end
+ end
+
+ it_behaves_like 'a verification process that exits early'
+
+ context 'when feature flag :service_desk_custom_email is disabled' do
+ before do
+ stub_feature_flags(service_desk_custom_email: false)
+ end
+
+ it_behaves_like 'a verification process that exits early'
+ end
+
+ context 'when service desk setting exists' do
+ let(:settings) { create(:service_desk_setting, project: project, custom_email: 'user@example.com') }
+ let(:service) { described_class.new(project: settings.project, current_user: user) }
+
+ it 'aborts verification process and exits early', :aggregate_failures do
+ # Because we exit early it should not send any verification or notification emails
+ expect(service).to receive(:setup_and_deliver_verification_email).exactly(0).times
+ expect(Notify).to receive(:service_desk_verification_triggered_email).exactly(0).times
+
+ response = service.execute
+ settings.reload
+
+ expect(response).to be_error
+
+ expect(settings.custom_email_enabled).to be false
+ # Because service should normally add initial verification object
+ expect(settings.custom_email_verification).to be nil
+ end
+
+ context 'when user has maintainer role in project' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'initiates verification process successfully', :aggregate_failures do
+ # Creates one verification email
+ expect(Notify).to receive(:service_desk_custom_email_verification_email).once
+
+ # Check whether the correct amount of notification emails were sent
+ expect(Notify).to receive(:service_desk_verification_triggered_email).exactly(project.owners.size + 1).times
+
+ response = service.execute
+
+ settings.reload
+ verification = settings.custom_email_verification
+
+ expect(response).to be_success
+
+ expect(settings.custom_email_enabled).to be false
+
+ expect(verification).to be_started
+ expect(verification.token).not_to be_nil
+ expect(verification.triggered_at).not_to be_nil
+ expect(verification).to have_attributes(
+ triggerer: user,
+ error: nil
+ )
+ end
+
+ context 'when providing invalid SMTP credentials' do
+ before do
+ allow(Notify).to receive(:service_desk_verification_result_email).and_return(message_delivery)
+ end
+
+ it_behaves_like 'a verification process with ramp up error', SocketError, 'smtp_host_issue'
+ it_behaves_like 'a verification process with ramp up error', OpenSSL::SSL::SSLError, 'smtp_host_issue'
+ it_behaves_like 'a verification process with ramp up error',
+ Net::SMTPAuthenticationError.new('Invalid username or password'), 'invalid_credentials'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/service_desk/custom_email_verifications/update_service_spec.rb b/spec/services/service_desk/custom_email_verifications/update_service_spec.rb
new file mode 100644
index 00000000000..f1e683c0185
--- /dev/null
+++ b/spec/services/service_desk/custom_email_verifications/update_service_spec.rb
@@ -0,0 +1,151 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ServiceDesk::CustomEmailVerifications::UpdateService, feature_category: :service_desk do
+ describe '#execute' do
+ let_it_be_with_reload(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ let!(:credential) { create(:service_desk_custom_email_credential, project: project) }
+ let(:settings) { create(:service_desk_setting, project: project, custom_email: 'custom-support-email@example.com') }
+
+ let(:mail_object) { nil }
+ let(:message_delivery) { instance_double(ActionMailer::MessageDelivery) }
+ let(:service) { described_class.new(project: settings.project, params: { mail: mail_object }) }
+
+ before do
+ allow(message_delivery).to receive(:deliver_later)
+ allow(Notify).to receive(:service_desk_verification_result_email).and_return(message_delivery)
+ end
+
+ shared_examples 'a failing verification process' do |expected_error_identifier|
+ it 'refuses to verify and sends result emails' do
+ expect(Notify).to receive(:service_desk_verification_result_email).twice
+
+ response = described_class.new(project: settings.project, params: { mail: mail_object }).execute
+
+ settings.reset
+ verification.reset
+
+ expect(response).to be_error
+ expect(settings).not_to be_custom_email_enabled
+ expect(verification).to be_failed
+
+ expect(response.reason).to eq expected_error_identifier
+ expect(verification.error).to eq expected_error_identifier
+ end
+ end
+
+ shared_examples 'an early exit from the verification process' do |expected_state|
+ it 'exits early' do
+ expect(Notify).to receive(:service_desk_verification_result_email).exactly(0).times
+
+ response = service.execute
+
+ settings.reset
+ verification.reset
+
+ expect(response).to be_error
+ expect(settings).not_to be_custom_email_enabled
+ expect(verification.state).to eq expected_state
+ end
+ end
+
+ it 'exits early' do
+ expect(Notify).to receive(:service_desk_verification_result_email).exactly(0).times
+
+ response = service.execute
+
+ settings.reset
+
+ expect(response).to be_error
+ expect(settings).not_to be_custom_email_enabled
+ end
+
+ context 'when feature flag :service_desk_custom_email is disabled' do
+ before do
+ stub_feature_flags(service_desk_custom_email: false)
+ end
+
+ it 'exits early' do
+ expect(Notify).to receive(:service_desk_verification_result_email).exactly(0).times
+
+ response = service.execute
+
+ expect(response).to be_error
+ end
+ end
+
+ context 'when verification exists' do
+ let!(:verification) { create(:service_desk_custom_email_verification, project: project) }
+
+ context 'when we do not have a verification email' do
+ # Raise if verification started but no email provided
+ it_behaves_like 'a failing verification process', 'mail_not_received_within_timeframe'
+
+ context 'when already verified' do
+ before do
+ verification.mark_as_finished!
+ end
+
+ it_behaves_like 'an early exit from the verification process', 'finished'
+ end
+
+ context 'when we already have an error' do
+ before do
+ verification.mark_as_failed!(:smtp_host_issue)
+ end
+
+ it_behaves_like 'an early exit from the verification process', 'failed'
+ end
+ end
+
+ context 'when we have a verification email' do
+ before do
+ verification.update!(token: 'ZROT4ZZXA-Y6') # token from email fixture
+ end
+
+ let(:email_raw) { email_fixture('emails/service_desk_custom_email_address_verification.eml') }
+ let(:mail_object) { Mail::Message.new(email_raw) }
+
+ it 'verifies and sends result emails' do
+ expect(Notify).to receive(:service_desk_verification_result_email).twice
+
+ response = service.execute
+
+ settings.reset
+ verification.reset
+
+ expect(response).to be_success
+ expect(settings).not_to be_custom_email_enabled
+ expect(verification).to be_finished
+ end
+
+ context 'and verification tokens do not match' do
+ before do
+ verification.update!(token: 'XXXXXXZXA-XX')
+ end
+
+ it_behaves_like 'a failing verification process', 'incorrect_token'
+ end
+
+ context 'and from address does not match with custom email' do
+ before do
+ settings.update!(custom_email: 'some-other@example.com')
+ end
+
+ it_behaves_like 'a failing verification process', 'incorrect_from'
+ end
+
+ context 'and timeframe for receiving the email is over' do
+ before do
+ verification.update!(triggered_at: 40.minutes.ago)
+ end
+
+ it_behaves_like 'a failing verification process', 'mail_not_received_within_timeframe'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/merge_request_diff_helpers.rb b/spec/support/helpers/merge_request_diff_helpers.rb
index 7515c789add..bbd9382fcc2 100644
--- a/spec/support/helpers/merge_request_diff_helpers.rb
+++ b/spec/support/helpers/merge_request_diff_helpers.rb
@@ -3,6 +3,18 @@
module MergeRequestDiffHelpers
PageEndReached = Class.new(StandardError)
+ def add_diff_line_draft_comment(comment, line_holder, diff_side = nil)
+ click_diff_line(line_holder, diff_side)
+ page.within('.js-discussion-note-form') do
+ fill_in('note_note', with: comment)
+ begin
+ click_button('Start a review', wait: 0.1)
+ rescue Capybara::ElementNotFound
+ click_button('Add to review')
+ end
+ end
+ end
+
def click_diff_line(line_holder, diff_side = nil)
line = get_line_components(line_holder, diff_side)
scroll_to_elements_bottom(line_holder)