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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-12-09 18:10:12 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-12-09 18:10:12 +0300
commite91cb68359c900aa51ffdb1863502168742e94f0 (patch)
treeb7dd1749da6e2a11899905b4eae258236cd4f6a6 /spec
parent1361891b0a87187364d1586395df176a8984e914 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/boards/lists_controller_spec.rb10
-rw-r--r--spec/features/projects/environments/environments_spec.rb24
-rw-r--r--spec/fixtures/lib/gitlab/performance_bar/peek_data.json104
-rw-r--r--spec/frontend/__mocks__/@toast-ui/vue-editor/index.js17
-rw-r--r--spec/frontend/environments/environment_item_spec.js76
-rw-r--r--spec/frontend/environments/mock_data.js97
-rw-r--r--spec/frontend/static_site_editor/components/edit_area_spec.js13
-rw-r--r--spec/frontend/static_site_editor/pages/home_spec.js9
-rw-r--r--spec/frontend/static_site_editor/services/submit_content_changes_spec.js32
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js13
-rw-r--r--spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js64
-rw-r--r--spec/frontend/vue_shared/security_reports/mock_data.js119
-rw-r--r--spec/frontend/vue_shared/security_reports/security_reports_app_spec.js140
-rw-r--r--spec/frontend/vue_shared/security_reports/utils_spec.js28
-rw-r--r--spec/graphql/mutations/boards/lists/create_spec.rb5
-rw-r--r--spec/lib/api/entities/merge_request_basic_spec.rb27
-rw-r--r--spec/lib/gitlab/background_migration/populate_dismissed_state_for_vulnerabilities_spec.rb44
-rw-r--r--spec/lib/gitlab/i18n/po_linter_spec.rb55
-rw-r--r--spec/lib/gitlab/i18n/translation_entry_spec.rb112
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled_spec.rb64
-rw-r--r--spec/lib/gitlab/performance_bar/stats_spec.rb42
-rw-r--r--spec/services/boards/lists/create_service_spec.rb50
-rw-r--r--spec/workers/gitlab_performance_bar_stats_worker_spec.rb30
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