summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-05-02 15:16:59 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-02 15:16:59 +0000
commit6f991190fe4dbb93070b090a9a31d71b25e8101d (patch)
tree0805552c79613c87d5e99c08f9a588d3cfe6f3c5 /spec
parent51d59a3538b97d85ebb46039044d3f498809b55a (diff)
downloadgitlab-ce-6f991190fe4dbb93070b090a9a31d71b25e8101d.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/import/bulk_imports_controller_spec.rb47
-rw-r--r--spec/features/dashboard/issues_filter_spec.rb2
-rw-r--r--spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js76
-rw-r--r--spec/frontend/admin/abuse_report/components/history_items_spec.js66
-rw-r--r--spec/frontend/admin/abuse_report/components/report_header_spec.js59
-rw-r--r--spec/frontend/admin/abuse_report/components/reported_content_spec.js188
-rw-r--r--spec/frontend/admin/abuse_report/components/user_detail_spec.js66
-rw-r--r--spec/frontend/admin/abuse_report/components/user_details_spec.js210
-rw-r--r--spec/frontend/admin/abuse_report/mock_data.js61
-rw-r--r--spec/frontend/admin/abuse_reports/components/abuse_report_actions_spec.js43
-rw-r--r--spec/frontend/admin/abuse_reports/mock_data.js2
-rw-r--r--spec/frontend/ide/components/repo_tab_spec.js79
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js13
-rw-r--r--spec/helpers/admin/abuse_reports_helper_spec.rb10
-rw-r--r--spec/lib/gitlab/quick_actions/extractor_spec.rb41
-rw-r--r--spec/models/abuse_report_spec.rb140
-rw-r--r--spec/models/authentication_event_spec.rb15
-rw-r--r--spec/policies/abuse_report_policy_spec.rb25
-rw-r--r--spec/requests/admin/abuse_reports_controller_spec.rb10
-rw-r--r--spec/requests/users_controller_spec.rb11
-rw-r--r--spec/serializers/admin/abuse_report_details_entity_spec.rb158
-rw-r--r--spec/serializers/admin/abuse_report_details_serializer_spec.rb20
-rw-r--r--spec/serializers/admin/abuse_report_entity_spec.rb7
-rw-r--r--spec/services/preview_markdown_service_spec.rb10
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb21
26 files changed, 1318 insertions, 72 deletions
diff --git a/spec/controllers/import/bulk_imports_controller_spec.rb b/spec/controllers/import/bulk_imports_controller_spec.rb
index 4e7f9572f65..c5e5aa03669 100644
--- a/spec/controllers/import/bulk_imports_controller_spec.rb
+++ b/spec/controllers/import/bulk_imports_controller_spec.rb
@@ -213,36 +213,41 @@ RSpec.describe Import::BulkImportsController, feature_category: :importers do
end
end
- context 'when host url is local or not http' do
- %w[https://localhost:3000 http://192.168.0.1 ftp://testing].each do |url|
- before do
- stub_application_setting(allow_local_requests_from_web_hooks_and_services: false)
-
- session[:bulk_import_gitlab_access_token] = 'test'
- session[:bulk_import_gitlab_url] = url
- end
+ shared_examples 'unacceptable url' do |url, expected_error|
+ before do
+ stub_application_setting(allow_local_requests_from_web_hooks_and_services: false)
- it 'denies network request' do
- get :status
+ session[:bulk_import_gitlab_access_token] = 'test'
+ session[:bulk_import_gitlab_url] = url
+ end
- expect(controller).to redirect_to(new_group_path(anchor: 'import-group-pane'))
- expect(flash[:alert]).to eq('Specified URL cannot be used: "Only allowed schemes are http, https"')
- end
+ it 'denies network request' do
+ get :status
+ expect(controller).to redirect_to(new_group_path(anchor: 'import-group-pane'))
+ expect(flash[:alert]).to eq("Specified URL cannot be used: \"#{expected_error}\"")
end
+ end
+
+ context 'when host url is local or not http' do
+ include_examples 'unacceptable url', 'https://localhost:3000', "Only allowed schemes are http, https"
+ include_examples 'unacceptable url', 'http://192.168.0.1', "Only allowed schemes are http, https"
+ include_examples 'unacceptable url', 'ftp://testing', "Only allowed schemes are http, https"
context 'when local requests are allowed' do
%w[https://localhost:3000 http://192.168.0.1].each do |url|
- before do
- stub_application_setting(allow_local_requests_from_web_hooks_and_services: true)
+ context "with #{url}" do
+ before do
+ stub_application_setting(allow_local_requests_from_web_hooks_and_services: true)
- session[:bulk_import_gitlab_access_token] = 'test'
- session[:bulk_import_gitlab_url] = url
- end
+ session[:bulk_import_gitlab_access_token] = 'test'
+ session[:bulk_import_gitlab_url] = url
+ end
- it 'allows network request' do
- get :status
+ it 'allows network request' do
+ get :status
- expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
end
end
diff --git a/spec/features/dashboard/issues_filter_spec.rb b/spec/features/dashboard/issues_filter_spec.rb
index e67e04ee0b0..964ac2f714d 100644
--- a/spec/features/dashboard/issues_filter_spec.rb
+++ b/spec/features/dashboard/issues_filter_spec.rb
@@ -96,7 +96,7 @@ RSpec.describe 'Dashboard Issues filtering', :js, feature_category: :team_planni
visit issues_dashboard_path(assignee_username: user.username)
end
- it 'remembers last sorting value' do
+ it 'remembers last sorting value', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/408749' do
click_button 'Created date'
click_button 'Updated date'
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
new file mode 100644
index 00000000000..cabbb5e1591
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js
@@ -0,0 +1,76 @@
+import { shallowMount } from '@vue/test-utils';
+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 { mockAbuseReport } from '../mock_data';
+
+describe('AbuseReportApp', () => {
+ let wrapper;
+
+ const findReportHeader = () => wrapper.findComponent(ReportHeader);
+ const findUserDetails = () => wrapper.findComponent(UserDetails);
+ const findReportedContent = () => wrapper.findComponent(ReportedContent);
+ const findHistoryItems = () => wrapper.findComponent(HistoryItems);
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(AbuseReportApp, {
+ propsData: {
+ abuseReport: mockAbuseReport,
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('ReportHeader', () => {
+ it('renders ReportHeader', () => {
+ expect(findReportHeader().props('user')).toBe(mockAbuseReport.user);
+ expect(findReportHeader().props('actions')).toBe(mockAbuseReport.actions);
+ });
+
+ describe('when no user is present', () => {
+ beforeEach(() => {
+ createComponent({
+ abuseReport: { ...mockAbuseReport, user: undefined },
+ });
+ });
+
+ it('does not render the ReportHeader', () => {
+ expect(findReportHeader().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('UserDetails', () => {
+ it('renders UserDetails', () => {
+ expect(findUserDetails().props('user')).toBe(mockAbuseReport.user);
+ });
+
+ describe('when no user is present', () => {
+ beforeEach(() => {
+ createComponent({
+ abuseReport: { ...mockAbuseReport, user: undefined },
+ });
+ });
+
+ it('does not render the UserDetails', () => {
+ expect(findUserDetails().exists()).toBe(false);
+ });
+ });
+ });
+
+ it('renders ReportedContent', () => {
+ expect(findReportedContent().props('report')).toBe(mockAbuseReport.report);
+ expect(findReportedContent().props('reporter')).toBe(mockAbuseReport.reporter);
+ });
+
+ it('renders HistoryItems', () => {
+ expect(findHistoryItems().props('report')).toBe(mockAbuseReport.report);
+ expect(findHistoryItems().props('reporter')).toBe(mockAbuseReport.reporter);
+ });
+});
diff --git a/spec/frontend/admin/abuse_report/components/history_items_spec.js b/spec/frontend/admin/abuse_report/components/history_items_spec.js
new file mode 100644
index 00000000000..86e994fdc57
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/history_items_spec.js
@@ -0,0 +1,66 @@
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { sprintf } from '~/locale';
+import HistoryItems from '~/admin/abuse_report/components/history_items.vue';
+import HistoryItem from '~/vue_shared/components/registry/history_item.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { HISTORY_ITEMS_I18N } from '~/admin/abuse_report/constants';
+import { mockAbuseReport } from '../mock_data';
+
+describe('HistoryItems', () => {
+ let wrapper;
+
+ const { report, reporter } = mockAbuseReport;
+
+ const findHistoryItem = () => wrapper.findComponent(HistoryItem);
+ const findTimeAgo = () => wrapper.findComponent(TimeAgoTooltip);
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(HistoryItems, {
+ propsData: {
+ report,
+ reporter,
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the icon', () => {
+ expect(findHistoryItem().props('icon')).toBe('warning');
+ });
+
+ describe('rendering the title', () => {
+ it('renders the reporters name and the category', () => {
+ const title = sprintf(HISTORY_ITEMS_I18N.reportedByForCategory, {
+ name: reporter.name,
+ category: report.category,
+ });
+ expect(findHistoryItem().text()).toContain(title);
+ });
+
+ describe('when the reporter is not defined', () => {
+ beforeEach(() => {
+ createComponent({ reporter: undefined });
+ });
+
+ it('renders the `No user found` as the reporters name and the category', () => {
+ const title = sprintf(HISTORY_ITEMS_I18N.reportedByForCategory, {
+ name: HISTORY_ITEMS_I18N.deletedReporter,
+ category: report.category,
+ });
+ expect(findHistoryItem().text()).toContain(title);
+ });
+ });
+ });
+
+ it('renders the time-ago tooltip', () => {
+ expect(findTimeAgo().props('time')).toBe(report.reportedAt);
+ });
+});
diff --git a/spec/frontend/admin/abuse_report/components/report_header_spec.js b/spec/frontend/admin/abuse_report/components/report_header_spec.js
new file mode 100644
index 00000000000..d584cab05b3
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/report_header_spec.js
@@ -0,0 +1,59 @@
+import { 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 { mockAbuseReport } from '../mock_data';
+
+describe('ReportHeader', () => {
+ let wrapper;
+
+ const { user, actions } = mockAbuseReport;
+
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
+ const findLink = () => wrapper.findComponent(GlLink);
+ const findButton = () => wrapper.findComponent(GlButton);
+ const findActions = () => wrapper.findComponent(AbuseReportActions);
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(ReportHeader, {
+ propsData: {
+ user,
+ actions,
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the users avatar', () => {
+ expect(findAvatar().props('src')).toBe(user.avatarUrl);
+ });
+
+ it('renders the users name', () => {
+ expect(wrapper.html()).toContain(user.name);
+ });
+
+ it('renders a link to the users profile page', () => {
+ const link = findLink();
+
+ expect(link.attributes('href')).toBe(user.path);
+ expect(link.text()).toBe(`@${user.username}`);
+ });
+
+ it('renders a button with a link to the users admin path', () => {
+ const button = findButton();
+
+ expect(button.attributes('href')).toBe(user.adminPath);
+ expect(button.text()).toBe(REPORT_HEADER_I18N.adminProfile);
+ });
+
+ it('renders the actions', () => {
+ const actionsComponent = findActions();
+
+ expect(actionsComponent.props('report')).toMatchObject(actions);
+ });
+});
diff --git a/spec/frontend/admin/abuse_report/components/reported_content_spec.js b/spec/frontend/admin/abuse_report/components/reported_content_spec.js
new file mode 100644
index 00000000000..471310e01d5
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/reported_content_spec.js
@@ -0,0 +1,188 @@
+import { GlSprintf, GlButton, GlModal, GlCard, GlAvatar, GlLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { sprintf } from '~/locale';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+import ReportedContent from '~/admin/abuse_report/components/reported_content.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { REPORTED_CONTENT_I18N } from '~/admin/abuse_report/constants';
+import { mockAbuseReport } from '../mock_data';
+
+jest.mock('~/behaviors/markdown/render_gfm');
+
+const modalId = 'abuse-report-screenshot-modal';
+
+describe('ReportedContent', () => {
+ let wrapper;
+
+ const { report, reporter } = { ...mockAbuseReport };
+
+ const findScreenshotButton = () => wrapper.findByTestId('screenshot-button');
+ const findReportUrlButton = () => wrapper.findByTestId('report-url-button');
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findCard = () => wrapper.findComponent(GlCard);
+ const findCardHeader = () => findCard().find('.js-test-card-header');
+ const findCardBody = () => findCard().find('.js-test-card-body');
+ const findCardFooter = () => findCard().find('.js-test-card-footer');
+ const findAvatar = () => findCardFooter().findComponent(GlAvatar);
+ const findProfileLink = () => findCardFooter().findComponent(GlLink);
+ const findTimeAgo = () => findCardFooter().findComponent(TimeAgoTooltip);
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(ReportedContent, {
+ propsData: {
+ report,
+ reporter,
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
+ GlButton,
+ GlCard,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the reported type', () => {
+ expect(wrapper.html()).toContain(sprintf(REPORTED_CONTENT_I18N.reportTypes[report.type]));
+ });
+
+ describe('when the type is unknown', () => {
+ beforeEach(() => {
+ createComponent({ report: { ...report, type: null } });
+ });
+
+ it('renders a header with a generic text content', () => {
+ expect(wrapper.html()).toContain(sprintf(REPORTED_CONTENT_I18N.reportTypes.unknown));
+ });
+ });
+
+ describe('showing the screenshot', () => {
+ describe('when the report contains a screenshot', () => {
+ it('renders a button to show the screenshot', () => {
+ expect(findScreenshotButton().text()).toBe(REPORTED_CONTENT_I18N.viewScreenshot);
+ });
+
+ it('renders a modal with the corrrect id and title', () => {
+ const modal = findModal();
+
+ expect(modal.props('title')).toBe(REPORTED_CONTENT_I18N.screenshotTitle);
+ expect(modal.props('modalId')).toBe(modalId);
+ });
+
+ it('contains an image with the screenshot', () => {
+ expect(findModal().find('img').attributes('src')).toBe(report.screenshot);
+ expect(findModal().find('img').attributes('alt')).toBe(
+ REPORTED_CONTENT_I18N.screenshotTitle,
+ );
+ });
+
+ it('opens the modal when clicking the button', async () => {
+ const modal = findModal();
+
+ expect(modal.props('visible')).toBe(false);
+
+ await findScreenshotButton().trigger('click');
+
+ expect(modal.props('visible')).toBe(true);
+ });
+ });
+
+ describe('when the report does not contain a screenshot', () => {
+ beforeEach(() => {
+ createComponent({ report: { ...report, screenshot: '' } });
+ });
+
+ it('does not render a button and a modal', () => {
+ expect(findScreenshotButton().exists()).toBe(false);
+ expect(findModal().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('showing a button to open the reported URL', () => {
+ describe('when the report contains a URL', () => {
+ it('renders a button with a link to the reported URL', () => {
+ expect(findReportUrlButton().text()).toBe(
+ sprintf(REPORTED_CONTENT_I18N.goToType[report.type]),
+ );
+ });
+ });
+
+ describe('when the report type is unknown', () => {
+ beforeEach(() => {
+ createComponent({ report: { ...report, type: null } });
+ });
+
+ it('renders a button with a generic text content', () => {
+ expect(findReportUrlButton().text()).toBe(sprintf(REPORTED_CONTENT_I18N.goToType.unknown));
+ });
+ });
+
+ describe('when the report contains no URL', () => {
+ beforeEach(() => {
+ createComponent({ report: { ...report, url: '' } });
+ });
+
+ it('does not render a button with a link to the reported URL', () => {
+ expect(findReportUrlButton().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('rendering the card header', () => {
+ describe('when the report contains the reported content', () => {
+ it('renders the content', () => {
+ expect(findCardHeader().text()).toBe(report.content.replace(/<\/?[^>]+>/g, ''));
+ });
+
+ it('renders gfm', () => {
+ expect(renderGFM).toHaveBeenCalled();
+ });
+ });
+
+ describe('when the report does not contain the reported content', () => {
+ beforeEach(() => {
+ createComponent({ report: { ...report, content: '' } });
+ });
+
+ it('does not render the card header', () => {
+ expect(findCardHeader().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('rendering the card body', () => {
+ it('renders the reported by', () => {
+ expect(findCardBody().text()).toBe(REPORTED_CONTENT_I18N.reportedBy);
+ });
+ });
+
+ describe('rendering the card footer', () => {
+ it('renders the reporters avatar', () => {
+ expect(findAvatar().props('src')).toBe(reporter.avatarUrl);
+ });
+
+ it('renders the users name', () => {
+ expect(findCardFooter().text()).toContain(reporter.name);
+ });
+
+ it('renders a link to the users profile page', () => {
+ const link = findProfileLink();
+
+ expect(link.attributes('href')).toBe(reporter.path);
+ expect(link.text()).toBe(`@${reporter.username}`);
+ });
+
+ it('renders the time-ago tooltip', () => {
+ expect(findTimeAgo().props('time')).toBe(report.reportedAt);
+ });
+
+ it('renders the message', () => {
+ expect(findCardFooter().text()).toContain(report.message);
+ });
+ });
+});
diff --git a/spec/frontend/admin/abuse_report/components/user_detail_spec.js b/spec/frontend/admin/abuse_report/components/user_detail_spec.js
new file mode 100644
index 00000000000..d9e02bc96e2
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/user_detail_spec.js
@@ -0,0 +1,66 @@
+import { shallowMount } from '@vue/test-utils';
+import UserDetail from '~/admin/abuse_report/components/user_detail.vue';
+
+describe('UserDetail', () => {
+ let wrapper;
+
+ const label = 'user detail label';
+ const value = 'user detail value';
+
+ const createComponent = (props = {}, slots = {}) => {
+ wrapper = shallowMount(UserDetail, {
+ propsData: {
+ label,
+ value,
+ ...props,
+ },
+ slots,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('UserDetail', () => {
+ it('renders the label', () => {
+ expect(wrapper.text()).toContain(label);
+ });
+
+ describe('rendering the value', () => {
+ const slots = {
+ default: ['slot provided user detail'],
+ };
+
+ describe('when `value` property and no default slot is provided', () => {
+ it('renders the `value` as content', () => {
+ expect(wrapper.text()).toContain(value);
+ });
+ });
+
+ describe('when default slot and no `value` property is provided', () => {
+ beforeEach(() => {
+ createComponent({ label, value: null }, slots);
+ });
+
+ it('renders the content provided via the default slot', () => {
+ expect(wrapper.text()).toContain(slots.default[0]);
+ });
+ });
+
+ describe('when `value` property and default slot are both provided', () => {
+ beforeEach(() => {
+ createComponent({ label, value }, slots);
+ });
+
+ it('does not render `value` as content', () => {
+ expect(wrapper.text()).not.toContain(value);
+ });
+
+ it('renders the content provided via the default slot', () => {
+ expect(wrapper.text()).toContain(slots.default[0]);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/admin/abuse_report/components/user_details_spec.js b/spec/frontend/admin/abuse_report/components/user_details_spec.js
new file mode 100644
index 00000000000..ca499fbaa6e
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/user_details_spec.js
@@ -0,0 +1,210 @@
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { sprintf } from '~/locale';
+import UserDetails from '~/admin/abuse_report/components/user_details.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { USER_DETAILS_I18N } from '~/admin/abuse_report/constants';
+import { mockAbuseReport } from '../mock_data';
+
+describe('UserDetails', () => {
+ let wrapper;
+
+ const { user } = mockAbuseReport;
+
+ const findUserDetail = (attribute) => wrapper.findByTestId(attribute);
+ const findUserDetailLabel = (attribute) => findUserDetail(attribute).props('label');
+ const findUserDetailValue = (attribute) => findUserDetail(attribute).props('value');
+ const findLinkIn = (component) => component.findComponent(GlLink);
+ const findLinkFor = (attribute) => findLinkIn(findUserDetail(attribute));
+ const findTimeIn = (component) => component.findComponent(TimeAgoTooltip).props('time');
+ const findTimeFor = (attribute) => findTimeIn(findUserDetail(attribute));
+ const findOtherReport = (index) => wrapper.findByTestId(`other-report-${index}`);
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(UserDetails, {
+ propsData: {
+ user,
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('createdAt', () => {
+ it('renders the users createdAt with the correct label', () => {
+ expect(findUserDetailLabel('createdAt')).toBe(USER_DETAILS_I18N.createdAt);
+ expect(findTimeFor('createdAt')).toBe(user.createdAt);
+ });
+ });
+
+ describe('email', () => {
+ it('renders the users email with the correct label', () => {
+ expect(findUserDetailLabel('email')).toBe(USER_DETAILS_I18N.email);
+ expect(findLinkFor('email').attributes('href')).toBe(`mailto:${user.email}`);
+ expect(findLinkFor('email').text()).toBe(user.email);
+ });
+ });
+
+ describe('plan', () => {
+ it('renders the users plan with the correct label', () => {
+ expect(findUserDetailLabel('plan')).toBe(USER_DETAILS_I18N.plan);
+ expect(findUserDetailValue('plan')).toBe(user.plan);
+ });
+ });
+
+ describe('verification', () => {
+ it('renders the users verification with the correct label', () => {
+ expect(findUserDetailLabel('verification')).toBe(USER_DETAILS_I18N.verification);
+ expect(findUserDetailValue('verification')).toBe('Email, Credit card');
+ });
+ });
+
+ describe('creditCard', () => {
+ it('renders the correct label', () => {
+ expect(findUserDetailLabel('creditCard')).toBe(USER_DETAILS_I18N.creditCard);
+ });
+
+ it('renders the users name', () => {
+ expect(findUserDetail('creditCard').text()).toContain(
+ sprintf(USER_DETAILS_I18N.registeredWith, { ...user.creditCard }),
+ );
+
+ expect(findUserDetail('creditCard').text()).toContain(user.creditCard.name);
+ });
+
+ describe('similar credit cards', () => {
+ it('renders the number of similar records', () => {
+ expect(findUserDetail('creditCard').text()).toContain(
+ sprintf('Card matches %{similarRecordsCount} accounts', { ...user.creditCard }),
+ );
+ });
+
+ it('renders a link to the matching cards', () => {
+ expect(findLinkFor('creditCard').attributes('href')).toBe(user.creditCard.cardMatchesLink);
+
+ expect(findLinkFor('creditCard').text()).toBe(
+ sprintf('%{similarRecordsCount} accounts', { ...user.creditCard }),
+ );
+
+ expect(findLinkFor('creditCard').text()).toContain(
+ user.creditCard.similarRecordsCount.toString(),
+ );
+ });
+
+ describe('when the number of similar credit cards is less than 2', () => {
+ beforeEach(() => {
+ createComponent({
+ user: { ...user, creditCard: { ...user.creditCard, similarRecordsCount: 1 } },
+ });
+ });
+
+ it('does not render the number of similar records', () => {
+ expect(findUserDetail('creditCard').text()).not.toContain(
+ sprintf('Card matches %{similarRecordsCount} accounts', { ...user.creditCard }),
+ );
+ });
+
+ it('does not render a link to the matching cards', () => {
+ expect(findLinkFor('creditCard').exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('when the users creditCard is blank', () => {
+ beforeEach(() => {
+ createComponent({
+ user: { ...user, creditCard: undefined },
+ });
+ });
+
+ it('does not render the users creditCard', () => {
+ expect(findUserDetail('creditCard').exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('otherReports', () => {
+ it('renders the correct label', () => {
+ expect(findUserDetailLabel('otherReports')).toBe(USER_DETAILS_I18N.otherReports);
+ });
+
+ describe.each(user.otherReports)('renders a line for report %#', (otherReport) => {
+ const index = user.otherReports.indexOf(otherReport);
+
+ it('renders the category', () => {
+ expect(findOtherReport(index).text()).toContain(
+ sprintf('Reported for %{category}', { ...otherReport }),
+ );
+ });
+
+ it('renders a link to the report', () => {
+ expect(findLinkIn(findOtherReport(index)).attributes('href')).toBe(otherReport.reportPath);
+ });
+
+ it('renders the time it was created', () => {
+ expect(findTimeIn(findOtherReport(index))).toBe(otherReport.createdAt);
+ });
+ });
+
+ describe('when the users otherReports is empty', () => {
+ beforeEach(() => {
+ createComponent({
+ user: { ...user, otherReports: [] },
+ });
+ });
+
+ it('does not render the users otherReports', () => {
+ expect(findUserDetail('otherReports').exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('normalLocation', () => {
+ it('renders the correct label', () => {
+ expect(findUserDetailLabel('normalLocation')).toBe(USER_DETAILS_I18N.normalLocation);
+ });
+
+ describe('when the users mostUsedIp is blank', () => {
+ it('renders the users lastSignInIp', () => {
+ expect(findUserDetailValue('normalLocation')).toBe(user.lastSignInIp);
+ });
+ });
+
+ describe('when the users mostUsedIp is not blank', () => {
+ const mostUsedIp = '127.0.0.1';
+
+ beforeEach(() => {
+ createComponent({
+ user: { ...user, mostUsedIp },
+ });
+ });
+
+ it('renders the users mostUsedIp', () => {
+ expect(findUserDetailValue('normalLocation')).toBe(mostUsedIp);
+ });
+ });
+ });
+
+ describe('lastSignInIp', () => {
+ it('renders the users lastSignInIp with the correct label', () => {
+ expect(findUserDetailLabel('lastSignInIp')).toBe(USER_DETAILS_I18N.lastSignInIp);
+ expect(findUserDetailValue('lastSignInIp')).toBe(user.lastSignInIp);
+ });
+ });
+
+ it.each(['snippets', 'groups', 'notes'])(
+ 'renders the users %s with the correct label',
+ (attribute) => {
+ expect(findUserDetailLabel(attribute)).toBe(USER_DETAILS_I18N[attribute]);
+ expect(findUserDetailValue(attribute)).toBe(
+ USER_DETAILS_I18N[`${attribute}Count`](user[`${attribute}Count`]),
+ );
+ },
+ );
+});
diff --git a/spec/frontend/admin/abuse_report/mock_data.js b/spec/frontend/admin/abuse_report/mock_data.js
new file mode 100644
index 00000000000..ee0f0967735
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/mock_data.js
@@ -0,0 +1,61 @@
+export const mockAbuseReport = {
+ user: {
+ username: 'spamuser417',
+ name: 'Sp4m User',
+ createdAt: '2023-03-29T09:30:23.885Z',
+ email: 'sp4m@spam.com',
+ lastActivityOn: '2023-04-02',
+ avatarUrl: 'https://www.gravatar.com/avatar/a2579caffc69ea5d7606f9dd9d8504ba?s=80&d=identicon',
+ path: '/spamuser417',
+ adminPath: '/admin/users/spamuser417',
+ plan: 'Free',
+ verificationState: { email: true, phone: false, creditCard: true },
+ creditCard: {
+ name: 'S. User',
+ similarRecordsCount: 2,
+ cardMatchesLink: '/admin/users/spamuser417/card_match',
+ },
+ otherReports: [
+ {
+ category: 'offensive',
+ createdAt: '2023-02-28T10:09:54.982Z',
+ reportPath: '/admin/abuse_reports/29',
+ },
+ {
+ category: 'crypto',
+ createdAt: '2023-03-31T11:57:11.849Z',
+ reportPath: '/admin/abuse_reports/31',
+ },
+ ],
+ mostUsedIp: null,
+ lastSignInIp: '::1',
+ snippetsCount: 0,
+ groupsCount: 0,
+ notesCount: 6,
+ },
+ reporter: {
+ username: 'reporter',
+ name: 'R Porter',
+ avatarUrl: 'https://www.gravatar.com/avatar/a2579caffc69ea5d7606f9dd9d8504ba?s=80&d=identicon',
+ path: '/reporter',
+ },
+ report: {
+ message: 'This is obvious spam',
+ reportedAt: '2023-03-29T09:39:50.502Z',
+ category: 'spam',
+ type: 'comment',
+ content:
+ '<p data-sourcepos="1:1-1:772" dir="auto">Farmers Toy Sale ON NOW | SHOP CATALOGUE ... 50% off Kids\' Underwear by Hanes ... BUY 1 GET 1 HALF PRICE on Women\'s Clothing by Whistle, Ella Clothing Farmers Toy Sale ON <a href="http://www.farmers.com" rel="nofollow noreferrer noopener" target="_blank">www.farmers.com</a> | SHOP CATALOGUE ... 50% off Kids\' Underwear by Hanes ... BUY 1 GET 1 HALF PRICE on Women\'s Clothing by Whistle, Ella Clothing Farmers Toy Sale ON NOW | SHOP CATALOGUE ... 50% off Kids\' Underwear by Farmers Toy Sale ON NOW | SHOP CATALOGUE ... 50% off Kids\' Underwear by Hanes ... BUY 1 GET 1 HALF PRICE on Women\'s Clothing by Whistle, Ella Clothing Farmers Toy Sale ON <a href="http://www.farmers.com" rel="nofollow noreferrer noopener" target="_blank">www.farmers.com</a> | SHOP CATALOGUE ... 50% off Kids\' Underwear by Hanes ... BUY 1 GET 1 HALF PRICE on Women\'s Clothing by Whistle, Ella Clothing Farmers Toy Sale ON NOW | SHOP CATALOGUE ... 50% off Kids\' Underwear by.</p>',
+ 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',
+ },
+};
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
index 2d0f00ea585..9708de69caa 100644
--- a/spec/frontend/admin/abuse_reports/components/abuse_report_actions_spec.js
+++ b/spec/frontend/admin/abuse_reports/components/abuse_report_actions_spec.js
@@ -3,15 +3,16 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { GlDisclosureDropdown, GlDisclosureDropdownItem, GlModal } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { useMockLocationHelper } from 'helpers/mock_window_location_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';
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;
@@ -69,8 +70,6 @@ describe('AbuseReportActions', () => {
describe('actions', () => {
let axiosMock;
- useMockLocationHelper();
-
beforeEach(() => {
axiosMock = new MockAdapter(axios);
@@ -99,7 +98,25 @@ describe('AbuseReportActions', () => {
findConfirmationModal().vm.$emit('primary');
await axios.waitForAll();
- expect(window.location.reload).toHaveBeenCalled();
+ 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');
+ });
});
});
@@ -162,7 +179,23 @@ describe('AbuseReportActions', () => {
await axios.waitForAll();
- expect(window.location.reload).toHaveBeenCalled();
+ 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');
+ });
});
});
});
diff --git a/spec/frontend/admin/abuse_reports/mock_data.js b/spec/frontend/admin/abuse_reports/mock_data.js
index 90289757a74..ee9e56d043b 100644
--- a/spec/frontend/admin/abuse_reports/mock_data.js
+++ b/spec/frontend/admin/abuse_reports/mock_data.js
@@ -12,6 +12,7 @@ export const mockAbuseReports = [
removeUserAndReportPath: '/remove/user/mr_abuser/and/report/path',
removeReportPath: '/remove/report/path',
message: 'message 1',
+ reportPath: '/admin/abuse_reports/1',
},
{
category: 'phishing',
@@ -26,5 +27,6 @@ export const mockAbuseReports = [
removeUserAndReportPath: '/remove/user/mr_phisher/and/report/path',
removeReportPath: '/remove/report/path',
message: 'message 2',
+ reportPath: '/admin/abuse_reports/2',
},
];
diff --git a/spec/frontend/ide/components/repo_tab_spec.js b/spec/frontend/ide/components/repo_tab_spec.js
index b329baea783..d4f29b16a88 100644
--- a/spec/frontend/ide/components/repo_tab_spec.js
+++ b/spec/frontend/ide/components/repo_tab_spec.js
@@ -1,11 +1,10 @@
import { GlTab } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import { stubComponent } from 'helpers/stub_component';
import RepoTab from '~/ide/components/repo_tab.vue';
-import { createRouter } from '~/ide/ide_router';
import { createStore } from '~/ide/stores';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { file } from '../helpers';
Vue.use(Vuex);
@@ -17,36 +16,40 @@ const GlTabStub = stubComponent(GlTab, {
describe('RepoTab', () => {
let wrapper;
let store;
- let router;
+ const pushMock = jest.fn();
const findTab = () => wrapper.findComponent(GlTabStub);
+ const findCloseButton = () => wrapper.findByTestId('close-button');
function createComponent(propsData) {
- wrapper = mount(RepoTab, {
+ wrapper = mountExtended(RepoTab, {
store,
propsData,
stubs: {
GlTab: GlTabStub,
},
+ mocks: {
+ $router: {
+ push: pushMock,
+ },
+ },
});
}
beforeEach(() => {
store = createStore();
- router = createRouter(store);
- jest.spyOn(router, 'push').mockImplementation(() => {});
});
it('renders a close link and a name link', () => {
+ const tab = file();
createComponent({
- tab: file(),
+ tab,
});
- wrapper.vm.$store.state.openFiles.push(wrapper.vm.tab);
- const close = wrapper.find('.multi-file-tab-close');
+ store.state.openFiles.push(tab);
const name = wrapper.find(`[title]`);
- expect(close.html()).toContain('#close');
- expect(name.text().trim()).toEqual(wrapper.vm.tab.name);
+ expect(findCloseButton().html()).toContain('#close');
+ expect(name.text()).toBe(tab.name);
});
it('does not call openPendingTab when tab is active', async () => {
@@ -58,35 +61,33 @@ describe('RepoTab', () => {
},
});
- jest.spyOn(wrapper.vm, 'openPendingTab').mockImplementation(() => {});
+ jest.spyOn(store, 'dispatch');
await findTab().vm.$emit('click');
- expect(wrapper.vm.openPendingTab).not.toHaveBeenCalled();
+ expect(store.dispatch).not.toHaveBeenCalledWith('openPendingTab');
});
- it('fires clickFile when the link is clicked', () => {
- createComponent({
- tab: file(),
- });
-
- jest.spyOn(wrapper.vm, 'clickFile').mockImplementation(() => {});
+ it('fires clickFile when the link is clicked', async () => {
+ const { getters } = store;
+ const tab = file();
+ createComponent({ tab });
- findTab().vm.$emit('click');
+ await findTab().vm.$emit('click', tab);
- expect(wrapper.vm.clickFile).toHaveBeenCalledWith(wrapper.vm.tab);
+ expect(pushMock).toHaveBeenCalledWith(getters.getUrlForPath(tab.path));
});
- it('calls closeFile when clicking close button', () => {
- createComponent({
- tab: file(),
- });
+ it('calls closeFile when clicking close button', async () => {
+ const tab = file();
+ createComponent({ tab });
+ store.state.entries[tab.path] = tab;
- jest.spyOn(wrapper.vm, 'closeFile').mockImplementation(() => {});
+ jest.spyOn(store, 'dispatch');
- wrapper.find('.multi-file-tab-close').trigger('click');
+ await findCloseButton().trigger('click');
- expect(wrapper.vm.closeFile).toHaveBeenCalledWith(wrapper.vm.tab);
+ expect(store.dispatch).toHaveBeenCalledWith('closeFile', tab);
});
it('changes icon on hover', async () => {
@@ -114,7 +115,7 @@ describe('RepoTab', () => {
createComponent({ tab });
- expect(wrapper.find('button').attributes('aria-label')).toBe(closeLabel);
+ expect(findCloseButton().attributes('aria-label')).toBe(closeLabel);
});
describe('locked file', () => {
@@ -152,15 +153,15 @@ describe('RepoTab', () => {
createComponent({
tab,
});
- wrapper.vm.$store.state.openFiles.push(tab);
- wrapper.vm.$store.state.changedFiles.push(tab);
- wrapper.vm.$store.state.entries[tab.path] = tab;
- wrapper.vm.$store.dispatch('setFileActive', tab.path);
+ store.state.openFiles.push(tab);
+ store.state.changedFiles.push(tab);
+ store.state.entries[tab.path] = tab;
+ store.dispatch('setFileActive', tab.path);
- await wrapper.find('.multi-file-tab-close').trigger('click');
+ await findCloseButton().trigger('click');
expect(tab.opened).toBe(false);
- expect(wrapper.vm.$store.state.changedFiles).toHaveLength(1);
+ expect(store.state.changedFiles).toHaveLength(1);
});
it('closes tab when clicking close btn', async () => {
@@ -169,11 +170,11 @@ describe('RepoTab', () => {
createComponent({
tab,
});
- wrapper.vm.$store.state.openFiles.push(tab);
- wrapper.vm.$store.state.entries[tab.path] = tab;
- wrapper.vm.$store.dispatch('setFileActive', tab.path);
+ store.state.openFiles.push(tab);
+ store.state.entries[tab.path] = tab;
+ store.dispatch('setFileActive', tab.path);
- await wrapper.find('.multi-file-tab-close').trigger('click');
+ await findCloseButton().trigger('click');
expect(tab.opened).toBe(false);
});
diff --git a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
index 387a5dc8f1d..64fb2806447 100644
--- a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
@@ -162,9 +162,17 @@ describe('MrWidgetOptions', () => {
describe('computed', () => {
describe('componentName', () => {
+ // eslint-disable-next-line jest/no-disabled-tests
+ it.skip.each`
+ ${'merged'} | ${'mr-widget-merged'}
+ `('should translate $state into $componentName', ({ state, componentName }) => {
+ wrapper.vm.mr.state = state;
+
+ expect(wrapper.vm.componentName).toEqual(componentName);
+ });
+
it.each`
state | componentName
- ${'merged'} | ${'mr-widget-merged'}
${'conflicts'} | ${'mr-widget-conflicts'}
${'shaMismatch'} | ${'sha-mismatch'}
`('should translate $state into $componentName', ({ state, componentName }) => {
diff --git a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
index 69dedd6b68a..e9d5da4edcf 100644
--- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
@@ -106,6 +106,19 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
});
});
+ it.each`
+ desc | supportsQuickActions
+ ${'passes render_quick_actions param to renderMarkdownPath if quick actions are enabled'} | ${true}
+ ${'does not pass render_quick_actions param to renderMarkdownPath if quick actions are disabled'} | ${false}
+ `('$desc', async ({ supportsQuickActions }) => {
+ buildWrapper({ propsData: { supportsQuickActions } });
+
+ await enableContentEditor();
+
+ expect(mock.history.post).toHaveLength(1);
+ expect(mock.history.post[0].url).toContain(`render_quick_actions=${supportsQuickActions}`);
+ });
+
it('enables content editor switcher when contentEditorEnabled prop is true', () => {
buildWrapper({ propsData: { enableContentEditor: true } });
diff --git a/spec/helpers/admin/abuse_reports_helper_spec.rb b/spec/helpers/admin/abuse_reports_helper_spec.rb
index 83393cc641d..496b7361b6e 100644
--- a/spec/helpers/admin/abuse_reports_helper_spec.rb
+++ b/spec/helpers/admin/abuse_reports_helper_spec.rb
@@ -21,4 +21,14 @@ RSpec.describe Admin::AbuseReportsHelper, feature_category: :insider_threat do
expect(data['categories']).to match_array(AbuseReport.categories.keys)
end
end
+
+ describe '#abuse_report_data' do
+ let(:report) { build_stubbed(:abuse_report) }
+
+ subject(:data) { helper.abuse_report_data(report)[:abuse_report_data] }
+
+ it 'has the expected attributes' do
+ expect(data).to include('user', 'reporter', 'report', 'actions')
+ end
+ end
end
diff --git a/spec/lib/gitlab/quick_actions/extractor_spec.rb b/spec/lib/gitlab/quick_actions/extractor_spec.rb
index e2f289041ce..f91e8d2a7ef 100644
--- a/spec/lib/gitlab/quick_actions/extractor_spec.rb
+++ b/spec/lib/gitlab/quick_actions/extractor_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::QuickActions::Extractor do
+RSpec.describe Gitlab::QuickActions::Extractor, feature_category: :team_planning do
let(:definitions) do
Class.new do
include Gitlab::QuickActions::Dsl
@@ -19,7 +19,8 @@ RSpec.describe Gitlab::QuickActions::Extractor do
end.command_definitions
end
- let(:extractor) { described_class.new(definitions) }
+ let(:extractor) { described_class.new(definitions, keep_actions: keep_actions) }
+ let(:keep_actions) { false }
shared_examples 'command with no argument' do
it 'extracts command' do
@@ -176,6 +177,31 @@ RSpec.describe Gitlab::QuickActions::Extractor do
end
end
+ describe 'command with keep_actions' do
+ let(:keep_actions) { true }
+
+ context 'at the start of content' do
+ it_behaves_like 'command with a single argument' do
+ let(:original_msg) { "/assign @joe\nworld" }
+ let(:final_msg) { "\n/assign @joe\n\nworld" }
+ end
+ end
+
+ context 'in the middle of content' do
+ it_behaves_like 'command with a single argument' do
+ let(:original_msg) { "hello\n/assign @joe\nworld" }
+ let(:final_msg) { "hello\n\n/assign @joe\n\nworld" }
+ end
+ end
+
+ context 'at the end of content' do
+ it_behaves_like 'command with a single argument' do
+ let(:original_msg) { "hello\n/assign @joe" }
+ let(:final_msg) { "hello\n\n/assign @joe" }
+ end
+ end
+ end
+
it 'extracts command with multiple arguments and various prefixes' do
msg = %(hello\n/power @user.name %9.10 ~"bar baz.2"\nworld)
msg, commands = extractor.extract_commands(msg)
@@ -244,10 +270,19 @@ RSpec.describe Gitlab::QuickActions::Extractor do
msg = %(hello\nworld\n/reopen\n/substitution wow this is a thing.)
msg, commands = extractor.extract_commands(msg)
- expect(commands).to eq [['reopen'], ['substitution', 'wow this is a thing.']]
+ expect(commands).to match_array [['reopen'], ['substitution', 'wow this is a thing.']]
expect(msg).to eq "hello\nworld\nfoo"
end
+ it 'extracts and performs substitution commands with keep_actions' do
+ extractor = described_class.new(definitions, keep_actions: true)
+ msg = %(hello\nworld\n/reopen\n/substitution wow this is a thing.)
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to match_array [['reopen'], ['substitution', 'wow this is a thing.']]
+ expect(msg).to eq "hello\nworld\n\n/reopen\n\nfoo"
+ end
+
it 'extracts multiple commands' do
msg = %(hello\n/power @user.name %9.10 ~"bar baz.2" label\nworld\n/reopen)
msg, commands = extractor.extract_commands(msg)
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index 8a9ac618e00..6e678127aff 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe AbuseReport, feature_category: :insider_threat do
+ include Gitlab::Routing.url_helpers
+
let_it_be(:report, reload: true) { create(:abuse_report) }
let_it_be(:user, reload: true) { create(:admin) }
@@ -180,6 +182,144 @@ RSpec.describe AbuseReport, feature_category: :insider_threat do
end
end
+ describe '#report_type' do
+ let(:report) { build_stubbed(:abuse_report, reported_from_url: url) }
+ let_it_be(:issue) { create(:issue) }
+ let_it_be(:merge_request) { create(:merge_request) }
+ let_it_be(:user) { create(:user) }
+
+ subject { report.report_type }
+
+ context 'when reported from an issue' do
+ let(:url) { project_issue_url(issue.project, issue) }
+
+ it { is_expected.to eq :issue }
+ end
+
+ context 'when reported from a merge request' do
+ let(:url) { project_merge_request_url(merge_request.project, merge_request) }
+
+ it { is_expected.to eq :merge_request }
+ end
+
+ context 'when reported from a profile' do
+ let(:url) { user_url(user) }
+
+ it { is_expected.to eq :profile }
+ end
+
+ describe 'comment type' do
+ context 'when reported from an issue comment' do
+ let(:url) { project_issue_url(issue.project, issue, anchor: 'note_123') }
+
+ it { is_expected.to eq :comment }
+ end
+
+ context 'when reported from a merge request comment' do
+ let(:url) { project_merge_request_url(merge_request.project, merge_request, anchor: 'note_123') }
+
+ it { is_expected.to eq :comment }
+ end
+
+ context 'when anchor exists not from an issue or merge request URL' do
+ let(:url) { user_url(user, anchor: 'note_123') }
+
+ it { is_expected.to eq :profile }
+ end
+
+ context 'when note id is invalid' do
+ let(:url) { project_merge_request_url(merge_request.project, merge_request, anchor: 'note_12x') }
+
+ it { is_expected.to eq :merge_request }
+ end
+ end
+
+ context 'when URL cannot be matched' do
+ let(:url) { '/xxx' }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#reported_content' do
+ let(:report) { build_stubbed(:abuse_report, reported_from_url: url) }
+ let_it_be(:issue) { create(:issue, description: 'issue description') }
+ let_it_be(:merge_request) { create(:merge_request, description: 'mr description') }
+ let_it_be(:user) { create(:user) }
+
+ subject { report.reported_content }
+
+ context 'when reported from an issue' do
+ let(:url) { project_issue_url(issue.project, issue) }
+
+ it { is_expected.to eq issue.description_html }
+ end
+
+ context 'when reported from a merge request' do
+ let(:url) { project_merge_request_url(merge_request.project, merge_request) }
+
+ it { is_expected.to eq merge_request.description_html }
+ end
+
+ context 'when reported from a merge request with an invalid note ID' do
+ let(:url) do
+ "#{project_merge_request_url(merge_request.project, merge_request)}#note_[]"
+ end
+
+ it { is_expected.to eq merge_request.description_html }
+ end
+
+ context 'when reported from a profile' do
+ let(:url) { user_url(user) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when reported from an unknown URL' do
+ let(:url) { '/xxx' }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when reported from an invalid URL' do
+ let(:url) { 'http://example.com/[]' }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when reported from an issue comment' do
+ let(:note) { create(:note, noteable: issue, project: issue.project, note: 'comment in issue') }
+ let(:url) { project_issue_url(issue.project, issue, anchor: "note_#{note.id}") }
+
+ it { is_expected.to eq note.note_html }
+ end
+
+ context 'when reported from a merge request comment' do
+ let(:note) { create(:note, noteable: merge_request, project: merge_request.project, note: 'comment in mr') }
+ let(:url) { project_merge_request_url(merge_request.project, merge_request, anchor: "note_#{note.id}") }
+
+ it { is_expected.to eq note.note_html }
+ end
+
+ context 'when report type cannot be determined, because the comment does not exist' do
+ let(:url) do
+ project_merge_request_url(merge_request.project, merge_request, anchor: "note_#{non_existing_record_id}")
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#other_reports_for_user' do
+ let(:report) { create(:abuse_report) }
+ let(:another_user_report) { create(:abuse_report, user: report.user) }
+ let(:another_report) { create(:abuse_report) }
+
+ it 'returns other reports for the same user' do
+ expect(report.other_reports_for_user).to match_array(another_user_report)
+ end
+ end
+
describe 'enums' do
let(:categories) do
{
diff --git a/spec/models/authentication_event_spec.rb b/spec/models/authentication_event_spec.rb
index 23e253c2a28..17fe10b5b4e 100644
--- a/spec/models/authentication_event_spec.rb
+++ b/spec/models/authentication_event_spec.rb
@@ -71,4 +71,19 @@ RSpec.describe AuthenticationEvent do
it { is_expected.to eq(false) }
end
end
+
+ describe '.most_used_ip_address_for_user' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:most_used_ip_address) { '::1' }
+ let_it_be(:another_ip_address) { '127.0.0.1' }
+
+ subject { described_class.most_used_ip_address_for_user(user) }
+
+ before do
+ create_list(:authentication_event, 2, user: user, ip_address: most_used_ip_address)
+ create(:authentication_event, user: user, ip_address: another_ip_address)
+ end
+
+ it { is_expected.to eq(most_used_ip_address) }
+ end
end
diff --git a/spec/policies/abuse_report_policy_spec.rb b/spec/policies/abuse_report_policy_spec.rb
new file mode 100644
index 00000000000..b17b6886b9a
--- /dev/null
+++ b/spec/policies/abuse_report_policy_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe AbuseReportPolicy, feature_category: :insider_threat do
+ let(:abuse_report) { build_stubbed(:abuse_report) }
+
+ subject(:policy) { described_class.new(user, abuse_report) }
+
+ context 'when the user is not an admin' do
+ let(:user) { create(:user) }
+
+ it 'cannot read_abuse_report' do
+ expect(policy).to be_disallowed(:read_abuse_report)
+ end
+ end
+
+ context 'when the user is an admin', :enable_admin_mode do
+ let(:user) { create(:admin) }
+
+ it 'can read_abuse_report' do
+ expect(policy).to be_allowed(:read_abuse_report)
+ end
+ end
+end
diff --git a/spec/requests/admin/abuse_reports_controller_spec.rb b/spec/requests/admin/abuse_reports_controller_spec.rb
index 3d3bfcd3f60..ab527ab4df6 100644
--- a/spec/requests/admin/abuse_reports_controller_spec.rb
+++ b/spec/requests/admin/abuse_reports_controller_spec.rb
@@ -42,4 +42,14 @@ RSpec.describe Admin::AbuseReportsController, type: :request, feature_category:
end
end
end
+
+ describe 'GET #show' do
+ let!(:report) { create(:abuse_report) }
+
+ it 'returns the requested report' do
+ get admin_abuse_report_path(report)
+
+ expect(assigns(:abuse_report)).to eq report
+ end
+ end
end
diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb
index 3367414b987..c49dbb6a269 100644
--- a/spec/requests/users_controller_spec.rb
+++ b/spec/requests/users_controller_spec.rb
@@ -770,6 +770,17 @@ RSpec.describe UsersController, feature_category: :user_management do
expect(response.body).to eq(expected_json)
end
end
+
+ context 'when a project has the same name as a desired username' do
+ let_it_be(:project) { create(:project, name: 'project-name') }
+
+ it 'returns JSON indicating a user by that username does not exist' do
+ get user_exists_url 'project-name'
+
+ expected_json = { exists: false }.to_json
+ expect(response.body).to eq(expected_json)
+ end
+ end
end
context 'when the rate limit has been reached' do
diff --git a/spec/serializers/admin/abuse_report_details_entity_spec.rb b/spec/serializers/admin/abuse_report_details_entity_spec.rb
new file mode 100644
index 00000000000..0e5e6a62ce1
--- /dev/null
+++ b/spec/serializers/admin/abuse_report_details_entity_spec.rb
@@ -0,0 +1,158 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Admin::AbuseReportDetailsEntity, feature_category: :insider_threat do
+ include Gitlab::Routing
+
+ let(:report) { build_stubbed(:abuse_report) }
+ let(:user) { report.user }
+ let(:reporter) { report.reporter }
+ let!(:other_report) { create(:abuse_report, user: user) } # rubocop:disable RSpec/FactoryBot/AvoidCreate
+
+ let(:entity) do
+ described_class.new(report)
+ end
+
+ describe '#as_json' do
+ subject(:entity_hash) { entity.as_json }
+
+ it 'exposes correct attributes' do
+ expect(entity_hash.keys).to include(
+ :user,
+ :reporter,
+ :report,
+ :actions
+ )
+ end
+
+ it 'correctly exposes `user`', :aggregate_failures do
+ user_hash = entity_hash[:user]
+
+ expect(user_hash.keys).to match_array([
+ :name,
+ :username,
+ :avatar_url,
+ :email,
+ :created_at,
+ :last_activity_on,
+ :path,
+ :admin_path,
+ :plan,
+ :verification_state,
+ :other_reports,
+ :most_used_ip,
+ :last_sign_in_ip,
+ :snippets_count,
+ :groups_count,
+ :notes_count
+ ])
+
+ expect(user_hash[:verification_state].keys).to match_array([
+ :email,
+ :phone,
+ :credit_card
+ ])
+
+ expect(user_hash[:other_reports][0].keys).to match_array([
+ :created_at,
+ :category,
+ :report_path
+ ])
+ end
+
+ describe 'users plan' do
+ it 'does not include the plan' do
+ expect(entity_hash[:user][:plan]).to be_nil
+ end
+
+ context 'when on .com', :saas, if: Gitlab.ee? do
+ before do
+ stub_ee_application_setting(should_check_namespace_plan: true)
+ create(:namespace_with_plan, plan: :bronze_plan, owner: user) # rubocop:disable RSpec/FactoryBot/AvoidCreate
+ end
+
+ it 'includes the plan' do
+ expect(entity_hash[:user][:plan]).to eq('Bronze')
+ end
+ end
+ end
+
+ describe 'users credit card' do
+ let(:credit_card_hash) { entity_hash[:user][:credit_card] }
+
+ context 'when the user has no verified credit card' do
+ it 'does not expose the credit card' do
+ expect(credit_card_hash).to be_nil
+ end
+ end
+
+ context 'when the user does have a verified credit card' do
+ let!(:credit_card) { build_stubbed(:credit_card_validation, user: user) }
+
+ it 'exposes the credit card' do
+ expect(credit_card_hash.keys).to match_array([
+ :name,
+ :similar_records_count,
+ :card_matches_link
+ ])
+ end
+
+ context 'when not on ee', unless: Gitlab.ee? do
+ it 'does not include the path to the admin card matches page' do
+ expect(credit_card_hash[:card_matches_link]).to be_nil
+ end
+ end
+
+ context 'when on ee', if: Gitlab.ee? do
+ it 'includes the path to the admin card matches page' do
+ expect(credit_card_hash[:card_matches_link]).not_to be_nil
+ end
+ end
+ end
+ end
+
+ it 'correctly exposes `reporter`' do
+ reporter_hash = entity_hash[:reporter]
+
+ expect(reporter_hash.keys).to match_array([
+ :name,
+ :username,
+ :avatar_url,
+ :path
+ ])
+ end
+
+ it 'correctly exposes `report`' do
+ report_hash = entity_hash[:report]
+
+ expect(report_hash.keys).to match_array([
+ :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
+ ])
+ end
+ end
+end
diff --git a/spec/serializers/admin/abuse_report_details_serializer_spec.rb b/spec/serializers/admin/abuse_report_details_serializer_spec.rb
new file mode 100644
index 00000000000..f22d92a1763
--- /dev/null
+++ b/spec/serializers/admin/abuse_report_details_serializer_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Admin::AbuseReportDetailsSerializer, feature_category: :insider_threat do
+ let_it_be(:resource) { build_stubbed(:abuse_report) }
+
+ subject { described_class.new.represent(resource).keys }
+
+ describe '#represent' do
+ it 'serializes an abuse report' do
+ is_expected.to include(
+ :user,
+ :reporter,
+ :report,
+ :actions
+ )
+ end
+ end
+end
diff --git a/spec/serializers/admin/abuse_report_entity_spec.rb b/spec/serializers/admin/abuse_report_entity_spec.rb
index 760c12d3cf9..2101fc15dd0 100644
--- a/spec/serializers/admin/abuse_report_entity_spec.rb
+++ b/spec/serializers/admin/abuse_report_entity_spec.rb
@@ -33,7 +33,8 @@ RSpec.describe Admin::AbuseReportEntity, feature_category: :insider_threat do
:block_user_path,
:remove_report_path,
:remove_user_and_report_path,
- :message
+ :message,
+ :report_path
)
end
@@ -81,6 +82,10 @@ RSpec.describe Admin::AbuseReportEntity, feature_category: :insider_threat do
expect(entity_hash[:remove_report_path]).to eq admin_abuse_report_path(abuse_report)
end
+ it 'correctly exposes :report_path' do
+ expect(entity_hash[:report_path]).to eq admin_abuse_report_path(abuse_report)
+ end
+
it 'correctly exposes :remove_user_and_report_path' do
expect(entity_hash[:remove_user_and_report_path]).to eq admin_abuse_report_path(abuse_report, remove_user: true)
end
diff --git a/spec/services/preview_markdown_service_spec.rb b/spec/services/preview_markdown_service_spec.rb
index 97e31ec2cd3..6fa44310ae5 100644
--- a/spec/services/preview_markdown_service_spec.rb
+++ b/spec/services/preview_markdown_service_spec.rb
@@ -117,6 +117,16 @@ RSpec.describe PreviewMarkdownService, feature_category: :team_planning do
expect(result[:text]).to eq 'Please do it'
end
+ context 'when render_quick_actions' do
+ it 'keeps quick actions' do
+ params[:render_quick_actions] = true
+
+ result = service.execute
+
+ expect(result[:text]).to eq "Please do it\n\n/assign #{user.to_reference}"
+ end
+ end
+
it 'explains quick actions effect' do
result = service.execute
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index b8abfd1e6ba..b07aa7cc6c9 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -2517,8 +2517,9 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
let(:content) { '/close' }
it 'includes issuable name' do
- _, explanations = service.explain(content, issue)
+ content_result, explanations = service.explain(content, issue)
+ expect(content_result).to eq('')
expect(explanations).to eq(['Closes this issue.'])
end
end
@@ -2946,6 +2947,24 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
end
end
end
+
+ context 'with keep_actions' do
+ let(:content) { '/close' }
+
+ it 'keeps quick actions' do
+ content_result, explanations = service.explain(content, issue, keep_actions: true)
+
+ expect(content_result).to eq("\n/close")
+ expect(explanations).to eq(['Closes this issue.'])
+ end
+
+ it 'removes the quick action' do
+ content_result, explanations = service.explain(content, issue, keep_actions: false)
+
+ expect(content_result).to eq('')
+ expect(explanations).to eq(['Closes this issue.'])
+ end
+ end
end
describe '#available_commands' do