diff options
Diffstat (limited to 'spec')
24 files changed, 991 insertions, 185 deletions
diff --git a/spec/controllers/boards/lists_controller_spec.rb b/spec/controllers/boards/lists_controller_spec.rb index ceac280c297..29141582c6f 100644 --- a/spec/controllers/boards/lists_controller_spec.rb +++ b/spec/controllers/boards/lists_controller_spec.rb @@ -85,20 +85,22 @@ RSpec.describe Boards::ListsController do context 'with invalid params' do context 'when label is nil' do - it 'returns a not found 404 response' do + it 'returns an unprocessable entity 422 response' do create_board_list user: user, board: board, label_id: nil - expect(response).to have_gitlab_http_status(:not_found) + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response['errors']).to eq(['Label not found']) end end context 'when label that does not belongs to project' do - it 'returns a not found 404 response' do + it 'returns an unprocessable entity 422 response' do label = create(:label, name: 'Development') create_board_list user: user, board: board, label_id: label.id - expect(response).to have_gitlab_http_status(:not_found) + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response['errors']).to eq(['Label not found']) end end end diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index a265e0c28fc..b207d415ce0 100644 --- a/spec/features/projects/environments/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb @@ -24,6 +24,10 @@ RSpec.describe 'Environments page', :js do 'button[title="Stop environment"]' end + def upcoming_deployment_content_selector + '[data-testid="upcoming-deployment-content"]' + end + describe 'page tabs' do it 'shows "Available" and "Stopped" tab with links' do visit_environments(project) @@ -362,6 +366,26 @@ RSpec.describe 'Environments page', :js do expect(page).to have_content('No deployments yet') end end + + context 'when there is an upcoming deployment' do + let_it_be(:project) { create(:project, :repository) } + + let!(:deployment) do + create(:deployment, :running, + environment: environment, + sha: project.commit.id) + end + + it "renders the upcoming deployment", :aggregate_failures do + visit_environments(project) + + within(upcoming_deployment_content_selector) do + expect(page).to have_content("##{deployment.iid}") + expect(page).to have_selector("a[href=\"#{project_job_path(project, deployment.deployable)}\"]") + expect(page).to have_link(href: /#{deployment.user.username}/) + end + end + end end it 'does have a new environment button' do diff --git a/spec/fixtures/lib/gitlab/performance_bar/peek_data.json b/spec/fixtures/lib/gitlab/performance_bar/peek_data.json new file mode 100644 index 00000000000..8e207b69ecb --- /dev/null +++ b/spec/fixtures/lib/gitlab/performance_bar/peek_data.json @@ -0,0 +1,104 @@ +{ + "context": {}, + "data": { + "host": { + "hostname": "pc", + "canary": null + }, + "active-record": { + "duration": "6ms", + "calls": "7 (0 cached)", + "details": [ + { + "duration": 1.096, + "sql": "SELECT COUNT(*) FROM ((SELECT \"badges\".* FROM \"badges\" WHERE \"badges\".\"type\" = 'ProjectBadge' AND \"badges\".\"project_id\" = 8)\nUNION\n(SELECT \"badges\".* FROM \"badges\" WHERE \"badges\".\"type\" = 'GroupBadge' AND \"badges\".\"group_id\" IN (SELECT \"namespaces\".\"id\" FROM \"namespaces\" WHERE \"namespaces\".\"type\" = 'Group' AND \"namespaces\".\"id\" = 28))) badges", + "backtrace": [ + "lib/gitlab/pagination/offset_pagination.rb:53:in `add_pagination_headers'", + "lib/gitlab/pagination/offset_pagination.rb:15:in `block in paginate'", + "lib/gitlab/pagination/offset_pagination.rb:14:in `tap'", + "lib/gitlab/pagination/offset_pagination.rb:14:in `paginate'", + "lib/api/helpers/pagination.rb:7:in `paginate'", + "lib/api/badges.rb:42:in `block (3 levels) in <class:Badges>'", + "ee/lib/gitlab/ip_address_state.rb:10:in `with'", + "lib/api/api_guard.rb:208:in `call'", + "lib/gitlab/jira/middleware.rb:19:in `call'" + ], + "cached": "", + "warnings": [] + }, + { + "duration": 0.817, + "sql": "SELECT \"projects\".* FROM \"projects\" WHERE \"projects\".\"pending_delete\" = $1 AND \"projects\".\"id\" = $2 LIMIT $3", + "backtrace": [ + "lib/api/helpers.rb:112:in `find_project'", + "ee/lib/ee/api/helpers.rb:88:in `find_project!'", + "lib/api/helpers/members_helpers.rb:14:in `public_send'", + "lib/api/helpers/members_helpers.rb:14:in `find_source'", + "lib/api/badges.rb:36:in `block (3 levels) in <class:Badges>'", + "ee/lib/gitlab/ip_address_state.rb:10:in `with'", + "lib/api/api_guard.rb:208:in `call'", + "lib/gitlab/jira/middleware.rb:19:in `call'" + ], + "cached": "", + "warnings": [] + }, + { + "duration": 0.817, + "sql": "SELECT \"projects\".* FROM \"projects\" WHERE \"projects\".\"pending_delete\" = $1 AND \"projects\".\"id\" = $2 LIMIT $3", + "backtrace": [ + "lib/api/helpers.rb:112:in `find_project'", + "ee/lib/ee/api/helpers.rb:88:in `find_project!'", + "lib/api/helpers/members_helpers.rb:14:in `public_send'", + "lib/api/helpers/members_helpers.rb:14:in `find_source'", + "lib/api/badges.rb:36:in `block (3 levels) in <class:Badges>'", + "ee/lib/gitlab/ip_address_state.rb:10:in `with'", + "lib/api/api_guard.rb:208:in `call'", + "lib/gitlab/jira/middleware.rb:19:in `call'" + ], + "cached": "", + "warnings": [] + } + ], + "warnings": [] + }, + "gitaly": { + "duration": "0ms", + "calls": 0, + "details": [], + "warnings": [] + }, + "redis": { + "duration": "0ms", + "calls": 1, + "details": [ + { + "cmd": "get cache:gitlab:flipper/v1/feature/api_kaminari_count_with_limit", + "duration": 0.155, + "backtrace": [ + "lib/gitlab/instrumentation/redis_interceptor.rb:30:in `call'", + "lib/feature.rb:81:in `enabled?'", + "lib/gitlab/pagination/offset_pagination.rb:30:in `paginate_with_limit_optimization'", + "lib/gitlab/pagination/offset_pagination.rb:14:in `paginate'", + "lib/api/helpers/pagination.rb:7:in `paginate'", + "lib/api/badges.rb:42:in `block (3 levels) in <class:Badges>'", + "ee/lib/gitlab/ip_address_state.rb:10:in `with'", + "lib/api/api_guard.rb:208:in `call'", + "lib/gitlab/jira/middleware.rb:19:in `call'" + ], + "storage": "Cache", + "warnings": [], + "instance": "Cache" + } + ], + "warnings": [] + }, + "es": { + "duration": "0ms", + "calls": 0, + "details": [], + "warnings": [] + } + }, + "has_warnings": false +} + diff --git a/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js b/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js index 9fee8e18d26..2acd8111c77 100644 --- a/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js +++ b/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js @@ -1,3 +1,12 @@ +export const mockEditorApi = { + eventManager: { + addEventType: jest.fn(), + listen: jest.fn(), + removeEventHandler: jest.fn(), + }, + getMarkdown: jest.fn(), +}; + export const Editor = { props: { initialValue: { @@ -18,14 +27,6 @@ export const Editor = { }, }, created() { - const mockEditorApi = { - eventManager: { - addEventType: jest.fn(), - listen: jest.fn(), - removeEventHandler: jest.fn(), - }, - }; - this.$emit('load', mockEditorApi); }, render(h) { diff --git a/spec/frontend/environments/environment_item_spec.js b/spec/frontend/environments/environment_item_spec.js index 1b429783821..bc692352103 100644 --- a/spec/frontend/environments/environment_item_spec.js +++ b/spec/frontend/environments/environment_item_spec.js @@ -1,3 +1,4 @@ +import { cloneDeep } from 'lodash'; import { mount } from '@vue/test-utils'; import { format } from 'timeago.js'; import EnvironmentItem from '~/environments/components/environment_item.vue'; @@ -30,6 +31,11 @@ describe('Environment item', () => { }); const findAutoStop = () => wrapper.find('.js-auto-stop'); + const findUpcomingDeployment = () => wrapper.find('[data-testid="upcoming-deployment"]'); + const findUpcomingDeploymentContent = () => + wrapper.find('[data-testid="upcoming-deployment-content"]'); + const findUpcomingDeploymentStatusLink = () => + wrapper.find('[data-testid="upcoming-deployment-status-link"]'); afterEach(() => { wrapper.destroy(); @@ -87,6 +93,72 @@ describe('Environment item', () => { }); }); + describe('When the envionment has an upcoming deployment', () => { + describe('When the upcoming deployment has a deployable', () => { + it('should render the build ID and user', () => { + expect(findUpcomingDeploymentContent().text()).toMatchInterpolatedText( + '#27 by upcoming-username', + ); + }); + + it('should render a status icon with a link and tooltip', () => { + expect(findUpcomingDeploymentStatusLink().exists()).toBe(true); + + expect(findUpcomingDeploymentStatusLink().attributes().href).toBe( + '/root/environment-test/-/jobs/892', + ); + + expect(findUpcomingDeploymentStatusLink().attributes().title).toBe( + 'Deployment running', + ); + }); + }); + + describe('When the deployment does not have a deployable', () => { + beforeEach(() => { + const environmentWithoutDeployable = cloneDeep(environment); + delete environmentWithoutDeployable.upcoming_deployment.deployable; + + factory({ + propsData: { + model: environmentWithoutDeployable, + canReadEnvironment: true, + tableData, + }, + }); + }); + + it('should still renders the build ID and user', () => { + expect(findUpcomingDeploymentContent().text()).toMatchInterpolatedText( + '#27 by upcoming-username', + ); + }); + + it('should not render the status icon', () => { + expect(findUpcomingDeploymentStatusLink().exists()).toBe(false); + }); + }); + }); + + describe('Without upcoming deployment', () => { + beforeEach(() => { + const environmentWithoutUpcomingDeployment = cloneDeep(environment); + delete environmentWithoutUpcomingDeployment.upcoming_deployment; + + factory({ + propsData: { + model: environmentWithoutUpcomingDeployment, + canReadEnvironment: true, + tableData, + }, + }); + }); + + it('should not render anything in the upcoming deployment column', () => { + expect(findUpcomingDeploymentContent().exists()).toBe(false); + }); + }); + describe('Without auto-stop date', () => { beforeEach(() => { factory({ @@ -209,6 +281,10 @@ describe('Environment item', () => { it('should render the number of children in a badge', () => { expect(wrapper.find('.folder-name .badge').text()).toContain(folder.size); }); + + it('should not render the "Upcoming deployment" column', () => { + expect(findUpcomingDeployment().exists()).toBe(false); + }); }); describe('When environment can be deleted', () => { diff --git a/spec/frontend/environments/mock_data.js b/spec/frontend/environments/mock_data.js index 77c5dad0bbf..e7b99c8688c 100644 --- a/spec/frontend/environments/mock_data.js +++ b/spec/frontend/environments/mock_data.js @@ -86,6 +86,98 @@ const environment = { ], deployed_at: '2016-11-29T18:11:58.430Z', }, + upcoming_deployment: { + id: 82, + iid: 27, + sha: '1132df044b73943943c949e7ac2c2f120a89bf59', + ref: { + name: 'master', + ref_path: '/root/environment-test/-/tree/master', + }, + status: 'running', + created_at: '2020-12-04T19:57:49.514Z', + deployed_at: null, + tag: false, + 'last?': false, + user: { + id: 1, + name: 'Upcoming Name', + username: 'upcoming-username', + state: 'active', + avatar_url: 'http://0.0.0.0:3000/uploads/-/system/user/avatar/2/avatar.png', + web_url: 'http://0.0.0.0:3000/upcoming-username', + show_status: false, + path: '/upcoming-username', + }, + deployable: { + id: 1310, + name: 'deploy_to_development', + started: '2020-12-04T19:58:10.806Z', + archived: false, + build_path: '/root/environment-test/-/jobs/892', + cancel_path: + '/root/environment-test/-/jobs/892/cancel?continue%5Bto%5D=%2Froot%2Fenvironment-test%2F-%2Fjobs%2F892', + playable: false, + scheduled: false, + created_at: '2020-12-04T19:57:49.455Z', + updated_at: '2020-12-04T19:58:10.809Z', + status: { + icon: 'status_running', + text: 'running', + label: 'running', + group: 'running', + tooltip: 'running', + has_details: true, + details_path: '/root/environment-test/-/jobs/892', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-29a8a37d8a61d1b6f68cf3484f9024e53cd6eb95e28eae3554f8011a1146bf27.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_running-9c635b2419a8e1ec991c993061b89cc5aefc0743bb238ecd0c381e7741a70e8c.png', + action: { + icon: 'cancel', + title: 'Cancel', + path: '/root/environment-test/-/jobs/892/cancel', + method: 'post', + button_title: 'Cancel this job', + }, + }, + }, + commit: { + id: '1132df044b73943943c949e7ac2c2f120a89bf59', + short_id: '1132df04', + created_at: '2020-12-01T15:46:26.000-05:00', + parent_ids: ['e0808dee2a5877563ec140e65d8b41908f90098c'], + title: 'Update .gitlab-ci.yml', + message: 'Update .gitlab-ci.yml', + author_name: 'Upcoming Name', + author_email: 'admin@example.com', + authored_date: '2020-12-01T15:46:26.000-05:00', + committer_name: 'Upcoming Name', + committer_email: 'admin@example.com', + committed_date: '2020-12-01T15:46:26.000-05:00', + web_url: + 'http://0.0.0.0:3000/root/environment-test/-/commit/1132df044b73943943c949e7ac2c2f120a89bf59', + author: { + id: 1, + name: 'Upcoming Name', + username: 'upcoming-username', + state: 'active', + avatar_url: 'http://0.0.0.0:3000/uploads/-/system/user/avatar/2/avatar.png', + web_url: 'http://0.0.0.0:3000/upcoming-username', + show_status: false, + path: '/upcoming-username', + }, + author_gravatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + commit_url: + 'http://0.0.0.0:3000/root/environment-test/-/commit/1132df044b73943943c949e7ac2c2f120a89bf59', + commit_path: '/root/environment-test/-/commit/1132df044b73943943c949e7ac2c2f120a89bf59', + }, + }, has_stop_action: true, environment_path: 'root/ci-folders/environments/31', log_path: 'root/ci-folders/environments/31/logs', @@ -156,6 +248,11 @@ const tableData = { title: 'Updated', spacing: 'section-10', }, + upcoming: { + title: 'Upcoming', + mobileTitle: 'Upcoming deployment', + spacing: 'section-10', + }, autoStop: { title: 'Auto stop in', spacing: 'section-5', diff --git a/spec/frontend/static_site_editor/components/edit_area_spec.js b/spec/frontend/static_site_editor/components/edit_area_spec.js index 247aff57c1a..ed33f93ec51 100644 --- a/spec/frontend/static_site_editor/components/edit_area_spec.js +++ b/spec/frontend/static_site_editor/components/edit_area_spec.js @@ -250,4 +250,17 @@ describe('~/static_site_editor/components/edit_area.vue', () => { expect(wrapper.emitted('submit').length).toBe(1); }); }); + + describe('when RichContentEditor component triggers load event', () => { + it('stores formatted markdown provided in the event data', () => { + const data = { formattedMarkdown: 'formatted markdown' }; + + findRichContentEditor().vm.$emit('load', data); + + // We can access the formatted markdown when submitting changes + findPublishToolbar().vm.$emit('submit'); + + expect(wrapper.emitted('submit')[0][0]).toMatchObject(data); + }); + }); }); diff --git a/spec/frontend/static_site_editor/pages/home_spec.js b/spec/frontend/static_site_editor/pages/home_spec.js index d0b72ad0cf0..3e488a950dc 100644 --- a/spec/frontend/static_site_editor/pages/home_spec.js +++ b/spec/frontend/static_site_editor/pages/home_spec.js @@ -235,6 +235,7 @@ describe('static_site_editor/pages/home', () => { describe('when submitting changes succeeds', () => { const newContent = `new ${content}`; + const formattedMarkdown = `formatted ${content}`; beforeEach(() => { mutateMock.mockResolvedValueOnce(hasSubmittedChangesMutationPayload).mockResolvedValueOnce({ @@ -243,7 +244,12 @@ describe('static_site_editor/pages/home', () => { }, }); - buildWrapper({ content: newContent, images }); + buildWrapper(); + + findEditMetaModal().vm.show = jest.fn(); + + findEditArea().vm.$emit('submit', { content: newContent, images, formattedMarkdown }); + findEditMetaModal().vm.$emit('primary', mergeRequestMeta); return wrapper.vm.$nextTick(); @@ -266,6 +272,7 @@ describe('static_site_editor/pages/home', () => { variables: { input: { content: newContent, + formattedMarkdown, project, sourcePath, username, diff --git a/spec/frontend/static_site_editor/services/submit_content_changes_spec.js b/spec/frontend/static_site_editor/services/submit_content_changes_spec.js index b75c0b7df8c..6c2bff6740a 100644 --- a/spec/frontend/static_site_editor/services/submit_content_changes_spec.js +++ b/spec/frontend/static_site_editor/services/submit_content_changes_spec.js @@ -11,6 +11,8 @@ import { TRACKING_ACTION_CREATE_MERGE_REQUEST, USAGE_PING_TRACKING_ACTION_CREATE_COMMIT, USAGE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST, + DEFAULT_FORMATTING_CHANGES_COMMIT_MESSAGE, + DEFAULT_FORMATTING_CHANGES_COMMIT_DESCRIPTION, } from '~/static_site_editor/constants'; import generateBranchName from '~/static_site_editor/services/generate_branch_name'; import submitContentChanges from '~/static_site_editor/services/submit_content_changes'; @@ -81,6 +83,36 @@ describe('submitContentChanges', () => { ); }); + describe('committing markdown formatting changes', () => { + const formattedMarkdown = `formatted ${content}`; + const commitPayload = { + branch, + commit_message: `${DEFAULT_FORMATTING_CHANGES_COMMIT_MESSAGE}\n\n${DEFAULT_FORMATTING_CHANGES_COMMIT_DESCRIPTION}`, + actions: [ + { + action: 'update', + file_path: sourcePath, + content: formattedMarkdown, + }, + ], + }; + + it('commits markdown formatting changes in a separate commit', () => { + return submitContentChanges(buildPayload({ formattedMarkdown })).then(() => { + expect(Api.commitMultiple).toHaveBeenCalledWith(projectId, commitPayload); + }); + }); + + it('does not commit markdown formatting changes when there are none', () => { + return submitContentChanges(buildPayload()).then(() => { + expect(Api.commitMultiple.mock.calls).toHaveLength(1); + expect(Api.commitMultiple.mock.calls[0][1]).not.toMatchObject({ + actions: commitPayload.actions, + }); + }); + }); + }); + it('commits the content changes to the branch when creating branch succeeds', () => { return submitContentChanges(buildPayload()).then(() => { expect(Api.commitMultiple).toHaveBeenCalledWith(projectId, { diff --git a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js index d50cf2915e8..cd1157a1c2e 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js @@ -1,4 +1,5 @@ import { shallowMount } from '@vue/test-utils'; +import { mockEditorApi } from '@toast-ui/vue-editor'; import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue'; import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue'; import InsertVideoModal from '~/vue_shared/components/rich_content_editor/modals/insert_video_modal.vue'; @@ -114,10 +115,17 @@ describe('Rich Content Editor', () => { }); describe('when editor is loaded', () => { + const formattedMarkdown = 'formatted markdown'; + beforeEach(() => { + mockEditorApi.getMarkdown.mockReturnValueOnce(formattedMarkdown); buildWrapper(); }); + afterEach(() => { + mockEditorApi.getMarkdown.mockReset(); + }); + it('adds the CUSTOM_EVENTS.openAddImageModal custom event listener', () => { expect(addCustomEventListener).toHaveBeenCalledWith( wrapper.vm.editorApi, @@ -137,6 +145,11 @@ describe('Rich Content Editor', () => { it('registers HTML to markdown renderer', () => { expect(registerHTMLToMarkdownRenderer).toHaveBeenCalledWith(wrapper.vm.editorApi); }); + + it('emits load event with the markdown formatted by Toast UI', () => { + expect(mockEditorApi.getMarkdown).toHaveBeenCalled(); + expect(wrapper.emitted('load')[0]).toEqual([{ formattedMarkdown }]); + }); }); describe('when editor is destroyed', () => { diff --git a/spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js b/spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js new file mode 100644 index 00000000000..7e70407655a --- /dev/null +++ b/spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js @@ -0,0 +1,64 @@ +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue'; + +describe('SecurityReportDownloadDropdown component', () => { + let wrapper; + let artifacts; + + const createComponent = props => { + wrapper = shallowMount(SecurityReportDownloadDropdown, { + propsData: { ...props }, + }); + }; + + const findDropdown = () => wrapper.find(GlDropdown); + const findDropdownItems = () => wrapper.findAll(GlDropdownItem); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('given report artifacts', () => { + beforeEach(() => { + artifacts = [ + { + name: 'foo', + path: '/foo.json', + }, + { + name: 'bar', + path: '/bar.json', + }, + ]; + + createComponent({ artifacts }); + }); + + it('renders a dropdown', () => { + expect(findDropdown().props('loading')).toBe(false); + }); + + it('renders a dropdown items for each artifact', () => { + artifacts.forEach((artifact, i) => { + const item = findDropdownItems().at(i); + expect(item.text()).toContain(artifact.name); + expect(item.attributes()).toMatchObject({ + href: artifact.path, + download: expect.any(String), + }); + }); + }); + }); + + describe('given it is loading', () => { + beforeEach(() => { + createComponent({ artifacts: [], loading: true }); + }); + + it('renders a loading dropdown', () => { + expect(findDropdown().props('loading')).toBe(true); + }); + }); +}); diff --git a/spec/frontend/vue_shared/security_reports/mock_data.js b/spec/frontend/vue_shared/security_reports/mock_data.js index 9061d4586c5..e93ca8329e7 100644 --- a/spec/frontend/vue_shared/security_reports/mock_data.js +++ b/spec/frontend/vue_shared/security_reports/mock_data.js @@ -1,3 +1,8 @@ +import { + REPORT_TYPE_SAST, + REPORT_TYPE_SECRET_DETECTION, +} from '~/vue_shared/security_reports/constants'; + export const mockFindings = [ { id: null, @@ -316,3 +321,117 @@ export const secretScanningDiffSuccessMock = { base_report_out_of_date: false, head_report_created_at: '2020-01-10T10:00:00.000Z', }; + +export const securityReportDownloadPathsQueryResponse = { + project: { + mergeRequest: { + headPipeline: { + id: 'gid://gitlab/Ci::Pipeline/176', + jobs: { + nodes: [ + { + name: 'secret_detection', + artifacts: { + nodes: [ + { + downloadPath: + '/gitlab-org/secrets-detection-test/-/jobs/1399/artifacts/download?file_type=trace', + fileType: 'TRACE', + __typename: 'CiJobArtifact', + }, + { + downloadPath: + '/gitlab-org/secrets-detection-test/-/jobs/1399/artifacts/download?file_type=secret_detection', + fileType: 'SECRET_DETECTION', + __typename: 'CiJobArtifact', + }, + ], + __typename: 'CiJobArtifactConnection', + }, + __typename: 'CiJob', + }, + { + name: 'bandit-sast', + artifacts: { + nodes: [ + { + downloadPath: + '/gitlab-org/secrets-detection-test/-/jobs/1400/artifacts/download?file_type=trace', + fileType: 'TRACE', + __typename: 'CiJobArtifact', + }, + { + downloadPath: + '/gitlab-org/secrets-detection-test/-/jobs/1400/artifacts/download?file_type=sast', + fileType: 'SAST', + __typename: 'CiJobArtifact', + }, + ], + __typename: 'CiJobArtifactConnection', + }, + __typename: 'CiJob', + }, + { + name: 'eslint-sast', + artifacts: { + nodes: [ + { + downloadPath: + '/gitlab-org/secrets-detection-test/-/jobs/1401/artifacts/download?file_type=trace', + fileType: 'TRACE', + __typename: 'CiJobArtifact', + }, + { + downloadPath: + '/gitlab-org/secrets-detection-test/-/jobs/1401/artifacts/download?file_type=sast', + fileType: 'SAST', + __typename: 'CiJobArtifact', + }, + ], + __typename: 'CiJobArtifactConnection', + }, + __typename: 'CiJob', + }, + ], + __typename: 'CiJobConnection', + }, + __typename: 'Pipeline', + }, + __typename: 'MergeRequest', + }, + __typename: 'Project', + }, +}; + +/** + * These correspond to SAST jobs in the securityReportDownloadPathsQueryResponse above. + */ +export const sastArtifacts = [ + { + name: 'bandit-sast', + reportType: REPORT_TYPE_SAST, + path: '/gitlab-org/secrets-detection-test/-/jobs/1400/artifacts/download?file_type=sast', + }, + { + name: 'eslint-sast', + reportType: REPORT_TYPE_SAST, + path: '/gitlab-org/secrets-detection-test/-/jobs/1401/artifacts/download?file_type=sast', + }, +]; + +/** + * These correspond to Secret Detection jobs in the securityReportDownloadPathsQueryResponse above. + */ +export const secretDetectionArtifacts = [ + { + name: 'secret_detection', + reportType: REPORT_TYPE_SECRET_DETECTION, + path: + '/gitlab-org/secrets-detection-test/-/jobs/1399/artifacts/download?file_type=secret_detection', + }, +]; + +export const expectedDownloadDropdownProps = { + loading: false, + artifacts: [...secretDetectionArtifacts, ...sastArtifacts], +}; diff --git a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js index d11b519e5ad..38ba0ef0319 100644 --- a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js +++ b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js @@ -1,10 +1,14 @@ import { mount, createLocalVue } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import { merge } from 'lodash'; +import VueApollo from 'vue-apollo'; import Vuex from 'vuex'; +import createMockApollo from 'jest/helpers/mock_apollo_helper'; import { trimText } from 'helpers/text_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { + expectedDownloadDropdownProps, + securityReportDownloadPathsQueryResponse, sastDiffSuccessMock, secretScanningDiffSuccessMock, } from 'jest/vue_shared/security_reports/mock_data'; @@ -15,7 +19,9 @@ import { REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION, } from '~/vue_shared/security_reports/constants'; +import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue'; import SecurityReportsApp from '~/vue_shared/security_reports/security_reports_app.vue'; +import securityReportDownloadPathsQuery from '~/vue_shared/security_reports/queries/security_report_download_paths.query.graphql'; jest.mock('~/flash'); @@ -47,8 +53,20 @@ describe('Security reports app', () => { ); }; + const pendingHandler = () => new Promise(() => {}); + const successHandler = () => Promise.resolve({ data: securityReportDownloadPathsQueryResponse }); + const failureHandler = () => Promise.resolve({ errors: [{ message: 'some error' }] }); + const createMockApolloProvider = handler => { + localVue.use(VueApollo); + + const requestHandlers = [[securityReportDownloadPathsQuery, handler]]; + + return createMockApollo(requestHandlers); + }; + const anyParams = expect.any(Object); + const findDownloadDropdown = () => wrapper.find(SecurityReportDownloadDropdown); const findPipelinesTabAnchor = () => wrapper.find('[data-testid="show-pipelines"]'); const findHelpLink = () => wrapper.find('[data-testid="help"]'); const setupMockJobArtifact = reportType => { @@ -103,7 +121,9 @@ describe('Security reports app', () => { }); it('renders the expected message', () => { - expect(wrapper.text()).toMatchInterpolatedText(SecurityReportsApp.i18n.scansHaveRun); + expect(wrapper.text()).toMatchInterpolatedText( + SecurityReportsApp.i18n.scansHaveRunWithDownloadGuidance, + ); }); describe('clicking the anchor to the pipelines tab', () => { @@ -172,7 +192,9 @@ describe('Security reports app', () => { }); it('renders the expected message', () => { - expect(wrapper.text()).toMatchInterpolatedText(SecurityReportsApp.i18n.scansHaveRun); + expect(wrapper.text()).toMatchInterpolatedText( + SecurityReportsApp.i18n.scansHaveRunWithDownloadGuidance, + ); }); }); @@ -320,4 +342,118 @@ describe('Security reports app', () => { }, ); }); + + describe('given coreSecurityMrWidgetDownloads feature flag is enabled', () => { + const createComponentWithFlagEnabled = options => + createComponent( + merge(options, { + provide: { + glFeatures: { + coreSecurityMrWidgetDownloads: true, + }, + }, + }), + ); + + describe('given the query is loading', () => { + beforeEach(() => { + createComponentWithFlagEnabled({ + apolloProvider: createMockApolloProvider(pendingHandler), + }); + }); + + // TODO: Remove this assertion as part of + // https://gitlab.com/gitlab-org/gitlab/-/issues/273431 + it('initially renders nothing', () => { + expect(wrapper.isEmpty()).toBe(true); + }); + }); + + describe('given the query loads successfully', () => { + beforeEach(() => { + createComponentWithFlagEnabled({ + apolloProvider: createMockApolloProvider(successHandler), + }); + }); + + it('renders the download dropdown', () => { + expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps); + }); + + it('renders the expected message', () => { + const text = wrapper.text(); + expect(text).not.toContain(SecurityReportsApp.i18n.scansHaveRunWithDownloadGuidance); + expect(text).toContain(SecurityReportsApp.i18n.scansHaveRun); + }); + + it('should not render the pipeline tab anchor', () => { + expect(findPipelinesTabAnchor().exists()).toBe(false); + }); + }); + + describe('given the query fails', () => { + beforeEach(() => { + createComponentWithFlagEnabled({ + apolloProvider: createMockApolloProvider(failureHandler), + }); + }); + + it('calls createFlash correctly', () => { + expect(createFlash).toHaveBeenCalledWith({ + message: SecurityReportsApp.i18n.apiError, + captureError: true, + error: expect.any(Error), + }); + }); + + // TODO: Remove this assertion as part of + // https://gitlab.com/gitlab-org/gitlab/-/issues/273431 + it('renders nothing', () => { + expect(wrapper.isEmpty()).toBe(true); + }); + }); + }); + + describe('given coreSecurityMrWidgetCounts and coreSecurityMrWidgetDownloads feature flags are enabled', () => { + let mock; + + beforeEach(() => { + mock = new MockAdapter(axios); + mock.onGet(SAST_COMPARISON_PATH).replyOnce(200, sastDiffSuccessMock); + mock.onGet(SECRET_SCANNING_COMPARISON_PATH).replyOnce(200, secretScanningDiffSuccessMock); + createComponent({ + propsData: { + sastComparisonPath: SAST_COMPARISON_PATH, + secretScanningComparisonPath: SECRET_SCANNING_COMPARISON_PATH, + }, + provide: { + glFeatures: { + coreSecurityMrWidgetCounts: true, + coreSecurityMrWidgetDownloads: true, + }, + }, + apolloProvider: createMockApolloProvider(successHandler), + }); + + return waitForPromises(); + }); + + afterEach(() => { + mock.restore(); + }); + + it('renders the download dropdown', () => { + expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps); + }); + + it('renders the expected counts message', () => { + expect(trimText(wrapper.text())).toContain( + 'Security scanning detected 3 potential vulnerabilities 2 Critical 1 High and 0 Others', + ); + }); + + it('should not render the pipeline tab anchor', () => { + expect(findPipelinesTabAnchor().exists()).toBe(false); + }); + }); }); diff --git a/spec/frontend/vue_shared/security_reports/utils_spec.js b/spec/frontend/vue_shared/security_reports/utils_spec.js new file mode 100644 index 00000000000..ea54644796a --- /dev/null +++ b/spec/frontend/vue_shared/security_reports/utils_spec.js @@ -0,0 +1,28 @@ +import { extractSecurityReportArtifacts } from '~/vue_shared/security_reports/utils'; +import { + REPORT_TYPE_SAST, + REPORT_TYPE_SECRET_DETECTION, +} from '~/vue_shared/security_reports/constants'; +import { + securityReportDownloadPathsQueryResponse, + sastArtifacts, + secretDetectionArtifacts, +} from './mock_data'; + +describe('extractSecurityReportArtifacts', () => { + it.each` + reportTypes | expectedArtifacts + ${[]} | ${[]} + ${['foo']} | ${[]} + ${[REPORT_TYPE_SAST]} | ${sastArtifacts} + ${[REPORT_TYPE_SECRET_DETECTION]} | ${secretDetectionArtifacts} + ${[REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION]} | ${[...secretDetectionArtifacts, ...sastArtifacts]} + `( + 'returns the expected artifacts given report types $reportTypes', + ({ reportTypes, expectedArtifacts }) => { + expect( + extractSecurityReportArtifacts(reportTypes, securityReportDownloadPathsQueryResponse), + ).toEqual(expectedArtifacts); + }, + ); +}); diff --git a/spec/graphql/mutations/boards/lists/create_spec.rb b/spec/graphql/mutations/boards/lists/create_spec.rb index 7a638d11ed3..894dd1f34b4 100644 --- a/spec/graphql/mutations/boards/lists/create_spec.rb +++ b/spec/graphql/mutations/boards/lists/create_spec.rb @@ -68,9 +68,8 @@ RSpec.describe Mutations::Boards::Lists::Create do context 'when label not found' do let(:list_create_params) { { label_id: "gid://gitlab/Label/#{non_existing_record_id}" } } - it 'raises an error' do - expect { subject } - .to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'Label not found!') + it 'returns an error' do + expect(subject[:errors]).to include 'Label not found' end end end diff --git a/spec/lib/api/entities/merge_request_basic_spec.rb b/spec/lib/api/entities/merge_request_basic_spec.rb index 715fcf4bcdb..fe4c27b70ae 100644 --- a/spec/lib/api/entities/merge_request_basic_spec.rb +++ b/spec/lib/api/entities/merge_request_basic_spec.rb @@ -40,4 +40,31 @@ RSpec.describe ::API::Entities::MergeRequestBasic do expect(batch.count).to be_within(3 * query.count).of(control.count) end end + + context 'reviewers' do + context "when merge_request_reviewers FF is enabled" do + before do + stub_feature_flags(merge_request_reviewers: true) + merge_request.reviewers = [user] + end + + it 'includes assigned reviewers' do + result = Gitlab::Json.parse(present(merge_request).to_json) + + expect(result['reviewers'][0]['username']).to eq user.username + end + end + + context "when merge_request_reviewers FF is disabled" do + before do + stub_feature_flags(merge_request_reviewers: false) + end + + it 'does not include reviewers' do + result = Gitlab::Json.parse(present(merge_request).to_json) + + expect(result.keys).not_to include('reviewers') + end + end + end end diff --git a/spec/lib/gitlab/background_migration/populate_dismissed_state_for_vulnerabilities_spec.rb b/spec/lib/gitlab/background_migration/populate_dismissed_state_for_vulnerabilities_spec.rb new file mode 100644 index 00000000000..bc55f240a58 --- /dev/null +++ b/spec/lib/gitlab/background_migration/populate_dismissed_state_for_vulnerabilities_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Gitlab::BackgroundMigration::PopulateDismissedStateForVulnerabilities, schema: 2020_11_30_103926 do + let(:users) { table(:users) } + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:vulnerabilities) { table(:vulnerabilities) } + + let!(:namespace) { namespaces.create!(name: "foo", path: "bar") } + let!(:user) { users.create!(name: 'John Doe', email: 'test@example.com', projects_limit: 5) } + let!(:project) { projects.create!(namespace_id: namespace.id) } + let!(:vulnerability_params) do + { + project_id: project.id, + author_id: user.id, + title: 'Vulnerability', + severity: 5, + confidence: 5, + report_type: 5 + } + end + + let!(:vulnerability_1) { vulnerabilities.create!(vulnerability_params.merge(state: 1)) } + let!(:vulnerability_2) { vulnerabilities.create!(vulnerability_params.merge(state: 3)) } + + describe '#perform' do + it 'changes state of vulnerability to dismissed' do + subject.perform(vulnerability_1.id, vulnerability_2.id) + + expect(vulnerability_1.reload.state).to eq(2) + expect(vulnerability_2.reload.state).to eq(2) + end + + it 'populates missing dismissal information' do + expect_next_instance_of(::Gitlab::BackgroundMigration::PopulateMissingVulnerabilityDismissalInformation) do |migration| + expect(migration).to receive(:perform).with(vulnerability_1.id, vulnerability_2.id) + end + + subject.perform(vulnerability_1.id, vulnerability_2.id) + end + end +end diff --git a/spec/lib/gitlab/i18n/po_linter_spec.rb b/spec/lib/gitlab/i18n/po_linter_spec.rb index e04c0b49480..f2ee6bb72d9 100644 --- a/spec/lib/gitlab/i18n/po_linter_spec.rb +++ b/spec/lib/gitlab/i18n/po_linter_spec.rb @@ -6,7 +6,7 @@ require 'simple_po_parser' # Disabling this cop to allow for multi-language examples in comments # rubocop:disable Style/AsciiComments RSpec.describe Gitlab::I18n::PoLinter do - let(:linter) { described_class.new(po_path: po_path, html_todolist: {}) } + let(:linter) { described_class.new(po_path: po_path) } let(:po_path) { 'spec/fixtures/valid.po' } def fake_translation(msgid:, translation:, plural_id: nil, plurals: []) @@ -24,8 +24,7 @@ RSpec.describe Gitlab::I18n::PoLinter do Gitlab::I18n::TranslationEntry.new( entry_data: data, - nplurals: plurals.size + 1, - html_allowed: nil + nplurals: plurals.size + 1 ) end @@ -160,53 +159,6 @@ RSpec.describe Gitlab::I18n::PoLinter do ] end end - - context 'when an entry contains html on the todolist' do - subject(:linter) { described_class.new(po_path: po_path, html_todolist: todolist) } - - let(:po_path) { 'spec/fixtures/potential_html.po' } - let(:todolist) do - { - 'String with a legitimate < use' => { - 'plural_id' => 'String with lots of < > uses', - 'translations' => [ - 'Translated string with a legitimate < use', - 'Translated string with lots of < > uses' - ] - } - } - end - - it 'does not present an error' do - message_id = 'String with a legitimate < use' - - expect(errors[message_id]).to be_nil - end - end - - context 'when an entry on the html todolist has changed' do - subject(:linter) { described_class.new(po_path: po_path, html_todolist: todolist) } - - let(:po_path) { 'spec/fixtures/potential_html.po' } - let(:todolist) do - { - 'String with a legitimate < use' => { - 'plural_id' => 'String with lots of < > uses', - 'translations' => [ - 'Translated string with a different legitimate < use', - 'Translated string with lots of < > uses' - ] - } - } - end - - it 'presents an error for the changed component' do - message_id = 'String with a legitimate < use' - - expect(errors[message_id]) - .to include a_string_starting_with('translation contains < or >.') - end - end end describe '#parse_po' do @@ -276,8 +228,7 @@ RSpec.describe Gitlab::I18n::PoLinter do fake_entry = Gitlab::I18n::TranslationEntry.new( entry_data: { msgid: 'the singular', msgid_plural: 'the plural', 'msgstr[0]' => 'the singular' }, - nplurals: 2, - html_allowed: nil + nplurals: 2 ) errors = [] diff --git a/spec/lib/gitlab/i18n/translation_entry_spec.rb b/spec/lib/gitlab/i18n/translation_entry_spec.rb index 2c95b0b0124..f05346d07d3 100644 --- a/spec/lib/gitlab/i18n/translation_entry_spec.rb +++ b/spec/lib/gitlab/i18n/translation_entry_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do describe '#singular_translation' do it 'returns the normal `msgstr` for translations without plural' do data = { msgid: 'Hello world', msgstr: 'Bonjour monde' } - entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 2) expect(entry.singular_translation).to eq('Bonjour monde') end @@ -18,7 +18,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do 'msgstr[0]' => 'Bonjour monde', 'msgstr[1]' => 'Bonjour mondes' } - entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 2) expect(entry.singular_translation).to eq('Bonjour monde') end @@ -27,7 +27,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do describe '#all_translations' do it 'returns all translations for singular translations' do data = { msgid: 'Hello world', msgstr: 'Bonjour monde' } - entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 2) expect(entry.all_translations).to eq(['Bonjour monde']) end @@ -39,7 +39,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do 'msgstr[0]' => 'Bonjour monde', 'msgstr[1]' => 'Bonjour mondes' } - entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 2) expect(entry.all_translations).to eq(['Bonjour monde', 'Bonjour mondes']) end @@ -52,7 +52,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do msgid_plural: 'Hello worlds', 'msgstr[0]' => 'Bonjour monde' } - entry = described_class.new(entry_data: data, nplurals: 1, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 1) expect(entry.plural_translations).to eq(['Bonjour monde']) end @@ -65,7 +65,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do 'msgstr[1]' => 'Bonjour mondes', 'msgstr[2]' => 'Bonjour tous les mondes' } - entry = described_class.new(entry_data: data, nplurals: 3, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 3) expect(entry.plural_translations).to eq(['Bonjour mondes', 'Bonjour tous les mondes']) end @@ -77,7 +77,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do msgid: 'hello world', msgstr: 'hello' } - entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 2) expect(entry).to have_singular_translation end @@ -89,7 +89,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do "msgstr[0]" => 'hello world', "msgstr[1]" => 'hello worlds' } - entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 2) expect(entry).to have_singular_translation end @@ -100,7 +100,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do msgid_plural: 'hello worlds', "msgstr[0]" => 'hello worlds' } - entry = described_class.new(entry_data: data, nplurals: 1, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 1) expect(entry).not_to have_singular_translation end @@ -109,7 +109,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do describe '#msgid_contains_newlines' do it 'is true when the msgid is an array' do data = { msgid: %w(hello world) } - entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 2) expect(entry.msgid_has_multiple_lines?).to be_truthy end @@ -118,7 +118,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do describe '#plural_id_contains_newlines' do it 'is true when the msgid is an array' do data = { msgid_plural: %w(hello world) } - entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 2) expect(entry.plural_id_has_multiple_lines?).to be_truthy end @@ -127,7 +127,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do describe '#translations_contain_newlines' do it 'is true when the msgid is an array' do data = { msgstr: %w(hello world) } - entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 2) expect(entry.translations_have_multiple_lines?).to be_truthy end @@ -135,7 +135,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do describe '#contains_unescaped_chars' do let(:data) { { msgid: '' } } - let(:entry) { described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) } + let(:entry) { described_class.new(entry_data: data, nplurals: 2) } it 'is true when the msgid is an array' do string = '「100%確定」' @@ -177,7 +177,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do describe '#msgid_contains_unescaped_chars' do it 'is true when the msgid contains a `%`' do data = { msgid: '「100%確定」' } - entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 2) expect(entry).to receive(:contains_unescaped_chars?).and_call_original expect(entry.msgid_contains_unescaped_chars?).to be_truthy @@ -187,7 +187,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do describe '#plural_id_contains_unescaped_chars' do it 'is true when the plural msgid contains a `%`' do data = { msgid_plural: '「100%確定」' } - entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 2) expect(entry).to receive(:contains_unescaped_chars?).and_call_original expect(entry.plural_id_contains_unescaped_chars?).to be_truthy @@ -197,7 +197,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do describe '#translations_contain_unescaped_chars' do it 'is true when the translation contains a `%`' do data = { msgstr: '「100%確定」' } - entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 2) expect(entry).to receive(:contains_unescaped_chars?).and_call_original expect(entry.translations_contain_unescaped_chars?).to be_truthy @@ -205,7 +205,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do end describe '#msgid_contains_potential_html?' do - subject(:entry) { described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) } + subject(:entry) { described_class.new(entry_data: data, nplurals: 2) } context 'when there are no angle brackets in the msgid' do let(:data) { { msgid: 'String with no brackets' } } @@ -225,7 +225,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do end describe '#plural_id_contains_potential_html?' do - subject(:entry) { described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) } + subject(:entry) { described_class.new(entry_data: data, nplurals: 2) } context 'when there are no angle brackets in the plural_id' do let(:data) { { msgid_plural: 'String with no brackets' } } @@ -245,7 +245,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do end describe '#translations_contain_potential_html?' do - subject(:entry) { described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) } + subject(:entry) { described_class.new(entry_data: data, nplurals: 2) } context 'when there are no angle brackets in the translations' do let(:data) { { msgstr: 'This string has no angle brackets' } } @@ -263,78 +263,4 @@ RSpec.describe Gitlab::I18n::TranslationEntry do end end end - - describe '#msgid_html_allowed?' do - subject(:entry) do - described_class.new(entry_data: { msgid: 'String with a <strong>' }, nplurals: 2, html_allowed: html_todo) - end - - context 'when the html in the string is in the todolist' do - let(:html_todo) { { 'plural_id' => nil, 'translations' => [] } } - - it 'returns true' do - expect(entry.msgid_html_allowed?).to be true - end - end - - context 'when the html in the string is not in the todolist' do - let(:html_todo) { nil } - - it 'returns false' do - expect(entry.msgid_html_allowed?).to be false - end - end - end - - describe '#plural_id_html_allowed?' do - subject(:entry) do - described_class.new(entry_data: { msgid_plural: 'String with many <strong>' }, nplurals: 2, html_allowed: html_todo) - end - - context 'when the html in the string is in the todolist' do - let(:html_todo) { { 'plural_id' => 'String with many <strong>', 'translations' => [] } } - - it 'returns true' do - expect(entry.plural_id_html_allowed?).to be true - end - end - - context 'when the html in the string is not in the todolist' do - let(:html_todo) { { 'plural_id' => 'String with some <strong>', 'translations' => [] } } - - it 'returns false' do - expect(entry.plural_id_html_allowed?).to be false - end - end - end - - describe '#translations_html_allowed?' do - subject(:entry) do - described_class.new(entry_data: { msgstr: 'String with a <strong>' }, nplurals: 2, html_allowed: html_todo) - end - - context 'when the html in the string is in the todolist' do - let(:html_todo) { { 'plural_id' => nil, 'translations' => ['String with a <strong>'] } } - - it 'returns true' do - expect(entry.translations_html_allowed?).to be true - end - end - - context 'when the html in the string is not in the todolist' do - let(:html_todo) { { 'plural_id' => nil, 'translations' => ['String with a different <strong>'] } } - - it 'returns false' do - expect(entry.translations_html_allowed?).to be false - end - end - - context 'when the todolist only has the msgid' do - let(:html_todo) { { 'plural_id' => nil, 'translations' => nil } } - - it 'returns false' do - expect(entry.translations_html_allowed?).to be false - end - end - end end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 738dc19ab7b..cc565b93aea 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -656,6 +656,7 @@ boards: lists: - user - milestone +- iteration - board - label - list_user_preferences diff --git a/spec/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled_spec.rb b/spec/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled_spec.rb new file mode 100644 index 00000000000..bbc8b0d67e0 --- /dev/null +++ b/spec/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::PerformanceBar::RedisAdapterWhenPeekEnabled do + include ExclusiveLeaseHelpers + + let(:peek_adapter) do + Class.new do + prepend Gitlab::PerformanceBar::RedisAdapterWhenPeekEnabled + + def initialize(client) + @client = client + end + + def save(id) + # no-op + end + end + end + + describe '#save' do + let(:client) { double } + let(:uuid) { 'foo' } + + before do + allow(Gitlab::PerformanceBar).to receive(:enabled_for_request?).and_return(true) + end + + it 'stores request id and enqueues stats job' do + expect_to_obtain_exclusive_lease(GitlabPerformanceBarStatsWorker::LEASE_KEY, uuid) + expect(GitlabPerformanceBarStatsWorker).to receive(:perform_in).with(GitlabPerformanceBarStatsWorker::WORKER_DELAY, uuid) + expect(client).to receive(:sadd).with(GitlabPerformanceBarStatsWorker::STATS_KEY, uuid) + + peek_adapter.new(client).save('foo') + end + + context 'when performance_bar_stats is disabled' do + before do + stub_feature_flags(performance_bar_stats: false) + end + + it 'ignores stats processing for the request' do + expect(GitlabPerformanceBarStatsWorker).not_to receive(:perform_in) + expect(client).not_to receive(:sadd) + + peek_adapter.new(client).save('foo') + end + end + + context 'when exclusive lease has been already taken' do + before do + stub_exclusive_lease_taken(GitlabPerformanceBarStatsWorker::LEASE_KEY) + end + + it 'stores request id but does not enqueue any job' do + expect(GitlabPerformanceBarStatsWorker).not_to receive(:perform_in) + expect(client).to receive(:sadd).with(GitlabPerformanceBarStatsWorker::STATS_KEY, uuid) + + peek_adapter.new(client).save('foo') + end + end + end +end diff --git a/spec/lib/gitlab/performance_bar/stats_spec.rb b/spec/lib/gitlab/performance_bar/stats_spec.rb new file mode 100644 index 00000000000..c34c6f7b31f --- /dev/null +++ b/spec/lib/gitlab/performance_bar/stats_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::PerformanceBar::Stats do + describe '#process' do + let(:request) { fixture_file('lib/gitlab/performance_bar/peek_data.json') } + let(:redis) { double(Gitlab::Redis::SharedState) } + let(:logger) { double(Gitlab::PerformanceBar::Logger) } + let(:request_id) { 'foo' } + let(:stats) { described_class.new(redis) } + + describe '#process' do + subject(:process) { stats.process(request_id) } + + before do + allow(stats).to receive(:logger).and_return(logger) + end + + it 'logs each SQL query including its duration' do + allow(redis).to receive(:get).and_return(request) + + expect(logger).to receive(:info) + .with({ duration_ms: 1.096, filename: 'lib/gitlab/pagination/offset_pagination.rb', + filenum: 53, method: 'add_pagination_headers', request_id: 'foo', type: :sql }) + expect(logger).to receive(:info) + .with({ duration_ms: 0.817, filename: 'lib/api/helpers.rb', + filenum: 112, method: 'find_project', request_id: 'foo', type: :sql }).twice + + subject + end + + it 'logs an error when the request could not be processed' do + allow(redis).to receive(:get).and_return(nil) + + expect(logger).to receive(:error).with(message: anything) + + subject + end + end + end +end diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb index 88b6c3098d1..d639fdbb46a 100644 --- a/spec/services/boards/lists/create_service_spec.rb +++ b/spec/services/boards/lists/create_service_spec.rb @@ -5,27 +5,29 @@ require 'spec_helper' RSpec.describe Boards::Lists::CreateService do describe '#execute' do shared_examples 'creating board lists' do - let(:user) { create(:user) } + let_it_be(:user) { create(:user) } - subject(:service) { described_class.new(parent, user, label_id: label.id) } - - before do + before_all do parent.add_developer(user) end + subject(:service) { described_class.new(parent, user, label_id: label.id) } + context 'when board lists is empty' do it 'creates a new list at beginning of the list' do - list = service.execute(board) + response = service.execute(board) - expect(list.position).to eq 0 + expect(response.success?).to eq(true) + expect(response.payload[:list].position).to eq 0 end end context 'when board lists has the done list' do it 'creates a new list at beginning of the list' do - list = service.execute(board) + response = service.execute(board) - expect(list.position).to eq 0 + expect(response.success?).to eq(true) + expect(response.payload[:list].position).to eq 0 end end @@ -34,9 +36,10 @@ RSpec.describe Boards::Lists::CreateService do create(:list, board: board, position: 0) create(:list, board: board, position: 1) - list = service.execute(board) + response = service.execute(board) - expect(list.position).to eq 2 + expect(response.success?).to eq(true) + expect(response.payload[:list].position).to eq 2 end end @@ -44,32 +47,35 @@ RSpec.describe Boards::Lists::CreateService do it 'creates a new list at end of the label lists' do list1 = create(:list, board: board, position: 0) - list2 = service.execute(board) + list2 = service.execute(board).payload[:list] expect(list1.reload.position).to eq 0 expect(list2.reload.position).to eq 1 end end - context 'when provided label does not belongs to the parent' do - it 'raises an error' do + context 'when provided label does not belong to the parent' do + it 'returns an error' do label = create(:label, name: 'in-development') service = described_class.new(parent, user, label_id: label.id) - expect { service.execute(board) }.to raise_error(ActiveRecord::RecordNotFound) + response = service.execute(board) + + expect(response.success?).to eq(false) + expect(response.errors).to include('Label not found') end end context 'when backlog param is sent' do it 'creates one and only one backlog list' do service = described_class.new(parent, user, 'backlog' => true) - list = service.execute(board) + list = service.execute(board).payload[:list] expect(list.list_type).to eq('backlog') expect(list.position).to be_nil expect(list).to be_valid - another_backlog = service.execute(board) + another_backlog = service.execute(board).payload[:list] expect(another_backlog).to eq list end @@ -77,17 +83,17 @@ RSpec.describe Boards::Lists::CreateService do end context 'when board parent is a project' do - let(:parent) { create(:project) } - let(:board) { create(:board, project: parent) } - let(:label) { create(:label, project: parent, name: 'in-progress') } + let_it_be(:parent) { create(:project) } + let_it_be(:board) { create(:board, project: parent) } + let_it_be(:label) { create(:label, project: parent, name: 'in-progress') } it_behaves_like 'creating board lists' end context 'when board parent is a group' do - let(:parent) { create(:group) } - let(:board) { create(:board, group: parent) } - let(:label) { create(:group_label, group: parent, name: 'in-progress') } + let_it_be(:parent) { create(:group) } + let_it_be(:board) { create(:board, group: parent) } + let_it_be(:label) { create(:group_label, group: parent, name: 'in-progress') } it_behaves_like 'creating board lists' end diff --git a/spec/workers/gitlab_performance_bar_stats_worker_spec.rb b/spec/workers/gitlab_performance_bar_stats_worker_spec.rb new file mode 100644 index 00000000000..367003dd1ad --- /dev/null +++ b/spec/workers/gitlab_performance_bar_stats_worker_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabPerformanceBarStatsWorker do + include ExclusiveLeaseHelpers + + subject(:worker) { described_class.new } + + describe '#perform' do + let(:redis) { double(Gitlab::Redis::SharedState) } + let(:uuid) { 1 } + + before do + expect(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis) + expect_to_cancel_exclusive_lease(GitlabPerformanceBarStatsWorker::LEASE_KEY, uuid) + end + + it 'fetches list of request ids and processes them' do + expect(redis).to receive(:smembers).with(GitlabPerformanceBarStatsWorker::STATS_KEY).and_return([1, 2]) + expect(redis).to receive(:del).with(GitlabPerformanceBarStatsWorker::STATS_KEY) + expect_next_instance_of(Gitlab::PerformanceBar::Stats) do |stats| + expect(stats).to receive(:process).with(1) + expect(stats).to receive(:process).with(2) + end + + worker.perform(uuid) + end + end +end |