diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-02 15:16:59 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-02 15:16:59 +0000 |
commit | 6f991190fe4dbb93070b090a9a31d71b25e8101d (patch) | |
tree | 0805552c79613c87d5e99c08f9a588d3cfe6f3c5 /spec | |
parent | 51d59a3538b97d85ebb46039044d3f498809b55a (diff) | |
download | gitlab-ce-6f991190fe4dbb93070b090a9a31d71b25e8101d.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
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 |