From 871b886a1794e5baefd6b2f96caf2ac4ce5da6ca Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 11 Jul 2023 09:10:29 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .rubocop_todo/rspec/described_class.yml | 46 --- .../components/grouped_issues_list.vue | 106 ------- .../ci/reports/components/summary_row.vue | 93 ------ .../javascripts/ci/reports/constants.js | 2 - .../merge_request_artifact_download.vue | 91 ------ .../security_reports/components/constants.js | 8 - .../security_reports/components/help_icon.vue | 58 ---- .../components/security_summary.vue | 59 ---- .../vue_shared/security_reports/constants.js | 1 - .../security_reports/security_reports_app.vue | 238 ---------------- .../security_reports/store/constants.js | 7 - .../security_reports/store/getters.js | 66 ----- .../security_reports/store/index.js | 16 -- .../security_reports/store/messages.js | 4 - .../store/modules/sast/actions.js | 26 -- .../store/modules/sast/index.js | 10 - .../store/modules/sast/mutation_types.js | 4 - .../store/modules/sast/mutations.js | 31 -- .../store/modules/sast/state.js | 14 - .../store/modules/secret_detection/actions.js | 26 -- .../store/modules/secret_detection/index.js | 10 - .../secret_detection/mutation_types.js | 4 - .../modules/secret_detection/mutations.js | 30 -- .../store/modules/secret_detection/state.js | 14 - .../security_reports/store/state.js | 5 - .../security_reports/store/utils.js | 154 ---------- .../vue_shared/security_reports/utils.js | 10 - .../admin/application_settings_controller.rb | 10 +- app/mailers/emails/issues.rb | 2 +- app/mailers/emails/notes.rb | 15 +- app/models/milestone.rb | 8 + app/models/user_custom_attribute.rb | 1 + app/serializers/prometheus_alert_entity.rb | 23 -- .../prometheus_alert_serializer.rb | 5 - app/services/milestones/create_service.rb | 8 + app/services/milestones/update_service.rb | 13 +- .../application_settings/_slack.html.haml | 52 +++- .../application_settings/general.html.haml | 2 +- ...ynamically_compute_deployment_approval.yml | 8 + .../key_set_optimizer_ignored_columns.yml | 8 + .../remove_deployments_api_ref_sort.yml | 2 +- .../development/slack_app_self_managed.yml | 8 + config/routes/admin.rb | 3 +- doc/administration/audit_events.md | 2 +- doc/api/graphql/reference/index.md | 22 +- .../runner_admission_controller/index.md | 6 +- doc/user/admin_area/index.md | 65 ++++- doc/user/admin_area/settings/index.md | 5 +- doc/user/admin_area/settings/slack_app.md | 108 +++++++ doc/user/profile/index.md | 6 +- doc/user/profile/notifications.md | 12 +- doc/user/profile/personal_access_tokens.md | 6 +- doc/user/profile/preferences.md | 2 +- .../integrations/gitlab_slack_application.md | 73 ++--- doc/user/project/merge_requests/changes.md | 2 +- .../project/repository/code_suggestions.md | 2 +- .../repository/gpg_signed_commits/index.md | 6 +- .../repository/ssh_signed_commits/index.md | 2 +- doc/user/project/working_with_projects.md | 97 ++++--- .../file_size_check/any_oversized_blob.rb | 24 ++ .../in_operator_optimization/query_builder.rb | 6 +- .../strategies/record_loader_strategy.rb | 7 +- lib/gitlab/pagination/keyset/order.rb | 7 +- lib/slack/manifest.rb | 114 ++++++++ locale/gitlab.pot | 190 ++++++------- rubocop/cop/ignored_columns.rb | 4 +- spec/config/settings_spec.rb | 4 +- .../application_settings_controller_spec.rb | 37 +++ .../repositories/git_http_controller_spec.rb | 10 +- .../application_experiment_spec.rb | 2 +- spec/features/admin/admin_settings_spec.rb | 47 ++- spec/fixtures/api/schemas/slack/manifest.json | 164 +++++++++++ .../grouped_issues_list_spec.js.snap | 26 -- .../components/grouped_issues_list_spec.js | 83 ------ .../ci/reports/components/summary_row_spec.js | 63 ----- spec/frontend/fixtures/timezones.rb | 2 +- .../security_summary_spec.js.snap | 144 ---------- .../merge_request_artifact_download_spec.js | 104 ------- .../security_reports/help_icon_spec.js | 63 ----- .../security_reports/security_summary_spec.js | 33 --- .../vue_shared/security_reports/mock_data.js | 136 --------- .../security_reports_app_spec.js | 267 ------------------ .../security_reports/store/getters_spec.js | 182 ------------ .../store/modules/sast/actions_spec.js | 197 ------------- .../store/modules/sast/mutations_spec.js | 84 ------ .../modules/secret_detection/actions_spec.js | 198 ------------- .../secret_detection/mutations_spec.js | 84 ------ .../security_reports/store/utils_spec.js | 63 ----- .../vue_shared/security_reports/utils_spec.js | 48 ---- spec/graphql/gitlab_schema_spec.rb | 2 +- spec/graphql/graphql_triggers_spec.rb | 22 +- spec/graphql/types/global_id_type_spec.rb | 6 +- spec/initializers/google_api_client_spec.rb | 2 +- .../any_oversized_blob_spec.rb | 32 +++ spec/lib/gitlab/import_export/all_models.yml | 1 + .../query_builder_spec.rb | 71 ++++- .../strategies/record_loader_strategy_spec.rb | 42 ++- .../gitlab/pagination/keyset/order_spec.rb | 40 +++ spec/lib/slack/manifest_spec.rb | 96 +++++++ spec/mailers/notify_spec.rb | 26 +- spec/models/milestone_spec.rb | 42 ++- spec/rubocop/cop/ignored_columns_spec.rb | 8 +- .../prometheus_alert_entity_spec.rb | 22 -- .../milestones/create_service_spec.rb | 74 ++++- .../milestones/update_service_spec.rb | 90 ++++-- .../_slack.html.haml_spec.rb | 42 +++ 106 files changed, 1444 insertions(+), 3284 deletions(-) delete mode 100644 app/assets/javascripts/ci/reports/components/grouped_issues_list.vue delete mode 100644 app/assets/javascripts/ci/reports/components/summary_row.vue delete mode 100644 app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue delete mode 100644 app/assets/javascripts/vue_shared/security_reports/components/constants.js delete mode 100644 app/assets/javascripts/vue_shared/security_reports/components/help_icon.vue delete mode 100644 app/assets/javascripts/vue_shared/security_reports/components/security_summary.vue delete mode 100644 app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue delete mode 100644 app/assets/javascripts/vue_shared/security_reports/store/constants.js delete mode 100644 app/assets/javascripts/vue_shared/security_reports/store/getters.js delete mode 100644 app/assets/javascripts/vue_shared/security_reports/store/index.js delete mode 100644 app/assets/javascripts/vue_shared/security_reports/store/messages.js delete mode 100644 app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js delete mode 100644 app/assets/javascripts/vue_shared/security_reports/store/modules/sast/index.js delete mode 100644 app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutation_types.js delete mode 100644 app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutations.js delete mode 100644 app/assets/javascripts/vue_shared/security_reports/store/modules/sast/state.js delete mode 100644 app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/actions.js delete mode 100644 app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/index.js delete mode 100644 app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/mutation_types.js delete mode 100644 app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/mutations.js delete mode 100644 app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/state.js delete mode 100644 app/assets/javascripts/vue_shared/security_reports/store/state.js delete mode 100644 app/assets/javascripts/vue_shared/security_reports/store/utils.js delete mode 100644 app/serializers/prometheus_alert_entity.rb delete mode 100644 app/serializers/prometheus_alert_serializer.rb create mode 100644 config/feature_flags/development/dynamically_compute_deployment_approval.yml create mode 100644 config/feature_flags/development/key_set_optimizer_ignored_columns.yml create mode 100644 config/feature_flags/development/slack_app_self_managed.yml create mode 100644 doc/user/admin_area/settings/slack_app.md create mode 100644 lib/gitlab/checks/file_size_check/any_oversized_blob.rb create mode 100644 lib/slack/manifest.rb create mode 100644 spec/fixtures/api/schemas/slack/manifest.json delete mode 100644 spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap delete mode 100644 spec/frontend/ci/reports/components/grouped_issues_list_spec.js delete mode 100644 spec/frontend/ci/reports/components/summary_row_spec.js delete mode 100644 spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap delete mode 100644 spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js delete mode 100644 spec/frontend/vue_shared/components/security_reports/help_icon_spec.js delete mode 100644 spec/frontend/vue_shared/components/security_reports/security_summary_spec.js delete mode 100644 spec/frontend/vue_shared/security_reports/security_reports_app_spec.js delete mode 100644 spec/frontend/vue_shared/security_reports/store/getters_spec.js delete mode 100644 spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js delete mode 100644 spec/frontend/vue_shared/security_reports/store/modules/sast/mutations_spec.js delete mode 100644 spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js delete mode 100644 spec/frontend/vue_shared/security_reports/store/modules/secret_detection/mutations_spec.js delete mode 100644 spec/frontend/vue_shared/security_reports/store/utils_spec.js delete mode 100644 spec/frontend/vue_shared/security_reports/utils_spec.js create mode 100644 spec/lib/gitlab/checks/file_size_check/any_oversized_blob_spec.rb create mode 100644 spec/lib/slack/manifest_spec.rb delete mode 100644 spec/serializers/prometheus_alert_entity_spec.rb create mode 100644 spec/views/admin/application_settings/_slack.html.haml_spec.rb diff --git a/.rubocop_todo/rspec/described_class.yml b/.rubocop_todo/rspec/described_class.yml index 511f47549ee..5868749048a 100644 --- a/.rubocop_todo/rspec/described_class.yml +++ b/.rubocop_todo/rspec/described_class.yml @@ -2,55 +2,9 @@ # Cop supports --autocorrect. RSpec/DescribedClass: Exclude: - - 'ee/spec/models/concerns/elastic/merge_request_spec.rb' - - 'ee/spec/models/concerns/elastic/note_spec.rb' - - 'ee/spec/models/concerns/elastic/project_spec.rb' - - 'ee/spec/models/concerns/elastic/repository_spec.rb' - - 'ee/spec/models/dast_scanner_profile_spec.rb' - - 'ee/spec/models/dast_site_profile_spec.rb' - - 'ee/spec/models/ee/ci/job_artifact_spec.rb' - - 'ee/spec/models/ee/ci/pending_build_spec.rb' - - 'ee/spec/models/ee/ci/runner_spec.rb' - - 'ee/spec/models/ee/gpg_key_spec.rb' - - 'ee/spec/models/ee/group_spec.rb' - - 'ee/spec/models/ee/project_spec.rb' - - 'ee/spec/models/ee/vulnerability_spec.rb' - - 'ee/spec/models/epic_issue_spec.rb' - - 'ee/spec/models/epic_spec.rb' - - 'ee/spec/models/geo/container_repository_registry_spec.rb' - - 'ee/spec/models/geo/design_registry_spec.rb' - - 'ee/spec/models/geo/package_file_registry_spec.rb' - - 'ee/spec/models/geo/project_registry_spec.rb' - - 'ee/spec/models/geo/secondary_usage_data_spec.rb' - - 'ee/spec/models/issuable_metric_image_spec.rb' - - 'ee/spec/models/issue_spec.rb' - - 'ee/spec/models/iteration_spec.rb' - - 'ee/spec/models/license_spec.rb' - - 'ee/spec/models/project_import_state_spec.rb' - - 'ee/spec/models/release_highlight_spec.rb' - - 'ee/spec/models/requirements_management/test_report_spec.rb' - - 'ee/spec/models/resource_weight_event_spec.rb' - - 'ee/spec/models/uploads/local_spec.rb' - - 'ee/spec/models/vulnerabilities/flag_spec.rb' - - 'ee/spec/services/arkose/blocked_users_report_service_spec.rb' - - 'ee/spec/services/ee/resource_events/synthetic_weight_notes_builder_service_spec.rb' - - 'ee/spec/services/ee/users/reject_service_spec.rb' - - 'ee/spec/services/security/ingestion/tasks/update_vulnerability_uuids_spec.rb' - - 'ee/spec/services/users/captcha_challenge_service_spec.rb' - - 'ee/spec/workers/concerns/elastic/indexing_control_spec.rb' - - 'ee/spec/workers/geo/secondary/registry_consistency_worker_spec.rb' - - 'ee/spec/workers/geo/verification_state_backfill_worker_spec.rb' - 'qa/spec/service/docker_run/base_spec.rb' - 'qa/spec/support/loglinking_spec.rb' - 'qa/spec/support/page_error_checker_spec.rb' - - 'spec/config/settings_spec.rb' - - 'spec/controllers/repositories/git_http_controller_spec.rb' - - 'spec/experiments/application_experiment_spec.rb' - - 'spec/frontend/fixtures/timezones.rb' - - 'spec/graphql/gitlab_schema_spec.rb' - - 'spec/graphql/graphql_triggers_spec.rb' - - 'spec/graphql/types/global_id_type_spec.rb' - - 'spec/initializers/google_api_client_spec.rb' - 'spec/lib/feature_spec.rb' - 'spec/lib/gitlab/ci/variables/collection/item_spec.rb' - 'spec/lib/gitlab/git/repository_spec.rb' diff --git a/app/assets/javascripts/ci/reports/components/grouped_issues_list.vue b/app/assets/javascripts/ci/reports/components/grouped_issues_list.vue deleted file mode 100644 index b21a486e259..00000000000 --- a/app/assets/javascripts/ci/reports/components/grouped_issues_list.vue +++ /dev/null @@ -1,106 +0,0 @@ - - - diff --git a/app/assets/javascripts/ci/reports/components/summary_row.vue b/app/assets/javascripts/ci/reports/components/summary_row.vue deleted file mode 100644 index ee55368c829..00000000000 --- a/app/assets/javascripts/ci/reports/components/summary_row.vue +++ /dev/null @@ -1,93 +0,0 @@ - - diff --git a/app/assets/javascripts/ci/reports/constants.js b/app/assets/javascripts/ci/reports/constants.js index 1137236d355..b24a299a7ed 100644 --- a/app/assets/javascripts/ci/reports/constants.js +++ b/app/assets/javascripts/ci/reports/constants.js @@ -7,8 +7,6 @@ export const STATUS_SUCCESS = 'success'; export const STATUS_NEUTRAL = 'neutral'; export const STATUS_NOT_FOUND = 'not_found'; -export const ICON_WARNING = 'warning'; - export const status = { LOADING, ERROR, diff --git a/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue b/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue deleted file mode 100644 index 4c2b082242b..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue +++ /dev/null @@ -1,91 +0,0 @@ - - - diff --git a/app/assets/javascripts/vue_shared/security_reports/components/constants.js b/app/assets/javascripts/vue_shared/security_reports/components/constants.js deleted file mode 100644 index 5e8199c1bcd..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/components/constants.js +++ /dev/null @@ -1,8 +0,0 @@ -export const SEVERITY_CLASS_NAME_MAP = { - critical: 'gl-text-red-800', - high: 'gl-text-red-600', - medium: 'gl-text-orange-400', - low: 'gl-text-orange-300', - info: 'gl-text-blue-400', - unknown: 'gl-text-gray-400', -}; diff --git a/app/assets/javascripts/vue_shared/security_reports/components/help_icon.vue b/app/assets/javascripts/vue_shared/security_reports/components/help_icon.vue deleted file mode 100644 index eed1c86c318..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/components/help_icon.vue +++ /dev/null @@ -1,58 +0,0 @@ - - - diff --git a/app/assets/javascripts/vue_shared/security_reports/components/security_summary.vue b/app/assets/javascripts/vue_shared/security_reports/components/security_summary.vue deleted file mode 100644 index e3aa25a294e..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/components/security_summary.vue +++ /dev/null @@ -1,59 +0,0 @@ - - - diff --git a/app/assets/javascripts/vue_shared/security_reports/constants.js b/app/assets/javascripts/vue_shared/security_reports/constants.js index a1d75e08be9..56c6affebd7 100644 --- a/app/assets/javascripts/vue_shared/security_reports/constants.js +++ b/app/assets/javascripts/vue_shared/security_reports/constants.js @@ -28,7 +28,6 @@ export const REPORT_TYPE_CLUSTER_IMAGE_SCANNING = 'cluster_image_scanning'; export const REPORT_TYPE_COVERAGE_FUZZING = 'coverage_fuzzing'; export const REPORT_TYPE_CORPUS_MANAGEMENT = 'corpus_management'; export const REPORT_TYPE_API_FUZZING = 'api_fuzzing'; -export const REPORT_TYPE_MANUALLY_ADDED = 'generic'; /** * SecurityReportTypeEnum values for use with GraphQL. diff --git a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue deleted file mode 100644 index 0cff5edf628..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue +++ /dev/null @@ -1,238 +0,0 @@ - - diff --git a/app/assets/javascripts/vue_shared/security_reports/store/constants.js b/app/assets/javascripts/vue_shared/security_reports/store/constants.js deleted file mode 100644 index 6aeab56eea2..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/constants.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Vuex module names corresponding to security scan types. These are similar to - * the snake_case report types from the backend, but should not be considered - * to be equivalent. - */ -export const MODULE_SAST = 'sast'; -export const MODULE_SECRET_DETECTION = 'secretDetection'; diff --git a/app/assets/javascripts/vue_shared/security_reports/store/getters.js b/app/assets/javascripts/vue_shared/security_reports/store/getters.js deleted file mode 100644 index c274f531139..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/getters.js +++ /dev/null @@ -1,66 +0,0 @@ -import { s__, sprintf } from '~/locale'; -import { LOADING, ERROR, SUCCESS } from '~/ci/reports/constants'; -import { TRANSLATION_IS_LOADING } from './messages'; -import { countVulnerabilities, groupedTextBuilder } from './utils'; - -export const summaryCounts = (state) => - countVulnerabilities( - state.reportTypes.reduce((acc, reportType) => { - acc.push(...state[reportType].newIssues); - return acc; - }, []), - ); - -export const groupedSummaryText = (state, getters) => { - const reportType = s__('ciReport|Security scanning'); - let status = ''; - - // All reports are loading - if (getters.areAllReportsLoading) { - return { message: sprintf(TRANSLATION_IS_LOADING, { reportType }) }; - } - - // All reports returned error - if (getters.allReportsHaveError) { - return { message: s__('ciReport|Security scanning failed loading any results') }; - } - - if (getters.areReportsLoading && getters.anyReportHasError) { - status = s__('ciReport|is loading, errors when loading results'); - } else if (getters.areReportsLoading && !getters.anyReportHasError) { - status = s__('ciReport|is loading'); - } else if (!getters.areReportsLoading && getters.anyReportHasError) { - status = s__('ciReport|: Loading resulted in an error'); - } - - const { critical, high, other } = getters.summaryCounts; - - return groupedTextBuilder({ reportType, status, critical, high, other }); -}; - -export const summaryStatus = (state, getters) => { - if (getters.areReportsLoading) { - return LOADING; - } - - if (getters.anyReportHasError || getters.anyReportHasIssues) { - return ERROR; - } - - return SUCCESS; -}; - -export const areReportsLoading = (state) => - state.reportTypes.some((reportType) => state[reportType].isLoading); - -export const areAllReportsLoading = (state) => - state.reportTypes.every((reportType) => state[reportType].isLoading); - -export const allReportsHaveError = (state) => - state.reportTypes.every((reportType) => state[reportType].hasError); - -export const anyReportHasError = (state) => - state.reportTypes.some((reportType) => state[reportType].hasError); - -export const anyReportHasIssues = (state) => - state.reportTypes.some((reportType) => state[reportType].newIssues.length > 0); diff --git a/app/assets/javascripts/vue_shared/security_reports/store/index.js b/app/assets/javascripts/vue_shared/security_reports/store/index.js deleted file mode 100644 index 164faa86744..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/index.js +++ /dev/null @@ -1,16 +0,0 @@ -import Vuex from 'vuex'; -import { MODULE_SAST, MODULE_SECRET_DETECTION } from './constants'; -import * as getters from './getters'; -import sast from './modules/sast'; -import secretDetection from './modules/secret_detection'; -import state from './state'; - -export default () => - new Vuex.Store({ - modules: { - [MODULE_SAST]: sast, - [MODULE_SECRET_DETECTION]: secretDetection, - }, - getters, - state, - }); diff --git a/app/assets/javascripts/vue_shared/security_reports/store/messages.js b/app/assets/javascripts/vue_shared/security_reports/store/messages.js deleted file mode 100644 index c25e252a768..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/messages.js +++ /dev/null @@ -1,4 +0,0 @@ -import { s__ } from '~/locale'; - -export const TRANSLATION_IS_LOADING = s__('ciReport|%{reportType} is loading'); -export const TRANSLATION_HAS_ERROR = s__('ciReport|%{reportType}: Loading resulted in an error'); diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js deleted file mode 100644 index 8aefc13a5fa..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js +++ /dev/null @@ -1,26 +0,0 @@ -import { REPORT_TYPE_SAST } from '~/vue_shared/security_reports/constants'; -import { fetchDiffData } from '../../utils'; -import * as types from './mutation_types'; - -export const setDiffEndpoint = ({ commit }, path) => commit(types.SET_DIFF_ENDPOINT, path); - -export const requestDiff = ({ commit }) => commit(types.REQUEST_DIFF); - -export const receiveDiffSuccess = ({ commit }, response) => - commit(types.RECEIVE_DIFF_SUCCESS, response); - -export const receiveDiffError = ({ commit }, response) => - commit(types.RECEIVE_DIFF_ERROR, response); - -export const fetchDiff = ({ state, rootState, dispatch }) => { - dispatch('requestDiff'); - - return fetchDiffData(rootState, state.paths.diffEndpoint, REPORT_TYPE_SAST) - .then((data) => { - dispatch('receiveDiffSuccess', data); - return data; - }) - .catch(() => { - dispatch('receiveDiffError'); - }); -}; diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/index.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/index.js deleted file mode 100644 index 1d5af1d4fe5..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/index.js +++ /dev/null @@ -1,10 +0,0 @@ -import * as actions from './actions'; -import mutations from './mutations'; -import state from './state'; - -export default { - namespaced: true, - state, - mutations, - actions, -}; diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutation_types.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutation_types.js deleted file mode 100644 index aacec0fb679..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutation_types.js +++ /dev/null @@ -1,4 +0,0 @@ -export const RECEIVE_DIFF_SUCCESS = 'RECEIVE_DIFF_SUCCESS'; -export const RECEIVE_DIFF_ERROR = 'RECEIVE_DIFF_ERROR'; -export const REQUEST_DIFF = 'REQUEST_DIFF'; -export const SET_DIFF_ENDPOINT = 'SET_DIFF_ENDPOINT'; diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutations.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutations.js deleted file mode 100644 index 11aa71d2b6b..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutations.js +++ /dev/null @@ -1,31 +0,0 @@ -import Vue from 'vue'; -import { parseDiff } from '../../utils'; -import * as types from './mutation_types'; - -export default { - [types.SET_DIFF_ENDPOINT](state, path) { - Vue.set(state.paths, 'diffEndpoint', path); - }, - - [types.REQUEST_DIFF](state) { - state.isLoading = true; - }, - - [types.RECEIVE_DIFF_SUCCESS](state, { diff, enrichData }) { - const { added, fixed, existing } = parseDiff(diff, enrichData); - const baseReportOutofDate = diff.base_report_out_of_date || false; - const hasBaseReport = Boolean(diff.base_report_created_at); - - state.isLoading = false; - state.newIssues = added; - state.resolvedIssues = fixed; - state.allIssues = existing; - state.baseReportOutofDate = baseReportOutofDate; - state.hasBaseReport = hasBaseReport; - }, - - [types.RECEIVE_DIFF_ERROR](state) { - state.isLoading = false; - state.hasError = true; - }, -}; diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/state.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/state.js deleted file mode 100644 index c1b3f546431..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/state.js +++ /dev/null @@ -1,14 +0,0 @@ -export default () => ({ - paths: { - diffEndpoint: null, - }, - - isLoading: false, - hasError: false, - - newIssues: [], - resolvedIssues: [], - allIssues: [], - baseReportOutofDate: false, - hasBaseReport: false, -}); diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/actions.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/actions.js deleted file mode 100644 index 13ca154bfa7..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/actions.js +++ /dev/null @@ -1,26 +0,0 @@ -import { REPORT_TYPE_SECRET_DETECTION } from '~/vue_shared/security_reports/constants'; -import { fetchDiffData } from '../../utils'; -import * as types from './mutation_types'; - -export const setDiffEndpoint = ({ commit }, path) => commit(types.SET_DIFF_ENDPOINT, path); - -export const requestDiff = ({ commit }) => commit(types.REQUEST_DIFF); - -export const receiveDiffSuccess = ({ commit }, response) => - commit(types.RECEIVE_DIFF_SUCCESS, response); - -export const receiveDiffError = ({ commit }, response) => - commit(types.RECEIVE_DIFF_ERROR, response); - -export const fetchDiff = ({ state, rootState, dispatch }) => { - dispatch('requestDiff'); - - return fetchDiffData(rootState, state.paths.diffEndpoint, REPORT_TYPE_SECRET_DETECTION) - .then((data) => { - dispatch('receiveDiffSuccess', data); - return data; - }) - .catch(() => { - dispatch('receiveDiffError'); - }); -}; diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/index.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/index.js deleted file mode 100644 index 1d5af1d4fe5..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/index.js +++ /dev/null @@ -1,10 +0,0 @@ -import * as actions from './actions'; -import mutations from './mutations'; -import state from './state'; - -export default { - namespaced: true, - state, - mutations, - actions, -}; diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/mutation_types.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/mutation_types.js deleted file mode 100644 index aacec0fb679..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/mutation_types.js +++ /dev/null @@ -1,4 +0,0 @@ -export const RECEIVE_DIFF_SUCCESS = 'RECEIVE_DIFF_SUCCESS'; -export const RECEIVE_DIFF_ERROR = 'RECEIVE_DIFF_ERROR'; -export const REQUEST_DIFF = 'REQUEST_DIFF'; -export const SET_DIFF_ENDPOINT = 'SET_DIFF_ENDPOINT'; diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/mutations.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/mutations.js deleted file mode 100644 index ee943b0621c..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/mutations.js +++ /dev/null @@ -1,30 +0,0 @@ -import { parseDiff } from '~/vue_shared/security_reports/store/utils'; -import * as types from './mutation_types'; - -export default { - [types.SET_DIFF_ENDPOINT](state, path) { - state.paths.diffEndpoint = path; - }, - - [types.REQUEST_DIFF](state) { - state.isLoading = true; - }, - - [types.RECEIVE_DIFF_SUCCESS](state, { diff, enrichData }) { - const { added, fixed, existing } = parseDiff(diff, enrichData); - const baseReportOutofDate = diff.base_report_out_of_date || false; - const hasBaseReport = Boolean(diff.base_report_created_at); - - state.isLoading = false; - state.newIssues = added; - state.resolvedIssues = fixed; - state.allIssues = existing; - state.baseReportOutofDate = baseReportOutofDate; - state.hasBaseReport = hasBaseReport; - }, - - [types.RECEIVE_DIFF_ERROR](state) { - state.isLoading = false; - state.hasError = true; - }, -}; diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/state.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/state.js deleted file mode 100644 index c1b3f546431..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/state.js +++ /dev/null @@ -1,14 +0,0 @@ -export default () => ({ - paths: { - diffEndpoint: null, - }, - - isLoading: false, - hasError: false, - - newIssues: [], - resolvedIssues: [], - allIssues: [], - baseReportOutofDate: false, - hasBaseReport: false, -}); diff --git a/app/assets/javascripts/vue_shared/security_reports/store/state.js b/app/assets/javascripts/vue_shared/security_reports/store/state.js deleted file mode 100644 index 5dc4d1ad2fb..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/state.js +++ /dev/null @@ -1,5 +0,0 @@ -import { MODULE_SAST, MODULE_SECRET_DETECTION } from './constants'; - -export default () => ({ - reportTypes: [MODULE_SAST, MODULE_SECRET_DETECTION], -}); diff --git a/app/assets/javascripts/vue_shared/security_reports/store/utils.js b/app/assets/javascripts/vue_shared/security_reports/store/utils.js deleted file mode 100644 index f620bad8dba..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/store/utils.js +++ /dev/null @@ -1,154 +0,0 @@ -import axios from '~/lib/utils/axios_utils'; -import pollUntilComplete from '~/lib/utils/poll_until_complete'; -import { __, n__, sprintf } from '~/locale'; -import { CRITICAL, HIGH } from '~/vulnerabilities/constants'; -import { - FEEDBACK_TYPE_DISMISSAL, - FEEDBACK_TYPE_ISSUE, - FEEDBACK_TYPE_MERGE_REQUEST, -} from '../constants'; - -export const fetchDiffData = (state, endpoint, category) => { - const requests = [pollUntilComplete(endpoint)]; - - if (state.canReadVulnerabilityFeedback) { - requests.push(axios.get(state.vulnerabilityFeedbackPath, { params: { category } })); - } - - return Promise.all(requests).then(([diffResponse, enrichResponse]) => ({ - diff: diffResponse.data, - enrichData: enrichResponse?.data ?? [], - })); -}; - -/** - * Returns given vulnerability enriched with the corresponding - * feedback (`dismissal` or `issue` type) - * @param {Object} vulnerabilityObject - * @param {Array} feedbackList - */ -export const enrichVulnerabilityWithFeedback = (vulnerabilityObject, feedbackList = []) => { - const vulnerability = { ...vulnerabilityObject }; - // Some records may have a null `uuid`, we need to fallback to using `project_fingerprint` in those cases. Once all entries have been fixed, we can remove the fallback. - // related epic: https://gitlab.com/groups/gitlab-org/-/epics/2791 - feedbackList - .filter((fb) => - fb.finding_uuid - ? fb.finding_uuid === vulnerability.uuid - : fb.project_fingerprint === vulnerability.project_fingerprint, - ) - .forEach((feedback) => { - if (feedback.feedback_type === FEEDBACK_TYPE_DISMISSAL) { - vulnerability.isDismissed = true; - vulnerability.dismissalFeedback = feedback; - } else if (feedback.feedback_type === FEEDBACK_TYPE_ISSUE && feedback.issue_iid) { - vulnerability.hasIssue = true; - vulnerability.issue_feedback = feedback; - } else if ( - feedback.feedback_type === FEEDBACK_TYPE_MERGE_REQUEST && - feedback.merge_request_iid - ) { - vulnerability.hasMergeRequest = true; - vulnerability.merge_request_feedback = feedback; - } - }); - - return vulnerability; -}; - -/** - * Generates the added, fixed, and existing vulnerabilities from the API report. - * - * @param {Object} diff The original reports. - * @param {Object} enrichData Feedback data to add to the reports. - * @returns {Object} - */ -export const parseDiff = (diff, enrichData) => { - const enrichVulnerability = (vulnerability) => ({ - ...enrichVulnerabilityWithFeedback(vulnerability, enrichData), - category: vulnerability.report_type, - title: vulnerability.message || vulnerability.name, - }); - - return { - added: diff.added ? diff.added.map(enrichVulnerability) : [], - fixed: diff.fixed ? diff.fixed.map(enrichVulnerability) : [], - existing: diff.existing ? diff.existing.map(enrichVulnerability) : [], - }; -}; - -const createCountMessage = ({ critical, high, other, total }) => { - const otherMessage = n__('%d Other', '%d Others', other); - const countMessage = __( - '%{criticalStart}%{critical} Critical%{criticalEnd} %{highStart}%{high} High%{highEnd} and %{otherStart}%{otherMessage}%{otherEnd}', - ); - return total ? sprintf(countMessage, { critical, high, otherMessage }) : ''; -}; - -const createStatusMessage = ({ reportType, status, total }) => { - const vulnMessage = n__('vulnerability', 'vulnerabilities', total); - let message; - if (status) { - message = __('%{reportType} %{status}'); - } else if (!total) { - message = __('%{reportType} detected no new vulnerabilities.'); - } else { - message = __( - '%{reportType} detected %{totalStart}%{total}%{totalEnd} potential %{vulnMessage}', - ); - } - return sprintf(message, { reportType, status, total, vulnMessage }); -}; - -/** - * Counts vulnerabilities. - * Returns the amount of critical, high, and other vulnerabilities. - * - * @param {Array} vulnerabilities The raw vulnerabilities to parse - * @returns {{critical: number, high: number, other: number}} - */ -export const countVulnerabilities = (vulnerabilities = []) => - vulnerabilities.reduce( - (acc, { severity }) => { - if (severity === CRITICAL) { - acc.critical += 1; - } else if (severity === HIGH) { - acc.high += 1; - } else { - acc.other += 1; - } - - return acc; - }, - { critical: 0, high: 0, other: 0 }, - ); - -/** - * Takes an object of options and returns the object with an externalized string representing - * the critical, high, and other severity vulnerabilities for a given report. - * - * The resulting string _may_ still contain sprintf-style placeholders. These - * are left in place so they can be replaced with markup, via the - * SecuritySummary component. - * @param {{reportType: string, status: string, critical: number, high: number, other: number}} options - * @returns {Object} the parameters with an externalized string - */ -export const groupedTextBuilder = ({ - reportType = '', - status = '', - critical = 0, - high = 0, - other = 0, -} = {}) => { - const total = critical + high + other; - - return { - countMessage: createCountMessage({ critical, high, other, total }), - message: createStatusMessage({ reportType, status, total }), - critical, - high, - other, - status, - total, - }; -}; diff --git a/app/assets/javascripts/vue_shared/security_reports/utils.js b/app/assets/javascripts/vue_shared/security_reports/utils.js index 0add91c402e..aa4f6978552 100644 --- a/app/assets/javascripts/vue_shared/security_reports/utils.js +++ b/app/assets/javascripts/vue_shared/security_reports/utils.js @@ -39,13 +39,3 @@ export const extractSecurityReportArtifacts = (reportTypes, jobs) => { return acc; }, []); }; - -export const extractSecurityReportArtifactsFromPipeline = (reportTypes, data) => { - const jobs = data.project?.pipeline?.jobs?.nodes ?? []; - return extractSecurityReportArtifacts(reportTypes, jobs); -}; - -export const extractSecurityReportArtifactsFromMergeRequest = (reportTypes, data) => { - const jobs = data.project?.mergeRequest?.headPipeline?.jobs?.nodes ?? []; - return extractSecurityReportArtifacts(reportTypes, jobs); -}; diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index dff1c04311d..f0b6d86d48d 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -30,7 +30,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController feature_category :continuous_integration, [:ci_cd, :reset_registration_token] urgency :low, [:ci_cd, :reset_registration_token] feature_category :service_ping, [:usage_data, :service_usage_data] - feature_category :integrations, [:integrations] + feature_category :integrations, [:integrations, :slack_app_manifest_share, :slack_app_manifest_download] feature_category :pages, [:lets_encrypt_terms_of_service] feature_category :error_tracking, [:reset_error_tracking_access_token] @@ -114,6 +114,14 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController redirect_to ::Gitlab::LetsEncrypt.terms_of_service_url end + def slack_app_manifest_share + redirect_to Slack::Manifest.share_url + end + + def slack_app_manifest_download + send_data Slack::Manifest.to_json, type: :json, disposition: 'attachment', filename: 'slack_manifest.json' + end + private def set_application_setting diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index 0328d262dc7..52a16475c07 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -190,7 +190,7 @@ module Emails to: @recipient.notification_email_for(@project.group), subject: subject("#{@issue.title} (##{@issue.iid})"), 'X-GitLab-NotificationReason' => reason, - 'X-GitLab-ConfidentialIssue' => confidentiality + 'X-GitLab-ConfidentialIssue' => confidentiality.to_s } end end diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb index 1e254a32885..bdd63dfc62c 100644 --- a/app/mailers/emails/notes.rb +++ b/app/mailers/emails/notes.rb @@ -15,7 +15,14 @@ module Emails @issue = @note.noteable @target_url = project_issue_url(*note_target_url_options) - mail_answer_note_thread(@issue, @note, note_thread_options(reason)) + mail_answer_note_thread( + @issue, + @note, + note_thread_options( + reason, + confidentiality: @issue.confidential? + ) + ) end def note_merge_request_email(recipient_id, note_id, reason = nil) @@ -62,13 +69,15 @@ module Emails { anchor: "note_#{@note.id}" } end - def note_thread_options(reason) + def note_thread_options(reason, confidentiality: nil) { from: sender(@note.author_id), to: @recipient.notification_email_for(@project&.group || @group), subject: subject("#{@note.noteable.title} (#{@note.noteable.reference_link_text})"), 'X-GitLab-NotificationReason' => reason - } + }.tap do |options| + options['X-GitLab-ConfidentialIssue'] = confidentiality.to_s unless confidentiality.nil? + end end def setup_note_mail(note_id, recipient_id) diff --git a/app/models/milestone.rb b/app/models/milestone.rb index d300b938fc0..8de717fb61d 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -10,6 +10,7 @@ class Milestone < ApplicationRecord include IidRoutes include UpdatedAtFilterable include EachBatch + include Spammable prepend_mod_with('Milestone') # rubocop: disable Cop/InjectEnterpriseEditionModule @@ -62,6 +63,9 @@ class Milestone < ApplicationRecord validate :parent_type_check validate :uniqueness_of_title, if: :title_changed? + attr_spammable :title, spam_title: true + attr_spammable :description, spam_description: true + state_machine :state, initial: :active do event :close do transition active: :closed @@ -255,6 +259,10 @@ class Milestone < ApplicationRecord end end + def check_for_spam?(*) + spammable_attribute_changed? && parent.public? + end + private def timebox_format_reference(format = :iid) diff --git a/app/models/user_custom_attribute.rb b/app/models/user_custom_attribute.rb index 0ba58a13237..425f2cc062b 100644 --- a/app/models/user_custom_attribute.rb +++ b/app/models/user_custom_attribute.rb @@ -16,6 +16,7 @@ class UserCustomAttribute < ApplicationRecord ARKOSE_RISK_BAND = 'arkose_risk_band' AUTO_BANNED_BY_ABUSE_REPORT_ID = 'auto_banned_by_abuse_report_id' ALLOW_POSSIBLE_SPAM = 'allow_possible_spam' + IDENTITY_VERIFICATION_PHONE_EXEMPT = 'identity_verification_phone_exempt' class << self def upsert_custom_attributes(custom_attributes) diff --git a/app/serializers/prometheus_alert_entity.rb b/app/serializers/prometheus_alert_entity.rb deleted file mode 100644 index fb25889e4db..00000000000 --- a/app/serializers/prometheus_alert_entity.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -class PrometheusAlertEntity < Grape::Entity - include RequestAwareEntity - - expose :id - expose :title - expose :query - expose :threshold - expose :runbook_url - - expose :operator do |prometheus_alert| - prometheus_alert.computed_operator - end - - private - - alias_method :prometheus_alert, :object - - def can_read_prometheus_alerts? - can?(request.current_user, :read_prometheus_alerts, prometheus_alert.project) - end -end diff --git a/app/serializers/prometheus_alert_serializer.rb b/app/serializers/prometheus_alert_serializer.rb deleted file mode 100644 index 4dafb7216db..00000000000 --- a/app/serializers/prometheus_alert_serializer.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -class PrometheusAlertSerializer < BaseSerializer - entity PrometheusAlertEntity -end diff --git a/app/services/milestones/create_service.rb b/app/services/milestones/create_service.rb index 6c3edd2e147..e8a14adc10d 100644 --- a/app/services/milestones/create_service.rb +++ b/app/services/milestones/create_service.rb @@ -5,11 +5,19 @@ module Milestones def execute milestone = parent.milestones.new(params) + before_create(milestone) + if milestone.save && milestone.project_milestone? event_service.open_milestone(milestone, current_user) end milestone end + + private + + def before_create(milestone) + milestone.check_for_spam(user: current_user, action: :create) + end end end diff --git a/app/services/milestones/update_service.rb b/app/services/milestones/update_service.rb index b9a12a35d31..90cb8ea9f5c 100644 --- a/app/services/milestones/update_service.rb +++ b/app/services/milestones/update_service.rb @@ -13,11 +13,22 @@ module Milestones end if params.present? - milestone.update(params.except(:state_event)) + milestone.assign_attributes(params.except(:state_event)) end + if milestone.changed? + before_update(milestone) + end + + milestone.save milestone end + + private + + def before_update(milestone) + milestone.check_for_spam(user: current_user, action: :update) + end end end diff --git a/app/views/admin/application_settings/_slack.html.haml b/app/views/admin/application_settings/_slack.html.haml index 69a5e284b4c..e04b29b496e 100644 --- a/app/views/admin/application_settings/_slack.html.haml +++ b/app/views/admin/application_settings/_slack.html.haml @@ -1,33 +1,71 @@ -- return unless Gitlab.dev_or_test_env? || Gitlab.com? +- gitlab_com = Gitlab.com? + +- return unless Feature.enabled?(:slack_app_self_managed) || gitlab_com - expanded = integration_expanded?('slack_app_') + %section.settings.as-slack.no-animate#js-slack-settings{ class: ('expanded' if expanded) } .settings-header %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only - = _('Slack application') + = s_('Integrations|GitLab for Slack app') = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do = expanded ? _('Collapse') : _('Expand') %p - = _('Slack integration allows you to interact with GitLab via slash commands in a chat window.') + = s_('SlackIntegration|Configure your GitLab for Slack app.') + = link_to(_('Learn more.'), help_page_path('user/admin_area/settings/slack_app'), target: '_blank', rel: 'noopener noreferrer') + .settings-content + - unless gitlab_com + %h5 + = s_('SlackIntegration|Step 1: Create your GitLab for Slack app') + %p + = s_('SlackIntegration|You must do this step only once.') + %p + = link_to slack_app_manifest_share_admin_application_settings_path, class: 'btn btn-default gl-button' do + = image_tag 'illustrations/slack_logo.svg', class: 'gl-w-9! gl-h-9! gl-my-n4! gl-ml-n4 gl-mr-n2!' + %strong.gl-button-text + = s_("SlackIntegration|Create Slack app") + %hr + %h5 + = s_('SlackIntegration|Step 2: Configure the app settings') + %p + - tag_pair_slack_apps = tag_pair(link_to('', 'https://api.slack.com/apps', target: '_blank', rel: 'noopener noreferrer'), :link_start, :link_end) + - tag_pair_strong = tag_pair(tag.strong, :strong_open, :strong_close) + = safe_format(s_('SlackIntegration|Copy the %{link_start}settings%{link_end} from %{strong_open}%{settings_heading}%{strong_close} in your GitLab for Slack app.'), tag_pair_slack_apps, tag_pair_strong, settings_heading: 'App Credentials') + = link_to(_('Learn more.'), help_page_path('user/admin_area/settings/slack_app', anchor: 'configure-the-settings'), target: '_blank', rel: 'noopener noreferrer') = gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-slack-settings'), html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) if expanded - %fieldset .form-group - = f.gitlab_ui_checkbox_component :slack_app_enabled, s_('ApplicationSettings|Enable Slack application'), - help_text: s_('ApplicationSettings|This option is only available on GitLab.com') + = f.gitlab_ui_checkbox_component :slack_app_enabled, s_('ApplicationSettings|Enable GitLab for Slack app') .form-group = f.label :slack_app_id, s_('SlackIntegration|Client ID'), class: 'label-bold' = f.text_field :slack_app_id, class: 'form-control gl-form-input' .form-group = f.label :slack_app_secret, s_('SlackIntegration|Client secret'), class: 'label-bold' = f.text_field :slack_app_secret, class: 'form-control gl-form-input' + .form-text.text-muted + = s_('SlackIntegration|Used for authenticating OAuth requests from the GitLab for Slack app.') .form-group = f.label :slack_app_signing_secret, s_('SlackIntegration|Signing secret'), class: 'label-bold' = f.text_field :slack_app_signing_secret, class: 'form-control gl-form-input' + .form-text.text-muted + = s_('SlackIntegration|Used for authenticating API requests from the GitLab for Slack app.') .form-group = f.label :slack_app_verification_token, s_('SlackIntegration|Verification token'), class: 'label-bold' = f.text_field :slack_app_verification_token, class: 'form-control gl-form-input' - + .form-text.text-muted + = s_('SlackIntegration|Used only for authenticating slash commands from the GitLab for Slack app. This method of authentication is deprecated by Slack.') = f.submit _('Save changes'), pajamas_button: true + + - unless gitlab_com + %hr + %h5 + = s_('SlackIntegration|Update your Slack app') + %p + = s_('SlackIntegration|When GitLab releases new features for the GitLab for Slack app, you might have to manually update your copy to use the new features.') + = link_to(_('Learn more.'), help_page_path('user/admin_area/settings/slack_app', anchor: 'update-the-gitlab-for-slack-app'), target: '_blank', rel: 'noopener noreferrer') + %p + = render Pajamas::ButtonComponent.new(href: slack_app_manifest_download_admin_application_settings_path, icon: 'download') do + = s_("SlackIntegration|Download latest manifest file") + diff --git a/app/views/admin/application_settings/general.html.haml b/app/views/admin/application_settings/general.html.haml index 022930bd6b4..2d56e9dd0dd 100644 --- a/app/views/admin/application_settings/general.html.haml +++ b/app/views/admin/application_settings/general.html.haml @@ -96,7 +96,6 @@ = render 'admin/application_settings/plantuml' = render 'admin/application_settings/diagramsnet' = render 'admin/application_settings/sourcegraph' -= render_if_exists 'admin/application_settings/slack' -# this partial is from JiHu, see details in https://jihulab.com/gitlab-cn/gitlab/-/merge_requests/417 = render_if_exists 'admin/application_settings/dingtalk_integration' -# this partial is from JiHu, see details in https://jihulab.com/gitlab-cn/gitlab/-/merge_requests/640 @@ -109,4 +108,5 @@ = render 'admin/application_settings/floc' = render_if_exists 'admin/application_settings/add_license' = render 'admin/application_settings/jira_connect' += render 'admin/application_settings/slack' = render_if_exists 'admin/application_settings/ai_access' diff --git a/config/feature_flags/development/dynamically_compute_deployment_approval.yml b/config/feature_flags/development/dynamically_compute_deployment_approval.yml new file mode 100644 index 00000000000..6a7e8972613 --- /dev/null +++ b/config/feature_flags/development/dynamically_compute_deployment_approval.yml @@ -0,0 +1,8 @@ +--- +name: dynamically_compute_deployment_approval +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120699 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/411370 +milestone: '16.2' +type: development +group: group::environments +default_enabled: false diff --git a/config/feature_flags/development/key_set_optimizer_ignored_columns.yml b/config/feature_flags/development/key_set_optimizer_ignored_columns.yml new file mode 100644 index 00000000000..af47a43646e --- /dev/null +++ b/config/feature_flags/development/key_set_optimizer_ignored_columns.yml @@ -0,0 +1,8 @@ +--- +name: key_set_optimizer_ignored_columns +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125462 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/417515 +milestone: '16.2' +type: development +group: group::project management +default_enabled: false diff --git a/config/feature_flags/development/remove_deployments_api_ref_sort.yml b/config/feature_flags/development/remove_deployments_api_ref_sort.yml index 26dd0434c94..584012ba2bf 100644 --- a/config/feature_flags/development/remove_deployments_api_ref_sort.yml +++ b/config/feature_flags/development/remove_deployments_api_ref_sort.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/416305 milestone: '16.2' type: development group: group::environments -default_enabled: false +default_enabled: true diff --git a/config/feature_flags/development/slack_app_self_managed.yml b/config/feature_flags/development/slack_app_self_managed.yml new file mode 100644 index 00000000000..92b0a869159 --- /dev/null +++ b/config/feature_flags/development/slack_app_self_managed.yml @@ -0,0 +1,8 @@ +--- +name: slack_app_self_managed +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124823 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/416448 +milestone: '16.2' +type: development +group: group::import and integrate +default_enabled: false diff --git a/config/routes/admin.rb b/config/routes/admin.rb index d9cd60f8086..0123bf0627c 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -155,7 +155,8 @@ namespace :admin do put :clear_repository_check_states match :general, :integrations, :repository, :ci_cd, :reporting, :metrics_and_profiling, :network, :preferences, via: [:get, :patch] get :lets_encrypt_terms_of_service - + get :slack_app_manifest_download, format: :json + get :slack_app_manifest_share get :service_usage_data resource :appearances, only: [:show, :create, :update], path: 'appearance', module: 'application_settings' do diff --git a/doc/administration/audit_events.md b/doc/administration/audit_events.md index df371b8a82f..e47e1018159 100644 --- a/doc/administration/audit_events.md +++ b/doc/administration/audit_events.md @@ -119,7 +119,7 @@ The first row contains the headers, which are listed in the following table alon Successful sign-in events are the only audit events available at all tiers. To see successful sign-in events: -1. Select your avatar. +1. On the left sidebar, select your avatar. 1. Select **Edit profile > Authentication log**. After upgrading to a paid tier, you can also see successful sign-in events on audit event pages. diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 1d7c61f63b1..57b400b670e 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1215,7 +1215,7 @@ Input type: `AuditEventsStreamingDestinationEventsAddInput` | ---- | ---- | ----------- | | `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | -| `eventTypeFilters` | [`[String!]`](#string) | Event type filters present. | +| `eventTypeFilters` | [`[String!]`](#string) | List of event type filters for the audit event external destination. | ### `Mutation.auditEventsStreamingDestinationEventsRemove` @@ -1236,6 +1236,26 @@ Input type: `AuditEventsStreamingDestinationEventsRemoveInput` | `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +### `Mutation.auditEventsStreamingDestinationInstanceEventsAdd` + +Input type: `AuditEventsStreamingDestinationInstanceEventsAddInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `destinationId` | [`AuditEventsInstanceExternalAuditEventDestinationID!`](#auditeventsinstanceexternalauditeventdestinationid) | Destination id. | +| `eventTypeFilters` | [`[String!]!`](#string) | List of event type filters to add for streaming. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +| `eventTypeFilters` | [`[String!]`](#string) | List of event type filters for the audit event external destination. | + ### `Mutation.auditEventsStreamingHeadersCreate` Input type: `AuditEventsStreamingHeadersCreateInput` diff --git a/doc/architecture/blueprints/runner_admission_controller/index.md b/doc/architecture/blueprints/runner_admission_controller/index.md index 420232895d1..922fcf67b04 100644 --- a/doc/architecture/blueprints/runner_admission_controller/index.md +++ b/doc/architecture/blueprints/runner_admission_controller/index.md @@ -1,9 +1,9 @@ --- status: proposed creation-date: "2023-03-07" -authors: [ "@username" ] -coach: "@username" -approvers: [ "@product-manager", "@engineering-manager" ] +authors: [ "@ajwalker" ] +coach: [ "@ayufan" ] +approvers: [ "@DarrenEastman", "@engineering-manager" ] owning-stage: "~devops::" participating-stages: [] --- diff --git a/doc/user/admin_area/index.md b/doc/user/admin_area/index.md index a59501f14ce..8b2ef115298 100644 --- a/doc/user/admin_area/index.md +++ b/doc/user/admin_area/index.md @@ -240,9 +240,9 @@ To [Create a new group](../group/index.md#create-a-group) select **New group**. [Topics](../project/working_with_projects.md#explore-topics) are used to categorize and find similar projects. -You can administer all topics in the GitLab instance from the Admin Area's Topics page. +### View all topics -To access the Topics page: +To view all topics in the GitLab instance: 1. On the left sidebar, expand the top-most chevron (**{chevron-down}**). 1. Select **Admin Area**. @@ -250,22 +250,71 @@ To access the Topics page: For each topic, the page displays its name and the number of projects labeled with the topic. -To create a new topic, select **New topic**. +### Search for topics -To edit a topic, select **Edit** in that topic's row. +1. On the left sidebar, expand the top-most chevron (**{chevron-down}**). +1. Select **Admin Area**. +1. Select **Overview > Topics**. +1. In the search box, enter your search criteria. + The topic search is case-insensitive and applies partial matching. -To remove a topic, select **Remove** in that topic's row. +### Create a topic -To remove a topic and move all assigned projects to another topic, select **Merge topics**. +To create a topic: -To search for topics by name, enter your criteria in the search box. The topic search is case -insensitive and applies partial matching. +1. On the left sidebar, expand the top-most chevron (**{chevron-down}**). +1. Select **Admin Area**. +1. Select **Overview > Topics**. +1. Select **New topic**. +1. Enter the **Topic slug (name)** and **Topic title**. +1. Optional. Enter a **Description** and add a **Topic avatar**. +1. Select **Save changes**. + +The created topics are displayed on the **Explore topics** page. NOTE: The assigned topics are visible only to everyone with access to the project, but everyone can see which topics exist on the GitLab instance. Do not include sensitive information in the name of a topic. +### Edit a topic + +You can edit a topic's name, title, description, and avatar at any time. +To edit a topic: + +1. On the left sidebar, expand the top-most chevron (**{chevron-down}**). +1. Select **Admin Area**. +1. Select **Overview > Topics**. +1. Select **Edit** in that topic's row. +1. Edit the topic slug (name), title, description, or avatar. +1. Select **Save changes**. + +### Remove a topic + +If you no longer need a topic, you can permanently remove it. +To remove a topic: + +1. On the left sidebar, expand the top-most chevron (**{chevron-down}**). +1. Select **Admin Area**. +1. Select **Overview > Topics**. +1. To remove a topic, select **Remove** in that topic's row. + +### Merge topics + +You can move all projects assigned to a topic to another topic. +The source topic is then permanently deleted. +After a merged topic is deleted, you cannot restore it. + +To merge topics: + +1. On the left sidebar, expand the top-most chevron (**{chevron-down}**). +1. Select **Admin Area**. +1. Select **Overview > Topics**. +1. Select **Merge topics**. +1. From the **Source topic** dropdown list, select the topic you want to merge and remove. +1. From the **Target topic** dropdown list, select the topic you want to merge the source topic into. +1. Select **Merge**. + ## Administering Gitaly servers You can list all Gitaly servers in the GitLab instance from the Admin Area's **Gitaly Servers** diff --git a/doc/user/admin_area/settings/index.md b/doc/user/admin_area/settings/index.md index 632adf273c4..3b38e1732d0 100644 --- a/doc/user/admin_area/settings/index.md +++ b/doc/user/admin_area/settings/index.md @@ -52,6 +52,7 @@ The **General** settings contain: Set max session time for web terminal. - [FLoC](floc.md) - Enable or disable [Federated Learning of Cohorts (FLoC)](https://en.wikipedia.org/wiki/Federated_Learning_of_Cohorts) tracking. +- [GitLab for Slack app](slack_app.md) - Enable and configure the GitLab for Slack app. ### CI/CD @@ -91,10 +92,6 @@ The **Integrations** settings contain: to receive invite email bounce events from Mailgun, if it is your email provider. - [PlantUML](../../../administration/integration/plantuml.md) - Allow rendering of PlantUML diagrams in documents. -- [Slack application](../../../user/project/integrations/gitlab_slack_application.md) - - Slack integration allows you to interact with GitLab via slash commands in a chat window. - This option is only available on GitLab.com, though it may be - [available for self-managed instances in the future](https://gitlab.com/gitlab-org/gitlab/-/issues/28164). - [Customer experience improvement and third-party offers](third_party_offers.md) - Control the display of customer experience improvement content and third-party offers. - [Snowplow](../../../development/internal_analytics/snowplow/index.md) - Configure the Snowplow integration. diff --git a/doc/user/admin_area/settings/slack_app.md b/doc/user/admin_area/settings/slack_app.md new file mode 100644 index 00000000000..3c826d89a52 --- /dev/null +++ b/doc/user/admin_area/settings/slack_app.md @@ -0,0 +1,108 @@ +--- +stage: Manage +group: Import and Integrate +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments +--- + +# GitLab for Slack app administration **(FREE SELF)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/358872) for self-managed instances in GitLab 16.2 [with a flag](../../../administration/feature_flags.md) named `slack_app_self_managed`. Disabled by default. + +FLAG: +On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `slack_app_self_managed`. On GitLab.com, this feature is available. + +This page contains information about configuring the GitLab for Slack app on self-managed instances. For user documentation, see [GitLab for Slack app](../../../user/project/integrations/gitlab_slack_application.md). + +The GitLab for Slack app distributed through the Slack app directory only works with GitLab.com. +On self-managed GitLab, you can create your own copy of the GitLab for Slack app from a [Slack app manifest file](https://api.slack.com/reference/manifests#creating_apps) and configure your instance. + +The app is a private one-time copy installed in your Slack workspace only and not distributed through the Slack app directory. To have the [GitLab for Slack app](../../../user/project/integrations/gitlab_slack_application.md) on your self-managed instance, you must first enable the integration. + +Prerequisites: + +- You must be at least a [workspace administrator](https://slack.com/help/articles/360018112273-Types-of-roles-in-Slack) in Slack. +- You must be [signed in](https://slack.com/signin) to your Slack workspace. + +## Create a GitLab for Slack app + +To create a GitLab for Slack app: + +1. On the left sidebar, expand the top-most chevron (**{chevron-down}**). +1. Select **Admin Area**. +1. On the left sidebar, select **Settings > General**. +1. Expand **GitLab for Slack app**. +1. Select **Create Slack app**. + +You are then redirected to Slack for the next steps. In the modal that appears: + +1. Select the Slack workspace to create the app in, then select **Next**. +1. Slack displays a summary of the app for review. To view the complete manifest, select **Edit Configurations**. To go back to the review summary, select **Next**. +1. Select **Create**. +1. Close the modal by selecting **Got it**. +1. Select **Install to Workspace**. + +## Configure the settings + +After you've [created a GitLab for Slack app](#create-a-gitlab-for-slack-app), you can configure the settings in GitLab: + +1. On the left sidebar, expand the top-most chevron (**{chevron-down}**). +1. Select **Admin Area**. +1. On the left sidebar, select **Settings > General**. +1. Expand **GitLab for Slack app**. +1. Select the **Enable GitLab for Slack app** checkbox. +1. Enter the details of your GitLab for Slack app: + 1. Go to [Slack API](https://api.slack.com/apps). + 1. Select **GitLab (\)**. You can search to find it. + 1. Scroll to **App Credentials**. +1. Select **Save changes**. + +### Test your configuration + +To test your GitLab for Slack app configuration: + +1. Enter the `/gitlab help` slash command into a channel in your Slack workspace. +1. Press Enter. + +You should see a list of available Slash commands. + +To use Slash commands for a project, configure the [GitLab for Slack app](../../../user/project/integrations/gitlab_slack_application.md) for the project. + +## Update the GitLab for Slack app + +When GitLab releases new features for the GitLab for Slack app, you might have to manually update your copy to use the new features. + +To update your copy of the GitLab for Slack app: + +- In GitLab: + + 1. On the left sidebar, expand the top-most chevron (**{chevron-down}**). + 1. Select **Admin Area**. + 1. On the left sidebar, select **Settings > General**. + 1. Expand **GitLab for Slack app**. + 1. Select **Download latest manifest file** to download `slack_manifest.json`. + +- In Slack: + + 1. Go to [Slack API](https://api.slack.com/apps). + 1. Select **GitLab (\)**. You can search to find it. + 1. On the left sidebar, select **App Manifest**. + 1. Select the **JSON** tab to switch to a JSON view of the manifest. + 1. Copy the contents of the `slack_manifest.json` file you've downloaded from GitLab. + 1. Paste the contents into the JSON viewer to replace any existing contents. + 1. Select **Save Changes**. + +## Connectivity requirements + +To enable the GitLab for Slack app functionality, your network must allow inbound and outbound connections between GitLab and Slack. + +- For [Slack notifications](../../../user/project/integrations/gitlab_slack_application.md#slack-notifications), the GitLab instance must be able to send requests to `https://slack.com`. +- For [Slash commands](../../../user/project/integrations/gitlab_slack_application.md#slash-commands) and other features, the GitLab instance must be able to receive requests from `https://slack.com`. + +## Troubleshooting + +### Slash commands return `/gitlab failed with the error "dispatch_failed"` in Slack + +Slash commands might return `/gitlab failed with the error "dispatch_failed"` in Slack. To resolve this issue, ensure: + +- The GitLab for Slack app is properly [configured](#configure-the-settings), and the **Enable GitLab for Slack app** checkbox is selected. +- Your GitLab instance [allows requests to and from Slack](#connectivity-requirements). diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md index 0a1f217d662..41c39ca43d6 100644 --- a/doc/user/profile/index.md +++ b/doc/user/profile/index.md @@ -250,7 +250,7 @@ Your primary email is used by default. To change your commit email: -1. In the upper-right corner, select your avatar. +1. On the left sidebar, select your avatar. 1. Select **Edit profile**. 1. In the **Commit email** dropdown list, select an email address. 1. Select **Update profile settings**. @@ -261,7 +261,7 @@ Your primary email is the default email address for your login, commit email, an To change your primary email: -1. In the upper-right corner, select your avatar. +1. On the left sidebar, select your avatar. 1. Select **Edit profile**. 1. In the **Email** field, enter your new email address. 1. Select **Update profile settings**. @@ -271,7 +271,7 @@ To change your primary email: You can select one of your [configured email addresses](#add-emails-to-your-user-profile) to be displayed on your public profile: -1. In the upper-right corner, select your avatar. +1. On the left sidebar, select your avatar. 1. Select **Edit profile**. 1. In the **Public email** field, select one of the available email addresses. 1. Select **Update profile settings**. diff --git a/doc/user/profile/notifications.md b/doc/user/profile/notifications.md index 0b769ff7eeb..1ef3f6002a6 100644 --- a/doc/user/profile/notifications.md +++ b/doc/user/profile/notifications.md @@ -46,7 +46,7 @@ anyone else. To edit your notification settings: -1. In the upper-right corner, select your avatar. +1. On the left sidebar, select your avatar. 1. Select **Preferences**. 1. On the left sidebar, select **Notifications**. 1. Edit the desired global, group, or project notification settings. @@ -99,7 +99,7 @@ You can select a notification level and email address for each group. To select a notification level for a group, use either of these methods: -1. In the upper-right corner, select your avatar. +1. On the left sidebar, select your avatar. 1. Select **Preferences**. 1. On the left sidebar, select **Notifications**. 1. Locate the project in the **Groups** section. @@ -118,7 +118,7 @@ Or: You can select an email address to receive notifications for each group you belong to. You can use group notifications, for example, if you work freelance, and want to keep email about clients' projects separate. -1. In the upper-right corner, select your avatar. +1. On the left sidebar, select your avatar. 1. Select **Preferences**. 1. On the left sidebar, select **Notifications**. 1. Locate the project in the **Groups** section. @@ -130,7 +130,7 @@ To help you stay up to date, you can select a notification level for each projec To select a notification level for a project, use either of these methods: -1. In the upper-right corner, select your avatar. +1. On the left sidebar, select your avatar. 1. Select **Preferences**. 1. On the left sidebar, select **Notifications**. 1. Locate the project in the **Projects** section. @@ -152,7 +152,7 @@ These emails are enabled by default. To opt out: -1. In the upper-right corner, select your avatar. +1. On the left sidebar, select your avatar. 1. Select **Preferences**. 1. On the left sidebar, select **Notifications**. 1. Clear the **Receive product marketing emails** checkbox. @@ -335,7 +335,7 @@ The participants are: If you no longer wish to receive any email notifications: -1. In the upper-right corner, select your avatar. +1. On the left sidebar, select your avatar. 1. Select **Preferences**. 1. On the left sidebar, select **Notifications**. 1. Clear the **Receive product marketing emails** checkbox. diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md index 8aa75d9d2d4..555e127c977 100644 --- a/doc/user/profile/personal_access_tokens.md +++ b/doc/user/profile/personal_access_tokens.md @@ -48,7 +48,7 @@ Use impersonation tokens to automate authentication as a specific user. You can create as many personal access tokens as you like. -1. In the upper-right corner, select your avatar. +1. On the left sidebar, select your avatar. 1. Select **Edit profile**. 1. On the left sidebar, select **Access Tokens**. 1. Enter a name and expiry date for the token. @@ -79,7 +79,7 @@ for guidance on managing personal access tokens (for example, setting a short ex At any time, you can revoke a personal access token. -1. In the upper-right corner, select your avatar. +1. On the left sidebar, select your avatar. 1. Select **Edit profile**. 1. On the left sidebar, select **Access Tokens**. 1. In the **Active personal access tokens** area, next to the key, select **Revoke**. @@ -96,7 +96,7 @@ Token usage information is updated every 10 minutes. GitLab considers a token us To view the last time a token was used: -1. In the upper-right corner, select your avatar. +1. On the left sidebar, select your avatar. 1. Select **Edit profile**. 1. On the left sidebar, select **Access Tokens**. 1. In the **Active personal access tokens** area, next to the key, view the **Last Used** date. diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md index 808a4984f73..f6fcd274d7c 100644 --- a/doc/user/profile/preferences.md +++ b/doc/user/profile/preferences.md @@ -12,7 +12,7 @@ of GitLab to their liking. To navigate to your profile's preferences: -1. In the upper-right corner, select your avatar. +1. On the left sidebar, select your avatar. 1. Select **Preferences**. ## Navigation theme diff --git a/doc/user/project/integrations/gitlab_slack_application.md b/doc/user/project/integrations/gitlab_slack_application.md index 81c50177856..baf598e35c3 100644 --- a/doc/user/project/integrations/gitlab_slack_application.md +++ b/doc/user/project/integrations/gitlab_slack_application.md @@ -4,53 +4,46 @@ group: Import and Integrate info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- -# GitLab for Slack app **(FREE SAAS)** +# GitLab for Slack app **(FREE)** -NOTE: -This feature is only configurable on GitLab.com. -For self-managed GitLab instances, use -[Slack slash commands](slack_slash_commands.md) and [Slack notifications](slack.md) instead. -For more information about our plans to make this feature configurable for all GitLab installations, -see [epic 1211](https://gitlab.com/groups/gitlab-org/-/epics/1211). +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/358872) for self-managed instances in GitLab 16.2 [with a flag](../../../administration/feature_flags.md) named `slack_app_self_managed`. Disabled by default. -Slack provides a native application that you can enable with your project's integrations on GitLab.com. GitLab -links your Slack user with your GitLab user so that commands you run in Slack are run by the linked GitLab user on -GitLab.com. +FLAG: +On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `slack_app_self_managed`. On GitLab.com, this feature is available. + +The GitLab for Slack app is a native Slack app that provides [slash commands](#slash-commands) and [notifications](#slack-notifications) in your Slack workspace. GitLab links your Slack user with your GitLab user so that commands +you run in Slack are run by the linked GitLab user on GitLab.com. ## Installation +Prerequisite: + +- You must have the [appropriate permissions to add apps to your Slack workspace](https://slack.com/help/articles/202035138-Add-apps-to-your-Slack-workspace). + In GitLab 15.0 and later, the GitLab for Slack app uses [granular permissions](https://medium.com/slack-developer-blog/more-precision-less-restrictions-a3550006f9c3). Although functionality has not changed, you should [reinstall the app](#update-the-gitlab-for-slack-app). -### Through the Slack App Directory +### Through project integration settings -To enable the GitLab for Slack app for your workspace, -install the [GitLab application](https://slack-platform.slack.com/apps/A676ADMV5-gitlab) -from the [Slack App Directory](https://slack.com/apps). +To install the GitLab for Slack app integration: -On the [GitLab for Slack app landing page](https://gitlab.com/-/profile/slack/edit), -you can select a GitLab project to link with your Slack workspace. - -### Through GitLab project settings - -Alternatively, you can configure the GitLab for Slack app with your project's -integration settings. - -You must have the appropriate permissions for your Slack -workspace to be able to install a new application. See -[Add apps to your Slack workspace](https://slack.com/help/articles/202035138-Add-apps-to-your-Slack-workspace). - -To enable the GitLab integration for your Slack workspace: - -1. Go to your project's **Settings > Integration > GitLab for Slack app** (only - visible on GitLab.com). +1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project. +1. Select **Settings > Integrations**. +1. Select **GitLab for Slack app**. On self-managed GitLab, an administrator must first [enable the integration](../../admin_area/settings/slack_app.md). 1. Select **Install GitLab for Slack app**. -1. Select **Allow** on Slack's confirmation screen. +1. On the Slack confirmation page, select **Allow**. To update the app in your Slack workspace to the latest version, you can also select **Reinstall GitLab for Slack app**. +### Through the Slack app directory **(FREE SAAS)** + +On GitLab.com, you can install the GitLab for Slack app +from the [Slack app directory](https://slack-platform.slack.com/apps/A676ADMV5-gitlab). +On the [GitLab for Slack app page](https://gitlab.com/-/profile/slack/edit), +select a GitLab project to link with your Slack workspace. + ## Slash commands You can use slash commands to run common GitLab operations. Replace `` with a project full path. @@ -91,9 +84,9 @@ The command returns an error if no matching action is found. By default, slash commands expect a project full path. To create a shorter project alias in the GitLab for Slack app: -1. Go to your project's home page. -1. Go to **Settings > Integrations** (only visible on GitLab.com). -1. On the **Integrations** page, select **GitLab for Slack app**. +1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project. +1. Select **Settings > Integrations**. +1. Select **GitLab for Slack app**. On self-managed GitLab, an administrator must first [enable the integration](../../admin_area/settings/slack_app.md). 1. The current **Project Alias**, if any, is displayed. To edit this value, select **Edit**. 1. Enter your desired alias, and select **Save changes**. @@ -174,6 +167,12 @@ The GitLab for Slack app is updated for all projects that use the integration. Alternatively, you can [configure a new Slack integration](https://about.gitlab.com/solutions/slack/). +### GitLab for Slack app does not appear in the list of integrations + +The GitLab for Slack app might not appear in the list of integrations. To have the GitLab for Slack app on your self-managed instance, an administrator must first [enable the integration](../../admin_area/settings/slack_app.md). On GitLab.com, the GitLab for Slack app is available by default. + +The GitLab for Slack app is enabled at the project level only. Support for the app at the group and instance levels is proposed in [issue 391526](https://gitlab.com/gitlab-org/gitlab/-/issues/391526). + ### Project or alias not found Some Slack commands must have a project full path or alias and fail with the following error @@ -187,7 +186,11 @@ As a workaround, ensure: - The project full path is correct. - If using a [project alias](#create-a-project-alias-for-slash-commands), the alias is correct. -- The GitLab for Slack app integration is [enabled for the project](#through-gitlab-project-settings). +- The GitLab for Slack app integration is [enabled for the project](#through-project-integration-settings). + +### Slash commands return `/gitlab failed with the error "dispatch_failed"` in Slack + +Slash commands might return `/gitlab failed with the error "dispatch_failed"` in Slack. To resolve this issue, ensure an administrator has properly configured the [GitLab for Slack app settings](../../admin_area/settings/slack_app.md) on your self-managed instance. ### Notifications are not received to a channel diff --git a/doc/user/project/merge_requests/changes.md b/doc/user/project/merge_requests/changes.md index a2863fe7601..79599580f3e 100644 --- a/doc/user/project/merge_requests/changes.md +++ b/doc/user/project/merge_requests/changes.md @@ -51,7 +51,7 @@ clear your browser's cookies or change this behavior again. To view one file at a time for all of your merge requests: -1. In the upper-right corner, select your avatar. +1. On the left sidebar, select your avatar. 1. Select **Preferences**. 1. Scroll to the **Behavior** section and select the **Show one file at a time on merge request's Changes tab** checkbox. 1. Select **Save changes**. diff --git a/doc/user/project/repository/code_suggestions.md b/doc/user/project/repository/code_suggestions.md index 5f753ffafe4..c926eeca5d3 100644 --- a/doc/user/project/repository/code_suggestions.md +++ b/doc/user/project/repository/code_suggestions.md @@ -79,7 +79,7 @@ Each individual user must also choose to enable Code Suggestions. Each user can enable Code Suggestions for themselves: 1. On the left sidebar, select your avatar. -1. On the left sidebar, select **Preferences**. +1. Select **Preferences**. 1. In the **Code Suggestions** section, select the **Enable Code Suggestions** checkbox. 1. Select **Save changes**. diff --git a/doc/user/project/repository/gpg_signed_commits/index.md b/doc/user/project/repository/gpg_signed_commits/index.md index c06c695ffa2..8d8639400bd 100644 --- a/doc/user/project/repository/gpg_signed_commits/index.md +++ b/doc/user/project/repository/gpg_signed_commits/index.md @@ -119,7 +119,7 @@ If you don't already have a GPG key, create one: To add a GPG key to your user settings: 1. Sign in to GitLab. -1. In the upper-right corner, select your avatar. +1. On the left sidebar, select your avatar. 1. Select **Edit profile**. 1. Select **GPG Keys** (**{key}**). 1. In **Key**, paste your _public_ key. @@ -253,7 +253,7 @@ If a GPG key becomes compromised, revoke it. Revoking a key changes both future To revoke a GPG key: -1. In the upper-right corner, select your avatar. +1. On the left sidebar, select your avatar. 1. Select **Edit profile**. 1. Select **GPG Keys** (**{key}**). 1. Select **Revoke** next to the GPG key you want to delete. @@ -268,7 +268,7 @@ When you remove a GPG key from your GitLab account: To remove a GPG key from your account: -1. In the upper-right corner, select your avatar. +1. On the left sidebar, select your avatar. 1. Select **Edit profile**. 1. Select **GPG Keys** (**{key}**). 1. Select **Remove** (**{remove}**) next to the GPG key you want to delete. diff --git a/doc/user/project/repository/ssh_signed_commits/index.md b/doc/user/project/repository/ssh_signed_commits/index.md index 8f29845fd9b..85a8917022e 100644 --- a/doc/user/project/repository/ssh_signed_commits/index.md +++ b/doc/user/project/repository/ssh_signed_commits/index.md @@ -169,7 +169,7 @@ If an SSH key becomes compromised, revoke it. Revoking a key changes both future To revoke an SSH key: -1. In the upper-right corner, select your avatar. +1. On the left sidebar, select your avatar. 1. Select **Edit profile**. 1. On the left sidebar, select (**{key}**) **SSH Keys**. 1. Select **Revoke** next to the SSH key you want to delete. diff --git a/doc/user/project/working_with_projects.md b/doc/user/project/working_with_projects.md index 2d47fa4cdf1..361397abac2 100644 --- a/doc/user/project/working_with_projects.md +++ b/doc/user/project/working_with_projects.md @@ -9,40 +9,46 @@ info: "To determine the technical writer assigned to the Stage/Group associated Most work in GitLab is done in a [project](../../user/project/index.md). Files and code are saved in projects, and most features are in the scope of projects. -## View projects +## View all projects for the instance -To view all your projects: - -1. On the left sidebar, expand the top-most chevron (**{chevron-down}**). -1. Select **View all your projects**. - -To browse all projects you can access: +To view all projects for the GitLab instance: 1. On the left sidebar, expand the top-most chevron (**{chevron-down}**). 1. Select **Explore**. -### Who can view the Projects page +On the left sidebar, **Projects** is selected. On the right, the list shows +all projects for the instance. -When you select a project, the project landing page shows the project contents. +If you are not authenticated, then the list shows public projects only. -For public projects, and members of internal and private projects -with [permissions to view the project's code](../permissions.md#project-members-permissions), -the project landing page shows: +## View projects you are a member of -- A [`README` or index file](repository/index.md#readme-and-index-files). -- A list of directories in the project's repository. +To view projects you are a member of: -For users without permission to view the project's code, the landing page shows: +1. On the left sidebar, expand the top-most chevron (**{chevron-down}**). +1. Select **Your work**. -- The wiki homepage. -- The list of issues in the project. +On the left sidebar, **Projects** is selected. On the list, on the **Yours** tab, +all the projects you are a member of are displayed. -### Access a project page with the project ID +## View personal projects -> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/53671) in GitLab 11.8. +Personal projects are projects created under your personal namespace. -To access a project from the GitLab UI using the project ID, -visit the `/projects/:id` URL in your browser or other tool accessing the project. +For example, if you create an account with the username `alex`, and create a project +called `my-project` under your username, the project is created at `https://gitlab.example.com/alex/my-project`. + +To view your personal projects: + +1. On the left sidebar, select your avatar and then your username. +1. On the left sidebar, select **Personal projects**. + +## View starred projects + +To view projects you have [starred](#star-a-project): + +1. On the left sidebar, select your avatar and then your username. +1. On the left sidebar, select **Starred projects**. ## Organizing projects with topics @@ -129,32 +135,6 @@ To add a star to a project: 1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project. 1. In the upper-right corner of the page, select **Star**. -## View starred projects - -1. On the left sidebar, expand the top-most chevron (**{chevron-down}**). -1. Select **View all your projects**. -1. Select the **Starred** tab. -1. GitLab displays information about your starred projects, including: - - - Project description, including name, description, and icon. - - Number of times this project has been starred. - - Number of times this project has been forked. - - Number of open merge requests. - - Number of open issues. - -## View personal projects - -Personal projects are projects created under your personal namespace. - -For example, if you create an account with the username `alex`, and create a project -called `my-project` under your username, the project is created at `https://gitlab.example.com/alex/my-project`. - -To view your personal projects: - -1. On the left sidebar, expand the top-most chevron (**{chevron-down}**). -1. Select **View all your projects**. -1. In the **Yours** tab, select **Personal**. - ## Delete a project After you delete a project: @@ -248,6 +228,29 @@ Prerequisite: 1. Use the toggle by each feature you want to turn on or off, or change access for. 1. Select **Save changes**. +## Access the Project overview page by using the project ID + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/53671) in GitLab 11.8. + +To access a project from the GitLab UI by using the project ID, +put the `/projects/:id` URL in your browser or other tool you use to access the project. + +## Who can view the Project overview page + +When you select a project, the **Project overview** page shows the project contents. + +For public projects, and members of internal and private projects +with [permissions to view the project's code](../permissions.md#project-members-permissions), +the project landing page shows: + +- A [`README` or index file](repository/index.md#readme-and-index-files). +- A list of directories in the project's repository. + +For users without permission to view the project's code, the landing page shows: + +- The wiki homepage. +- The list of issues in the project. + ## Leave a project When you leave a project: diff --git a/lib/gitlab/checks/file_size_check/any_oversized_blob.rb b/lib/gitlab/checks/file_size_check/any_oversized_blob.rb new file mode 100644 index 00000000000..dcdcd0e64ad --- /dev/null +++ b/lib/gitlab/checks/file_size_check/any_oversized_blob.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Gitlab + module Checks + module FileSizeCheck + class AnyOversizedBlob + def initialize(project:, changes:, file_size_limit_megabytes:) + @project = project + @newrevs = changes.pluck(:newrev).compact # rubocop:disable CodeReuse/ActiveRecord just plucking from an array + @file_size_limit_megabytes = file_size_limit_megabytes + end + attr_reader :project, :newrevs, :file_size_limit_megabytes + + def find!(timeout: nil) + blobs = project.repository.new_blobs(newrevs, dynamic_timeout: timeout) + + blobs.find do |blob| + ::Gitlab::Utils.bytes_to_megabytes(blob.size) > file_size_limit_megabytes + end + end + end + end + end +end diff --git a/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder.rb b/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder.rb index 8c0f082f61c..5f4304a7e1b 100644 --- a/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder.rb +++ b/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder.rb @@ -67,7 +67,11 @@ module Gitlab .select(finder_strategy.final_projections) .where("count <> 0") # filter out the initializer row - model.from(q.arel.as(table_name)) + if Feature.enabled?(:key_set_optimizer_ignored_columns) + model.select(Arel.star).from(q.arel.as(table_name)) + else + model.from(q.arel.as(table_name)) + end end private diff --git a/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy.rb b/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy.rb index 4f79a3593f4..d1675a8e916 100644 --- a/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy.rb +++ b/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy.rb @@ -14,6 +14,7 @@ module Gitlab @finder_query = finder_query @order_by_columns = order_by_columns @table_name = model.table_name + @model = model end def initializer_columns @@ -30,7 +31,11 @@ module Gitlab end def final_projections - ["(#{RECORDS_COLUMN}).*"] + if @model.default_select_columns.is_a?(Array) && Feature.enabled?(:key_set_optimizer_ignored_columns) + @model.default_select_columns.map { |column| "(#{RECORDS_COLUMN}).#{column.name}" } + else + ["(#{RECORDS_COLUMN}).*"] + end end private diff --git a/lib/gitlab/pagination/keyset/order.rb b/lib/gitlab/pagination/keyset/order.rb index e31e83c90da..482b2977b4b 100644 --- a/lib/gitlab/pagination/keyset/order.rb +++ b/lib/gitlab/pagination/keyset/order.rb @@ -246,7 +246,12 @@ module Gitlab scopes = where_values.map do |where_value| scope.dup.where(where_value).reorder(self) # rubocop: disable CodeReuse/ActiveRecord end - scope.model.from_union(scopes, remove_duplicates: false, remove_order: false) + + if Feature.enabled?(:key_set_optimizer_ignored_columns) + scope.model.select(scope.select_values).from_union(scopes, remove_duplicates: false, remove_order: false) + else + scope.model.from_union(scopes, remove_duplicates: false, remove_order: false) + end end def to_sql_literal(column_definitions) diff --git a/lib/slack/manifest.rb b/lib/slack/manifest.rb new file mode 100644 index 00000000000..de189f3bdf5 --- /dev/null +++ b/lib/slack/manifest.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +module Slack + module Manifest + class << self + delegate :to_json, to: :to_h + + def share_url + "https://api.slack.com/apps?new_app=1&manifest_json=#{ERB::Util.url_encode(to_json)}" + end + + def to_h + { + display_information: display_information, + features: features, + oauth_config: oauth_config, + settings: settings + } + end + + private + + def display_information + { + name: "GitLab (#{Gitlab.config.gitlab.host.first(26)})", + description: s_('SlackIntegration|Interact with GitLab without leaving your Slack workspace!'), + background_color: '#171321', + # Each element in this array will become a paragraph joined with `\r\n\r\n'. + long_description: [ + format( + s_( + 'SlackIntegration|Generated for %{host} by GitLab %{version}.' + ), + host: Gitlab.config.gitlab.host, + version: Gitlab::VERSION + ), + s_( + 'SlackIntegration|- *Notifications:* Get notifications to your team\'s Slack channel about events ' \ + 'happening inside your GitLab projects.' + ), + format( + s_( + 'SlackIntegration|- *Slash commands:* Quickly open, access, or close issues from Slack using the ' \ + '`%{slash_command}` command. Streamline your GitLab deployments with ChatOps.' + ), + slash_command: '/gitlab' + ) + ].join("\r\n\r\n") + } + end + + def features + { + app_home: { + home_tab_enabled: true, + messages_tab_enabled: false, + messages_tab_read_only_enabled: true + }, + bot_user: { + display_name: 'GitLab', + always_online: true + }, + slash_commands: [ + { + command: '/gitlab', + url: api_v4('slack/trigger'), + description: s_('SlackIntegration|GitLab slash commands'), + usage_hint: s_('SlackIntegration|your-project-name-or-alias command'), + should_escape: false + } + ] + } + end + + def oauth_config + { + redirect_urls: [ + Gitlab.config.gitlab.url + ], + scopes: { + bot: %w[ + commands + chat:write + chat:write.public + ] + } + } + end + + def settings + { + event_subscriptions: { + request_url: api_v4('integrations/slack/events'), + bot_events: %w[ + app_home_opened + ] + }, + interactivity: { + is_enabled: true, + request_url: api_v4('integrations/slack/interactions'), + message_menu_options_url: api_v4('integrations/slack/options') + }, + org_deploy_enabled: false, + socket_mode_enabled: false, + token_rotation_enabled: false + } + end + + def api_v4(path) + "#{Gitlab.config.gitlab.url}/api/v4/#{path}" + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 92fb8d868ee..b0fbb0f8e9d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -114,11 +114,6 @@ msgid_plural "%d Modules" msgstr[0] "" msgstr[1] "" -msgid "%d Other" -msgid_plural "%d Others" -msgstr[0] "" -msgstr[1] "" - msgid "%d Package" msgid_plural "%d Packages" msgstr[0] "" @@ -672,9 +667,6 @@ msgstr "" msgid "%{count} total weight" msgstr "" -msgid "%{criticalStart}%{critical} Critical%{criticalEnd} %{highStart}%{high} High%{highEnd} and %{otherStart}%{otherMessage}%{otherEnd}" -msgstr "" - msgid "%{dashboard_path} could not be found." msgstr "" @@ -1036,15 +1028,6 @@ msgstr[1] "" msgid "%{remaining_approvals} left" msgstr "" -msgid "%{reportType} %{status}" -msgstr "" - -msgid "%{reportType} detected %{totalStart}%{total}%{totalEnd} potential %{vulnMessage}" -msgstr "" - -msgid "%{reportType} detected no new vulnerabilities." -msgstr "" - msgid "%{requireStart}Require%{requireEnd} %{approvalsRequired} %{approvalStart}approval%{approvalEnd} from:" msgid_plural "%{requireStart}Require%{requireEnd} %{approvalsRequired} %{approvalStart}approvals%{approvalEnd} from:" msgstr[0] "" @@ -5608,7 +5591,7 @@ msgstr "" msgid "ApplicationSettings|Email restrictions for sign-ups" msgstr "" -msgid "ApplicationSettings|Enable Slack application" +msgid "ApplicationSettings|Enable GitLab for Slack app" msgstr "" msgid "ApplicationSettings|Enable domain denylist for sign-ups" @@ -5677,9 +5660,6 @@ msgstr "" msgid "ApplicationSettings|This feature is only available on GitLab.com" msgstr "" -msgid "ApplicationSettings|This option is only available on GitLab.com" -msgstr "" - msgid "ApplicationSettings|Upload denylist file" msgstr "" @@ -13207,6 +13187,9 @@ msgstr "" msgid "Create or import your first project" msgstr "" +msgid "Create phone verification exemption" +msgstr "" + msgid "Create project" msgstr "" @@ -33143,6 +33126,15 @@ msgstr "" msgid "Phone" msgstr "" +msgid "Phone verification exemption" +msgstr "" + +msgid "Phone verification exemption has been created." +msgstr "" + +msgid "Phone verification exemption has been removed." +msgstr "" + msgid "PhoneVerification|Enter a valid code." msgstr "" @@ -38245,6 +38237,9 @@ msgstr "" msgid "Remove parent epic from an epic" msgstr "" +msgid "Remove phone verification exemption" +msgstr "" + msgid "Remove priority" msgstr "" @@ -40984,12 +40979,6 @@ msgstr "" msgid "Security dashboard" msgstr "" -msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})" -msgstr "" - -msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})" -msgstr "" - msgid "SecurityApprovals|A merge request approval is required when test coverage declines." msgstr "" @@ -41823,9 +41812,6 @@ msgstr "" msgid "SecurityReports|More info" msgstr "" -msgid "SecurityReports|New vulnerabilities are vulnerabilities that the security scan detects in the merge request that are different to existing vulnerabilities in the default branch." -msgstr "" - msgid "SecurityReports|No longer detected" msgstr "" @@ -41880,12 +41866,6 @@ msgstr "" msgid "SecurityReports|Security reports can only be accessed by authorized users." msgstr "" -msgid "SecurityReports|Security reports help page link" -msgstr "" - -msgid "SecurityReports|Security scan results" -msgstr "" - msgid "SecurityReports|Security scans have run" msgstr "" @@ -41985,12 +41965,6 @@ msgstr "" msgid "SecurityReports|Undo dismiss" msgstr "" -msgid "SecurityReports|Upgrade to interact, track and shift left with vulnerability management features in the UI." -msgstr "" - -msgid "SecurityReports|Upgrade to manage vulnerabilities" -msgstr "" - msgid "SecurityReports|Vulnerability report" msgstr "" @@ -43162,12 +43136,6 @@ msgstr "" msgid "Skype:" msgstr "" -msgid "Slack application" -msgstr "" - -msgid "Slack integration allows you to interact with GitLab via slash commands in a chat window." -msgstr "" - msgid "Slack logo" msgstr "" @@ -43180,6 +43148,12 @@ msgstr "" msgid "Slack notifications will be deprecated" msgstr "" +msgid "SlackIntegration|- *Notifications:* Get notifications to your team's Slack channel about events happening inside your GitLab projects." +msgstr "" + +msgid "SlackIntegration|- *Slash commands:* Quickly open, access, or close issues from Slack using the `%{slash_command}` command. Streamline your GitLab deployments with ChatOps." +msgstr "" + msgid "SlackIntegration|Are you sure you want to remove this project from the GitLab for Slack app?" msgstr "" @@ -43189,18 +43163,39 @@ msgstr "" msgid "SlackIntegration|Client secret" msgstr "" +msgid "SlackIntegration|Configure your GitLab for Slack app." +msgstr "" + +msgid "SlackIntegration|Copy the %{link_start}settings%{link_end} from %{strong_open}%{settings_heading}%{strong_close} in your GitLab for Slack app." +msgstr "" + +msgid "SlackIntegration|Create Slack app" +msgstr "" + msgid "SlackIntegration|Create and read issue data and comments." msgstr "" +msgid "SlackIntegration|Download latest manifest file" +msgstr "" + +msgid "SlackIntegration|Generated for %{host} by GitLab %{version}." +msgstr "" + msgid "SlackIntegration|GitLab for Slack" msgstr "" msgid "SlackIntegration|GitLab for Slack was successfully installed." msgstr "" +msgid "SlackIntegration|GitLab slash commands" +msgstr "" + msgid "SlackIntegration|Install GitLab for Slack app" msgstr "" +msgid "SlackIntegration|Interact with GitLab without leaving your Slack workspace!" +msgstr "" + msgid "SlackIntegration|Perform deployments." msgstr "" @@ -43228,6 +43223,12 @@ msgstr "" msgid "SlackIntegration|Signing secret" msgstr "" +msgid "SlackIntegration|Step 1: Create your GitLab for Slack app" +msgstr "" + +msgid "SlackIntegration|Step 2: Configure the app settings" +msgstr "" + msgid "SlackIntegration|Team name" msgstr "" @@ -43243,9 +43244,24 @@ msgstr "" msgid "SlackIntegration|Update to the latest version to receive notifications from GitLab." msgstr "" +msgid "SlackIntegration|Update your Slack app" +msgstr "" + +msgid "SlackIntegration|Used for authenticating API requests from the GitLab for Slack app." +msgstr "" + +msgid "SlackIntegration|Used for authenticating OAuth requests from the GitLab for Slack app." +msgstr "" + +msgid "SlackIntegration|Used only for authenticating slash commands from the GitLab for Slack app. This method of authentication is deprecated by Slack." +msgstr "" + msgid "SlackIntegration|Verification token" msgstr "" +msgid "SlackIntegration|When GitLab releases new features for the GitLab for Slack app, you might have to manually update your copy to use the new features." +msgstr "" + msgid "SlackIntegration|You can now close this window and go to your Slack workspace." msgstr "" @@ -43255,9 +43271,15 @@ msgstr "" msgid "SlackIntegration|You may need to reinstall the GitLab for Slack app when we %{linkStart}make updates or change permissions%{linkEnd}." msgstr "" +msgid "SlackIntegration|You must do this step only once." +msgstr "" + msgid "SlackIntegration|cannot have more than %{limit} channels" msgstr "" +msgid "SlackIntegration|your-project-name-or-alias command" +msgstr "" + msgid "SlackModal|Are you sure you want to change the project?" msgstr "" @@ -43633,6 +43655,12 @@ msgstr "" msgid "Something went wrong. Try again later." msgstr "" +msgid "Something went wrong. Unable to create phone exemption." +msgstr "" + +msgid "Something went wrong. Unable to remove phone exemption." +msgstr "" + msgid "Sorry, no projects matched your search" msgstr "" @@ -47335,6 +47363,9 @@ msgstr "" msgid "This user has the %{access} role in the %{name} project." msgstr "" +msgid "This user is currently exempt from phone verification. Remove the exemption using the button below." +msgstr "" + msgid "This user is the author of this %{noteable}." msgstr "" @@ -47887,6 +47918,9 @@ msgstr "" msgid "To remove the %{link_start}read-only%{link_end} state and regain write access, you can reduce the number of users in your top-level group to %{free_limit} users or less. You can also upgrade to a paid tier, which do not have user limits. If you need additional time, you can start a free 30-day trial which includes unlimited users. To minimize the impact to operations, for a limited time, GitLab is offering a %{promotion_link_start}one-time 70 percent discount%{link_end} off the list price at time of purchase for a new, one year subscription of GitLab Premium SaaS to %{offer_availability_link_start}qualifying top-level groups%{link_end}. The offer is valid until %{offer_date}." msgstr "" +msgid "To replace phone verification with credit card verification, create a phone verification exemption using the button below." +msgstr "" + msgid "To resolve this, try to:" msgstr "" @@ -53708,27 +53742,6 @@ msgstr "" msgid "ciReport|%{improvedNum} improved" msgstr "" -msgid "ciReport|%{linkStartTag}Learn more about API Fuzzing%{linkEndTag}" -msgstr "" - -msgid "ciReport|%{linkStartTag}Learn more about Container Scanning %{linkEndTag}" -msgstr "" - -msgid "ciReport|%{linkStartTag}Learn more about Coverage Fuzzing %{linkEndTag}" -msgstr "" - -msgid "ciReport|%{linkStartTag}Learn more about DAST %{linkEndTag}" -msgstr "" - -msgid "ciReport|%{linkStartTag}Learn more about Dependency Scanning %{linkEndTag}" -msgstr "" - -msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}" -msgstr "" - -msgid "ciReport|%{linkStartTag}Learn more about Secret Detection %{linkEndTag}" -msgstr "" - msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}" msgstr "" @@ -53738,12 +53751,6 @@ msgstr "" msgid "ciReport|%{remainingPackagesCount} more" msgstr "" -msgid "ciReport|%{reportType} is loading" -msgstr "" - -msgid "ciReport|%{reportType}: Loading resulted in an error" -msgstr "" - msgid "ciReport|%{sameNum} same" msgstr "" @@ -53762,9 +53769,6 @@ msgstr "" msgid "ciReport|%{scanner}: Loading resulted in an error" msgstr "" -msgid "ciReport|: Loading resulted in an error" -msgstr "" - msgid "ciReport|API Fuzzing" msgstr "" @@ -53834,18 +53838,12 @@ msgstr "" msgid "ciReport|Container Scanning" msgstr "" -msgid "ciReport|Container Scanning detects known vulnerabilities in your container images." -msgstr "" - msgid "ciReport|Container scanning" msgstr "" msgid "ciReport|Container scanning detects known vulnerabilities in your docker images." msgstr "" -msgid "ciReport|Could not dismiss vulnerability because the associated pipeline no longer exists. Refresh the page and try again." -msgstr "" - msgid "ciReport|Coverage Fuzzing" msgstr "" @@ -53867,9 +53865,6 @@ msgstr "" msgid "ciReport|Dependency Scanning" msgstr "" -msgid "ciReport|Dependency Scanning detects known vulnerabilities in your project's dependencies." -msgstr "" - msgid "ciReport|Dependency scanning" msgstr "" @@ -53894,9 +53889,6 @@ msgstr "" msgid "ciReport|Dynamic Application Security Testing (DAST)" msgstr "" -msgid "ciReport|Dynamic Application Security Testing (DAST) detects vulnerabilities in your web application." -msgstr "" - msgid "ciReport|Failed to load %{reportName} report" msgstr "" @@ -53959,9 +53951,6 @@ msgstr "" msgid "ciReport|Manually added" msgstr "" -msgid "ciReport|New" -msgstr "" - msgid "ciReport|New vulnerabilities are vulnerabilities that the security scan detects in the merge request that are different to existing vulnerabilities in the default branch." msgstr "" @@ -53986,9 +53975,6 @@ msgstr "" msgid "ciReport|Secret Detection" msgstr "" -msgid "ciReport|Secret Detection detects leaked credentials in your source code." -msgstr "" - msgid "ciReport|Secret detection" msgstr "" @@ -54001,9 +53987,6 @@ msgstr "" msgid "ciReport|Security scanning" msgstr "" -msgid "ciReport|Security scanning failed loading any results" -msgstr "" - msgid "ciReport|Security scanning is loading" msgstr "" @@ -54019,9 +54002,6 @@ msgstr "" msgid "ciReport|Static Application Security Testing (SAST)" msgstr "" -msgid "ciReport|Static Application Security Testing (SAST) detects potential vulnerabilities in your source code." -msgstr "" - msgid "ciReport|TTFB P90" msgstr "" @@ -54063,12 +54043,6 @@ msgstr "" msgid "ciReport|in" msgstr "" -msgid "ciReport|is loading" -msgstr "" - -msgid "ciReport|is loading, errors when loading results" -msgstr "" - msgid "closed" msgstr "" diff --git a/rubocop/cop/ignored_columns.rb b/rubocop/cop/ignored_columns.rb index 8b17447c46b..86ab8df6927 100644 --- a/rubocop/cop/ignored_columns.rb +++ b/rubocop/cop/ignored_columns.rb @@ -19,8 +19,8 @@ module RuboCop # ignore_column :full_name, remove_after: '2023-05-22', remove_with: '16.0' # end class IgnoredColumns < RuboCop::Cop::Base - USE_CONCERN_ADD_MSG = 'Use `IgnoredColumns` concern instead of adding to `self.ignored_columns`.' - USE_CONCERN_SET_MSG = 'Use `IgnoredColumns` concern instead of setting `self.ignored_columns`.' + USE_CONCERN_ADD_MSG = 'Use `IgnorableColumns` concern instead of adding to `self.ignored_columns`.' + USE_CONCERN_SET_MSG = 'Use `IgnorableColumns` concern instead of setting `self.ignored_columns`.' WRONG_MODEL_MSG = <<~MSG If the model exists in CE and EE, the column has to be ignored in the CE model. If the model only exists in EE, then it has to be added there. diff --git a/spec/config/settings_spec.rb b/spec/config/settings_spec.rb index 55e675d5107..4639e533922 100644 --- a/spec/config/settings_spec.rb +++ b/spec/config/settings_spec.rb @@ -170,12 +170,12 @@ RSpec.describe Settings, feature_category: :system_access do it 'defaults to using the encrypted_settings_key_base for the key' do expect(Gitlab::EncryptedConfiguration).to receive(:new).with(hash_including(base_key: Gitlab::Application.secrets.encrypted_settings_key_base)) - Settings.encrypted('tmp/tests/test.enc') + described_class.encrypted('tmp/tests/test.enc') end it 'returns empty encrypted config when a key has not been set' do allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(nil) - expect(Settings.encrypted('tmp/tests/test.enc').read).to be_empty + expect(described_class.encrypted('tmp/tests/test.enc').read).to be_empty end end diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb index 537424093fb..60343c822af 100644 --- a/spec/controllers/admin/application_settings_controller_spec.rb +++ b/spec/controllers/admin/application_settings_controller_spec.rb @@ -487,6 +487,43 @@ RSpec.describe Admin::ApplicationSettingsController, :do_not_mock_admin_mode_set end end + describe 'GET #slack_app_manifest_download', feature_category: :integrations do + before do + sign_in(admin) + end + + subject { get :slack_app_manifest_download } + + it 'downloads the GitLab for Slack app manifest' do + allow(Slack::Manifest).to receive(:to_h).and_return({ foo: 'bar' }) + + subject + + expect(response.body).to eq('{"foo":"bar"}') + expect(response.headers['Content-Disposition']).to eq( + 'attachment; filename="slack_manifest.json"; filename*=UTF-8\'\'slack_manifest.json' + ) + end + end + + describe 'GET #slack_app_manifest_share', feature_category: :integrations do + before do + sign_in(admin) + end + + subject { get :slack_app_manifest_share } + + it 'redirects the user to the Slack Manifest share URL' do + allow(Slack::Manifest).to receive(:to_h).and_return({ foo: 'bar' }) + + subject + + expect(response).to redirect_to( + "https://api.slack.com/apps?new_app=1&manifest_json=%7B%22foo%22%3A%22bar%22%7D" + ) + end + end + describe 'GET #service_usage_data', feature_category: :service_ping do before do stub_usage_data_connections diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb index 276bd9b65b9..88af7d1fe45 100644 --- a/spec/controllers/repositories/git_http_controller_spec.rb +++ b/spec/controllers/repositories/git_http_controller_spec.rb @@ -79,7 +79,7 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m end context 'when repository container is a project' do - it_behaves_like Repositories::GitHttpController do + it_behaves_like described_class do let(:container) { project } let(:user) { project.first_owner } let(:access_checker_class) { Gitlab::GitAccess } @@ -133,7 +133,7 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m end context 'when the user is a deploy token' do - it_behaves_like Repositories::GitHttpController do + it_behaves_like described_class do let(:container) { project } let(:user) { create(:deploy_token, :project, projects: [project]) } let(:access_checker_class) { Gitlab::GitAccess } @@ -144,7 +144,7 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m end context 'when repository container is a project wiki' do - it_behaves_like Repositories::GitHttpController do + it_behaves_like described_class do let(:container) { create(:project_wiki, :empty_repo, project: project) } let(:user) { project.first_owner } let(:access_checker_class) { Gitlab::GitAccessWiki } @@ -155,7 +155,7 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m end context 'when repository container is a personal snippet' do - it_behaves_like Repositories::GitHttpController do + it_behaves_like described_class do let(:container) { personal_snippet } let(:user) { personal_snippet.author } let(:access_checker_class) { Gitlab::GitAccessSnippet } @@ -167,7 +167,7 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m end context 'when repository container is a project snippet' do - it_behaves_like Repositories::GitHttpController do + it_behaves_like described_class do let(:container) { project_snippet } let(:user) { project_snippet.author } let(:access_checker_class) { Gitlab::GitAccessSnippet } diff --git a/spec/experiments/application_experiment_spec.rb b/spec/experiments/application_experiment_spec.rb index ef8f8cbce3b..461a6390a33 100644 --- a/spec/experiments/application_experiment_spec.rb +++ b/spec/experiments/application_experiment_spec.rb @@ -36,7 +36,7 @@ RSpec.describe ApplicationExperiment, :experiment, feature_category: :experiment # _published_experiments.html.haml partial. application_experiment.publish - expect(ApplicationExperiment.published_experiments['namespaced/stub']).to include( + expect(described_class.published_experiments['namespaced/stub']).to include( experiment: 'namespaced/stub', excluded: false, key: anything, diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index 411ea8ea12e..55d3ad3aca3 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -8,11 +8,9 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do include UsageDataHelpers let_it_be(:admin) { create(:admin) } - let(:dot_com?) { false } context 'application setting :admin_mode is enabled', :request_store do before do - allow(Gitlab).to receive(:com?).and_return(dot_com?) stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') sign_in(admin) gitlab_enable_admin_mode_sign_in(admin) @@ -147,9 +145,7 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do end context 'Dormant users', feature_category: :user_management do - context 'when Gitlab.com' do - let(:dot_com?) { true } - + context 'when Gitlab.com', :saas do it 'does not expose the setting section' do # NOTE: not_to have_content may have false positives for content # that might not load instantly, so before checking that @@ -163,8 +159,6 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do end context 'when not Gitlab.com' do - let(:dot_com?) { false } - it 'exposes the setting section' do expect(page).to have_content('Dormant users') expect(page).to have_field('Deactivate dormant users after a period of inactivity') @@ -366,9 +360,46 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do end context 'GitLab for Slack app settings', feature_category: :integrations do + let(:create_heading) { 'Create your GitLab for Slack app' } + let(:configure_heading) { 'Configure the app settings' } + let(:update_heading) { 'Update your Slack app' } + + it 'has all sections' do + page.within('.as-slack') do + expect(page).to have_content(create_heading) + expect(page).to have_content(configure_heading) + expect(page).to have_content(update_heading) + end + end + + context 'when GitLab.com', :saas do + it 'only has the configure section' do + page.within('.as-slack') do + expect(page).to have_content(configure_heading) + + expect(page).not_to have_content(create_heading) + expect(page).not_to have_content(update_heading) + end + end + end + + context 'when the `slack_app_self_managed` flag is disabled' do + before do + stub_feature_flags(slack_app_self_managed: false) + visit general_admin_application_settings_path + end + + it 'does not display any sections' do + expect(page).not_to have_selector('.as-slack') + expect(page).not_to have_content(configure_heading) + expect(page).not_to have_content(create_heading) + expect(page).not_to have_content(update_heading) + end + end + it 'changes the settings' do page.within('.as-slack') do - check 'Enable Slack application' + check 'Enable GitLab for Slack app' fill_in 'Client ID', with: 'slack_app_id' fill_in 'Client secret', with: 'slack_app_secret' fill_in 'Signing secret', with: 'slack_app_signing_secret' diff --git a/spec/fixtures/api/schemas/slack/manifest.json b/spec/fixtures/api/schemas/slack/manifest.json new file mode 100644 index 00000000000..236c5df46bc --- /dev/null +++ b/spec/fixtures/api/schemas/slack/manifest.json @@ -0,0 +1,164 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "display_information": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 35 + }, + "description": { + "type": "string", + "maxLength": 140 + }, + "background_color": { + "type": "string", + "pattern": "^#[0-9A-F]{6}$" + }, + "long_description": { + "type": "string", + "maxLength": 4000 + } + }, + "required": [ + "name" + ] + }, + "features": { + "type": "object", + "properties": { + "app_home": { + "type": "object", + "properties": { + "home_tab_enabled": { + "type": "boolean" + }, + "messages_tab_enabled": { + "type": "boolean" + }, + "messages_tab_read_only_enabled": { + "type": "boolean" + } + } + }, + "bot_user": { + "type": "object", + "properties": { + "display_name": { + "type": "string", + "maxLength": 80 + }, + "always_online": { + "type": "boolean" + } + } + }, + "slash_commands": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "command": { + "type": "string", + "maxLength": 32 + }, + "url": { + "type": "string" + }, + "description": { + "type": "string", + "maxLength": 2000 + }, + "usage_hint": { + "type": "string", + "maxLength": 1000 + }, + "should_escape": { + "type": "boolean" + } + }, + "required": [ + "command", + "description" + ] + } + ] + } + } + }, + "oauth_config": { + "type": "object", + "properties": { + "redirect_urls": { + "type": "array", + "maxContains": 1000, + "items": [ + { + "type": "string" + } + ] + }, + "scopes": { + "type": "object", + "properties": { + "bot": { + "type": "array", + "maxContains": 255 + } + } + } + } + }, + "settings": { + "type": "object", + "properties": { + "event_subscriptions": { + "type": "object", + "properties": { + "request_url": { + "type": "string" + }, + "bot_events": { + "type": "array", + "maxContains": 100, + "items": [ + { + "type": "string" + } + ] + } + } + }, + "interactivity": { + "type": "object", + "properties": { + "is_enabled": { + "type": "boolean" + }, + "request_url": { + "type": "string" + }, + "message_menu_options_url": { + "type": "string" + } + } + }, + "org_deploy_enabled": { + "type": "boolean" + }, + "socket_mode_enabled": { + "type": "boolean" + }, + "token_rotation_enabled": { + "type": "boolean" + } + } + } + }, + "required": [ + "display_information" + ] +} diff --git a/spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap b/spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap deleted file mode 100644 index 311a67a3e31..00000000000 --- a/spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap +++ /dev/null @@ -1,26 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Grouped Issues List renders a smart virtual list with the correct props 1`] = ` -Object { - "length": 4, - "remain": 20, - "rtag": "div", - "size": 32, - "wclass": "report-block-list", - "wtag": "ul", -} -`; - -exports[`Grouped Issues List with data renders a report item with the correct props 1`] = ` -Object { - "component": "CodequalityIssueBody", - "iconComponent": "IssueStatusIcon", - "isNew": false, - "issue": Object { - "name": "foo", - }, - "showReportSectionStatusIcon": false, - "status": "none", - "statusIconSize": 24, -} -`; diff --git a/spec/frontend/ci/reports/components/grouped_issues_list_spec.js b/spec/frontend/ci/reports/components/grouped_issues_list_spec.js deleted file mode 100644 index 8beec220802..00000000000 --- a/spec/frontend/ci/reports/components/grouped_issues_list_spec.js +++ /dev/null @@ -1,83 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import GroupedIssuesList from '~/ci/reports/components/grouped_issues_list.vue'; -import ReportItem from '~/ci/reports/components/report_item.vue'; -import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue'; - -describe('Grouped Issues List', () => { - let wrapper; - - const createComponent = ({ propsData = {}, stubs = {} } = {}) => { - wrapper = shallowMount(GroupedIssuesList, { - propsData, - stubs, - }); - }; - - const findHeading = (groupName) => wrapper.find(`[data-testid="${groupName}Heading"`); - - it('renders a smart virtual list with the correct props', () => { - createComponent({ - propsData: { - resolvedIssues: [{ name: 'foo' }], - unresolvedIssues: [{ name: 'bar' }], - }, - stubs: { - SmartVirtualList, - }, - }); - - expect(wrapper.findComponent(SmartVirtualList).props()).toMatchSnapshot(); - }); - - describe('without data', () => { - beforeEach(() => { - createComponent(); - }); - - it.each(['unresolved', 'resolved'])('does not a render a header for %s issues', (issueName) => { - expect(findHeading(issueName).exists()).toBe(false); - }); - - it.each(['resolved', 'unresolved'])('does not render report items for %s issues', () => { - expect(wrapper.findComponent(ReportItem).exists()).toBe(false); - }); - }); - - describe('with data', () => { - it.each` - givenIssues | givenHeading | groupName - ${[{ name: 'foo issue' }]} | ${'Foo Heading'} | ${'resolved'} - ${[{ name: 'bar issue' }]} | ${'Bar Heading'} | ${'unresolved'} - `('renders the heading for $groupName issues', ({ givenIssues, givenHeading, groupName }) => { - createComponent({ - propsData: { [`${groupName}Issues`]: givenIssues, [`${groupName}Heading`]: givenHeading }, - }); - - expect(findHeading(groupName).text()).toBe(givenHeading); - }); - - it.each(['resolved', 'unresolved'])('renders all %s issues', (issueName) => { - const issues = [{ name: 'foo' }, { name: 'bar' }]; - - createComponent({ - propsData: { [`${issueName}Issues`]: issues }, - }); - - expect(wrapper.findAllComponents(ReportItem)).toHaveLength(issues.length); - }); - - it('renders a report item with the correct props', () => { - createComponent({ - propsData: { - resolvedIssues: [{ name: 'foo' }], - component: 'CodequalityIssueBody', - }, - stubs: { - ReportItem, - }, - }); - - expect(wrapper.findComponent(ReportItem).props()).toMatchSnapshot(); - }); - }); -}); diff --git a/spec/frontend/ci/reports/components/summary_row_spec.js b/spec/frontend/ci/reports/components/summary_row_spec.js deleted file mode 100644 index d4d2ac29fb1..00000000000 --- a/spec/frontend/ci/reports/components/summary_row_spec.js +++ /dev/null @@ -1,63 +0,0 @@ -import { mount } from '@vue/test-utils'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import HelpPopover from '~/vue_shared/components/help_popover.vue'; -import SummaryRow from '~/ci/reports/components/summary_row.vue'; - -describe('Summary row', () => { - let wrapper; - - const summary = 'SAST detected 1 new vulnerability and 1 fixed vulnerability'; - const popoverOptions = { - title: 'Static Application Security Testing (SAST)', - content: 'Learn more about SAST', - }; - const statusIcon = 'warning'; - - const createComponent = ({ props = {}, slots = {} } = {}) => { - wrapper = extendedWrapper( - mount(SummaryRow, { - propsData: { - summary, - popoverOptions, - statusIcon, - ...props, - }, - slots, - }), - ); - }; - - const findSummary = () => wrapper.findByTestId('summary-row-description'); - const findStatusIcon = () => wrapper.findByTestId('summary-row-icon'); - const findHelpPopover = () => wrapper.findComponent(HelpPopover); - - it('renders provided summary', () => { - createComponent(); - expect(findSummary().text()).toContain(summary); - }); - - it('renders provided icon', () => { - createComponent(); - expect(findStatusIcon().find('[data-testid="status_warning-icon"]').exists()).toBe(true); - }); - - it('renders help popover if popoverOptions are provided', () => { - createComponent(); - expect(findHelpPopover().props('options')).toEqual(popoverOptions); - }); - - it('does not render help popover if popoverOptions are not provided', () => { - createComponent({ props: { popoverOptions: null } }); - expect(findHelpPopover().exists()).toBe(false); - }); - - describe('summary slot', () => { - it('replaces the summary prop', () => { - const summarySlotContent = 'Summary slot content'; - createComponent({ slots: { summary: summarySlotContent } }); - - expect(wrapper.text()).not.toContain(summary); - expect(findSummary().text()).toContain(summarySlotContent); - }); - }); -}); diff --git a/spec/frontend/fixtures/timezones.rb b/spec/frontend/fixtures/timezones.rb index 2393f4e797d..f04e647c8eb 100644 --- a/spec/frontend/fixtures/timezones.rb +++ b/spec/frontend/fixtures/timezones.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe TimeZoneHelper, '(JavaScript fixtures)' do include JavaScriptFixturesHelpers - include TimeZoneHelper + include described_class let(:response) { @timezones.sort_by! { |tz| tz[:name] }.to_json } diff --git a/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap b/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap deleted file mode 100644 index 66d27b5d605..00000000000 --- a/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap +++ /dev/null @@ -1,144 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SecuritySummary component given the message {"countMessage": "%{criticalStart}0 Critical%{criticalEnd} %{highStart}1 High%{highEnd} and %{otherStart}0 Others%{otherEnd}", "critical": 0, "high": 1, "message": "Security scanning detected %{totalStart}1%{totalEnd} potential vulnerability", "other": 0, "status": "", "total": 1} interpolates correctly 1`] = ` - - Security scanning detected - - 1 - - potential vulnerability - - - - - 0 Critical - - - - - - - - 1 High - - - - and - - - - 0 Others - - - - - -`; - -exports[`SecuritySummary component given the message {"countMessage": "%{criticalStart}1 Critical%{criticalEnd} %{highStart}0 High%{highEnd} and %{otherStart}0 Others%{otherEnd}", "critical": 1, "high": 0, "message": "Security scanning detected %{totalStart}1%{totalEnd} potential vulnerability", "other": 0, "status": "", "total": 1} interpolates correctly 1`] = ` - - Security scanning detected - - 1 - - potential vulnerability - - - - - 1 Critical - - - - - - - - 0 High - - - - and - - - - 0 Others - - - - - -`; - -exports[`SecuritySummary component given the message {"countMessage": "%{criticalStart}1 Critical%{criticalEnd} %{highStart}2 High%{highEnd} and %{otherStart}0 Others%{otherEnd}", "critical": 1, "high": 2, "message": "Security scanning detected %{totalStart}3%{totalEnd} potential vulnerabilities", "other": 0, "status": "", "total": 3} interpolates correctly 1`] = ` - - Security scanning detected - - 3 - - potential vulnerabilities - - - - - 1 Critical - - - - - - - - 2 High - - - - and - - - - 0 Others - - - - - -`; - -exports[`SecuritySummary component given the message {"message": ""} interpolates correctly 1`] = ` - - - - -`; - -exports[`SecuritySummary component given the message {"message": "foo"} interpolates correctly 1`] = ` - - foo - - -`; diff --git a/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js b/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js deleted file mode 100644 index 6eebd129beb..00000000000 --- a/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js +++ /dev/null @@ -1,104 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import { - expectedDownloadDropdownPropsWithTitle, - securityReportMergeRequestDownloadPathsQueryResponse, -} from 'jest/vue_shared/security_reports/mock_data'; -import { createAlert } from '~/alert'; -import Component from '~/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue'; -import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue'; -import { - REPORT_TYPE_SAST, - REPORT_TYPE_SECRET_DETECTION, -} from '~/vue_shared/security_reports/constants'; -import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql'; - -jest.mock('~/alert'); - -describe('Merge request artifact Download', () => { - let wrapper; - - const defaultProps = { - reportTypes: [REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION], - targetProjectFullPath: '/path', - mrIid: 123, - }; - - const createWrapper = ({ propsData, options }) => { - wrapper = shallowMount(Component, { - stubs: { - SecurityReportDownloadDropdown, - }, - propsData: { - ...defaultProps, - ...propsData, - }, - ...options, - }); - }; - - const pendingHandler = () => new Promise(() => {}); - const successHandler = () => - Promise.resolve({ data: securityReportMergeRequestDownloadPathsQueryResponse }); - const failureHandler = () => Promise.resolve({ errors: [{ message: 'some error' }] }); - const createMockApolloProvider = (handler) => { - Vue.use(VueApollo); - const requestHandlers = [[securityReportMergeRequestDownloadPathsQuery, handler]]; - - return createMockApollo(requestHandlers); - }; - - const findDownloadDropdown = () => wrapper.findComponent(SecurityReportDownloadDropdown); - - describe('given the query is loading', () => { - beforeEach(() => { - createWrapper({ - options: { - apolloProvider: createMockApolloProvider(pendingHandler), - }, - }); - }); - - it('loading is true', () => { - expect(findDownloadDropdown().props('loading')).toBe(true); - }); - }); - - describe('given the query loads successfully', () => { - beforeEach(() => { - createWrapper({ - options: { - apolloProvider: createMockApolloProvider(successHandler), - }, - }); - }); - - it('renders the download dropdown', () => { - expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithTitle); - }); - }); - - describe('given the query fails', () => { - beforeEach(() => { - createWrapper({ - options: { - apolloProvider: createMockApolloProvider(failureHandler), - }, - }); - }); - - it('calls createAlert correctly', () => { - expect(createAlert).toHaveBeenCalledWith({ - message: Component.i18n.apiError, - captureError: true, - error: expect.any(Error), - }); - }); - - it('renders nothing', () => { - expect(findDownloadDropdown().props('artifacts')).toEqual([]); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js b/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js deleted file mode 100644 index 2f6e633fb34..00000000000 --- a/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js +++ /dev/null @@ -1,63 +0,0 @@ -import { GlLink, GlPopover } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import HelpIcon from '~/vue_shared/security_reports/components/help_icon.vue'; - -const helpPath = '/docs'; -const discoverProjectSecurityPath = '/discoverProjectSecurityPath'; - -describe('HelpIcon component', () => { - let wrapper; - - const createWrapper = (props) => { - wrapper = shallowMount(HelpIcon, { - propsData: { - helpPath, - ...props, - }, - }); - }; - - const findLink = () => wrapper.findComponent(GlLink); - const findPopover = () => wrapper.findComponent(GlPopover); - const findPopoverTarget = () => wrapper.findComponent({ ref: 'discoverProjectSecurity' }); - - describe('given a help path only', () => { - beforeEach(() => { - createWrapper(); - }); - - it('does not render a popover', () => { - expect(findPopover().exists()).toBe(false); - }); - - it('renders a help link', () => { - expect(findLink().attributes()).toMatchObject({ - href: helpPath, - target: '_blank', - }); - }); - }); - - describe('given a help path and discover project security path', () => { - beforeEach(() => { - createWrapper({ discoverProjectSecurityPath }); - }); - - it('renders a popover', () => { - const popover = findPopover(); - expect(popover.props('target')()).toBe(findPopoverTarget().element); - expect(popover.attributes()).toMatchObject({ - title: HelpIcon.i18n.upgradeToManageVulnerabilities, - triggers: 'click blur', - }); - expect(popover.text()).toContain(HelpIcon.i18n.upgradeToInteract); - }); - - it('renders a link to the discover path', () => { - expect(findLink().attributes()).toMatchObject({ - href: discoverProjectSecurityPath, - target: '_blank', - }); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/security_reports/security_summary_spec.js b/spec/frontend/vue_shared/components/security_reports/security_summary_spec.js deleted file mode 100644 index 61cdc329220..00000000000 --- a/spec/frontend/vue_shared/components/security_reports/security_summary_spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import { GlSprintf } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import SecuritySummary from '~/vue_shared/security_reports/components/security_summary.vue'; -import { groupedTextBuilder } from '~/vue_shared/security_reports/store/utils'; - -describe('SecuritySummary component', () => { - let wrapper; - - const createWrapper = (message) => { - wrapper = shallowMount(SecuritySummary, { - propsData: { message }, - stubs: { - GlSprintf, - }, - }); - }; - - describe.each([ - { message: '' }, - { message: 'foo' }, - groupedTextBuilder({ reportType: 'Security scanning', critical: 1, high: 0, total: 1 }), - groupedTextBuilder({ reportType: 'Security scanning', critical: 0, high: 1, total: 1 }), - groupedTextBuilder({ reportType: 'Security scanning', critical: 1, high: 2, total: 3 }), - ])('given the message %p', (message) => { - beforeEach(() => { - createWrapper(message); - }); - - it('interpolates correctly', () => { - expect(wrapper.element).toMatchSnapshot(); - }); - }); -}); diff --git a/spec/frontend/vue_shared/security_reports/mock_data.js b/spec/frontend/vue_shared/security_reports/mock_data.js index a9ad675e538..533d312a4de 100644 --- a/spec/frontend/vue_shared/security_reports/mock_data.js +++ b/spec/frontend/vue_shared/security_reports/mock_data.js @@ -341,120 +341,6 @@ export const securityReportMergeRequestDownloadPathsQueryNoArtifactsResponse = { }, }; -export const securityReportMergeRequestDownloadPathsQueryResponse = { - project: { - id: '1', - mergeRequest: { - id: 'mr-1', - headPipeline: { - id: 'gid://gitlab/Ci::Pipeline/176', - jobs: { - nodes: [ - { - id: 'job-1', - 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', - }, - { - id: 'job-2', - 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', - }, - { - id: 'job-3', - 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', - }, - { - id: 'job-4', - name: 'all_artifacts', - artifacts: { - nodes: [ - { - downloadPath: - '/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=archive', - fileType: 'ARCHIVE', - __typename: 'CiJobArtifact', - }, - { - downloadPath: - '/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=trace', - fileType: 'TRACE', - __typename: 'CiJobArtifact', - }, - { - downloadPath: - '/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=metadata', - fileType: 'METADATA', - __typename: 'CiJobArtifact', - }, - ], - __typename: 'CiJobArtifactConnection', - }, - __typename: 'CiJob', - }, - ], - __typename: 'CiJobConnection', - }, - __typename: 'Pipeline', - }, - __typename: 'MergeRequest', - }, - __typename: 'Project', - }, -}; - export const securityReportPipelineDownloadPathsQueryResponse = { project: { id: 'project-1', @@ -566,9 +452,6 @@ export const securityReportPipelineDownloadPathsQueryResponse = { __typename: 'Project', }; -/** - * These correspond to SAST jobs in the securityReportMergeRequestDownloadPathsQueryResponse above. - */ export const sastArtifacts = [ { name: 'bandit-sast', @@ -582,9 +465,6 @@ export const sastArtifacts = [ }, ]; -/** - * These correspond to Secret Detection jobs in the securityReportMergeRequestDownloadPathsQueryResponse above. - */ export const secretDetectionArtifacts = [ { name: 'secret_detection', @@ -594,13 +474,6 @@ export const secretDetectionArtifacts = [ }, ]; -export const expectedDownloadDropdownPropsWithTitle = { - loading: false, - artifacts: [...secretDetectionArtifacts, ...sastArtifacts], - text: '', - title: 'Download results', -}; - export const expectedDownloadDropdownPropsWithText = { loading: false, artifacts: [...secretDetectionArtifacts, ...sastArtifacts], @@ -608,9 +481,6 @@ export const expectedDownloadDropdownPropsWithText = { text: 'Download results', }; -/** - * These correspond to any jobs with zip archives in the securityReportMergeRequestDownloadPathsQueryResponse above. - */ export const archiveArtifacts = [ { name: 'all_artifacts Archive', @@ -619,9 +489,6 @@ export const archiveArtifacts = [ }, ]; -/** - * These correspond to any jobs with trace data in the securityReportMergeRequestDownloadPathsQueryResponse above. - */ export const traceArtifacts = [ { name: 'secret_detection Trace', @@ -645,9 +512,6 @@ export const traceArtifacts = [ }, ]; -/** - * These correspond to any jobs with metadata data in the securityReportMergeRequestDownloadPathsQueryResponse above. - */ export const metadataArtifacts = [ { name: 'all_artifacts Metadata', 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 deleted file mode 100644 index 257f59612e8..00000000000 --- a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js +++ /dev/null @@ -1,267 +0,0 @@ -import { mount } from '@vue/test-utils'; -import MockAdapter from 'axios-mock-adapter'; -import { merge } from 'lodash'; -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import Vuex from 'vuex'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import { trimText } from 'helpers/text_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import { - expectedDownloadDropdownPropsWithText, - securityReportMergeRequestDownloadPathsQueryNoArtifactsResponse, - securityReportMergeRequestDownloadPathsQueryResponse, - sastDiffSuccessMock, - secretDetectionDiffSuccessMock, -} from 'jest/vue_shared/security_reports/mock_data'; -import { createAlert } from '~/alert'; -import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; -import HelpIcon from '~/vue_shared/security_reports/components/help_icon.vue'; -import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue'; -import { - REPORT_TYPE_SAST, - REPORT_TYPE_SECRET_DETECTION, -} from '~/vue_shared/security_reports/constants'; -import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql'; -import SecurityReportsApp from '~/vue_shared/security_reports/security_reports_app.vue'; - -jest.mock('~/alert'); - -Vue.use(VueApollo); -Vue.use(Vuex); - -const SAST_COMPARISON_PATH = '/sast.json'; -const SECRET_DETECTION_COMPARISON_PATH = '/secret_detection.json'; - -describe('Security reports app', () => { - let wrapper; - - const props = { - pipelineId: 123, - projectId: 456, - securityReportsDocsPath: '/docs', - discoverProjectSecurityPath: '/discoverProjectSecurityPath', - }; - - const createComponent = (options) => { - wrapper = mount( - SecurityReportsApp, - merge( - { - propsData: { ...props }, - stubs: { - HelpIcon: true, - }, - }, - options, - ), - ); - }; - - const pendingHandler = () => new Promise(() => {}); - const successHandler = () => - Promise.resolve({ data: securityReportMergeRequestDownloadPathsQueryResponse }); - const successEmptyHandler = () => - Promise.resolve({ data: securityReportMergeRequestDownloadPathsQueryNoArtifactsResponse }); - const failureHandler = () => Promise.resolve({ errors: [{ message: 'some error' }] }); - const createMockApolloProvider = (handler) => { - const requestHandlers = [[securityReportMergeRequestDownloadPathsQuery, handler]]; - - return createMockApollo(requestHandlers); - }; - - const findDownloadDropdown = () => wrapper.findComponent(SecurityReportDownloadDropdown); - const findHelpIconComponent = () => wrapper.findComponent(HelpIcon); - - describe('given the artifacts query is loading', () => { - beforeEach(() => { - createComponent({ - apolloProvider: createMockApolloProvider(pendingHandler), - }); - }); - - // TODO: Remove this assertion as part of - // https://gitlab.com/gitlab-org/gitlab/-/issues/273431 - it('initially renders nothing', () => { - expect(wrapper.html()).toBe(''); - }); - }); - - describe('given the artifacts query loads successfully', () => { - beforeEach(() => { - createComponent({ - apolloProvider: createMockApolloProvider(successHandler), - }); - }); - - it('renders the download dropdown', () => { - expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithText); - }); - - it('renders the expected message', () => { - expect(wrapper.text()).toContain(SecurityReportsApp.i18n.scansHaveRun); - }); - - it('renders a help link', () => { - expect(findHelpIconComponent().props()).toEqual({ - helpPath: props.securityReportsDocsPath, - discoverProjectSecurityPath: props.discoverProjectSecurityPath, - }); - }); - }); - - describe('given the artifacts query loads successfully with no artifacts', () => { - beforeEach(() => { - createComponent({ - apolloProvider: createMockApolloProvider(successEmptyHandler), - }); - }); - - // TODO: Remove this assertion as part of - // https://gitlab.com/gitlab-org/gitlab/-/issues/273431 - it('initially renders nothing', () => { - expect(wrapper.html()).toBe(''); - }); - }); - - describe('given the artifacts query fails', () => { - beforeEach(() => { - createComponent({ - apolloProvider: createMockApolloProvider(failureHandler), - }); - }); - - it('calls createAlert correctly', () => { - expect(createAlert).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.html()).toBe(''); - }); - }); - - describe('given the coreSecurityMrWidgetCounts feature flag is enabled', () => { - let mock; - - const createComponentWithFlagEnabled = (options) => - createComponent( - merge(options, { - provide: { - glFeatures: { - coreSecurityMrWidgetCounts: true, - }, - }, - apolloProvider: createMockApolloProvider(successHandler), - }), - ); - - beforeEach(() => { - mock = new MockAdapter(axios); - }); - - afterEach(() => { - mock.restore(); - }); - - const SAST_SUCCESS_MESSAGE = - 'Security scanning detected 1 potential vulnerability 1 Critical 0 High and 0 Others'; - const SECRET_DETECTION_SUCCESS_MESSAGE = - 'Security scanning detected 2 potential vulnerabilities 1 Critical 1 High and 0 Others'; - describe.each` - reportType | pathProp | path | successResponse | successMessage - ${REPORT_TYPE_SAST} | ${'sastComparisonPath'} | ${SAST_COMPARISON_PATH} | ${sastDiffSuccessMock} | ${SAST_SUCCESS_MESSAGE} - ${REPORT_TYPE_SECRET_DETECTION} | ${'secretDetectionComparisonPath'} | ${SECRET_DETECTION_COMPARISON_PATH} | ${secretDetectionDiffSuccessMock} | ${SECRET_DETECTION_SUCCESS_MESSAGE} - `( - 'given a $pathProp and $reportType artifact', - ({ pathProp, path, successResponse, successMessage }) => { - describe('when loading', () => { - beforeEach(() => { - mock = new MockAdapter(axios, { delayResponse: 1 }); - mock.onGet(path).replyOnce(HTTP_STATUS_OK, successResponse); - - createComponentWithFlagEnabled({ - propsData: { - [pathProp]: path, - }, - }); - - return waitForPromises(); - }); - - it('should have loading message', () => { - expect(wrapper.text()).toContain('Security scanning is loading'); - }); - - it('renders the download dropdown', () => { - expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithText); - }); - }); - - describe('when successfully loaded', () => { - beforeEach(() => { - mock.onGet(path).replyOnce(HTTP_STATUS_OK, successResponse); - - createComponentWithFlagEnabled({ - propsData: { - [pathProp]: path, - }, - }); - - return waitForPromises(); - }); - - it('should show counts', () => { - expect(trimText(wrapper.text())).toContain(successMessage); - }); - - it('renders the download dropdown', () => { - expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithText); - }); - }); - - describe('when an error occurs', () => { - beforeEach(() => { - mock.onGet(path).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR); - - createComponentWithFlagEnabled({ - propsData: { - [pathProp]: path, - }, - }); - - return waitForPromises(); - }); - - it('should show error message', () => { - expect(trimText(wrapper.text())).toContain('Loading resulted in an error'); - }); - - it('renders the download dropdown', () => { - expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithText); - }); - }); - - describe('when the comparison endpoint is not provided', () => { - beforeEach(() => { - mock.onGet(path).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR); - - createComponentWithFlagEnabled(); - - return waitForPromises(); - }); - - it('renders the basic scansHaveRun message', () => { - expect(wrapper.text()).toContain(SecurityReportsApp.i18n.scansHaveRun); - }); - }); - }, - ); - }); -}); diff --git a/spec/frontend/vue_shared/security_reports/store/getters_spec.js b/spec/frontend/vue_shared/security_reports/store/getters_spec.js deleted file mode 100644 index bcc8955ba02..00000000000 --- a/spec/frontend/vue_shared/security_reports/store/getters_spec.js +++ /dev/null @@ -1,182 +0,0 @@ -import { - groupedSummaryText, - allReportsHaveError, - areReportsLoading, - anyReportHasError, - areAllReportsLoading, - anyReportHasIssues, - summaryCounts, -} from '~/vue_shared/security_reports/store/getters'; -import createSastState from '~/vue_shared/security_reports/store/modules/sast/state'; -import createSecretDetectionState from '~/vue_shared/security_reports/store/modules/secret_detection/state'; -import createState from '~/vue_shared/security_reports/store/state'; -import { groupedTextBuilder } from '~/vue_shared/security_reports/store/utils'; -import { CRITICAL, HIGH, LOW } from '~/vulnerabilities/constants'; - -const generateVuln = (severity) => ({ severity }); - -describe('Security reports getters', () => { - let state; - - beforeEach(() => { - state = createState(); - state.sast = createSastState(); - state.secretDetection = createSecretDetectionState(); - }); - - describe('summaryCounts', () => { - it('returns 0 count for empty state', () => { - expect(summaryCounts(state)).toEqual({ - critical: 0, - high: 0, - other: 0, - }); - }); - - describe('combines all reports', () => { - it('of the same severity', () => { - state.sast.newIssues = [generateVuln(CRITICAL)]; - state.secretDetection.newIssues = [generateVuln(CRITICAL)]; - - expect(summaryCounts(state)).toEqual({ - critical: 2, - high: 0, - other: 0, - }); - }); - - it('of different severities', () => { - state.sast.newIssues = [generateVuln(CRITICAL)]; - state.secretDetection.newIssues = [generateVuln(HIGH), generateVuln(LOW)]; - - expect(summaryCounts(state)).toEqual({ - critical: 1, - high: 1, - other: 1, - }); - }); - }); - }); - - describe('groupedSummaryText', () => { - it('returns failed text', () => { - expect( - groupedSummaryText(state, { - allReportsHaveError: true, - areReportsLoading: false, - summaryCounts: {}, - }), - ).toEqual({ message: 'Security scanning failed loading any results' }); - }); - - it('returns `is loading` as status text', () => { - expect( - groupedSummaryText(state, { - allReportsHaveError: false, - areReportsLoading: true, - summaryCounts: {}, - }), - ).toEqual( - groupedTextBuilder({ - reportType: 'Security scanning', - critical: 0, - high: 0, - other: 0, - status: 'is loading', - }), - ); - }); - - it('returns no new status text if there are existing ones', () => { - expect( - groupedSummaryText(state, { - allReportsHaveError: false, - areReportsLoading: false, - summaryCounts: {}, - }), - ).toEqual( - groupedTextBuilder({ - reportType: 'Security scanning', - critical: 0, - high: 0, - other: 0, - status: '', - }), - ); - }); - }); - - describe('areReportsLoading', () => { - it('returns true when any report is loading', () => { - state.sast.isLoading = true; - - expect(areReportsLoading(state)).toEqual(true); - }); - - it('returns false when none of the reports are loading', () => { - expect(areReportsLoading(state)).toEqual(false); - }); - }); - - describe('areAllReportsLoading', () => { - it('returns true when all reports are loading', () => { - state.sast.isLoading = true; - state.secretDetection.isLoading = true; - - expect(areAllReportsLoading(state)).toEqual(true); - }); - - it('returns false when some of the reports are loading', () => { - state.sast.isLoading = true; - - expect(areAllReportsLoading(state)).toEqual(false); - }); - - it('returns false when none of the reports are loading', () => { - expect(areAllReportsLoading(state)).toEqual(false); - }); - }); - - describe('allReportsHaveError', () => { - it('returns true when all reports have error', () => { - state.sast.hasError = true; - state.secretDetection.hasError = true; - - expect(allReportsHaveError(state)).toEqual(true); - }); - - it('returns false when none of the reports have error', () => { - expect(allReportsHaveError(state)).toEqual(false); - }); - - it('returns false when one of the reports does not have error', () => { - state.secretDetection.hasError = true; - - expect(allReportsHaveError(state)).toEqual(false); - }); - }); - - describe('anyReportHasError', () => { - it('returns true when any of the reports has error', () => { - state.sast.hasError = true; - - expect(anyReportHasError(state)).toEqual(true); - }); - - it('returns false when none of the reports has error', () => { - expect(anyReportHasError(state)).toEqual(false); - }); - }); - - describe('anyReportHasIssues', () => { - it('returns true when any of the reports has new issues', () => { - state.sast.newIssues.push(generateVuln(LOW)); - - expect(anyReportHasIssues(state)).toEqual(true); - }); - - it('returns false when none of the reports has error', () => { - expect(anyReportHasIssues(state)).toEqual(false); - }); - }); -}); diff --git a/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js deleted file mode 100644 index 0cab950cb77..00000000000 --- a/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js +++ /dev/null @@ -1,197 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import testAction from 'helpers/vuex_action_helper'; - -import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status'; -import * as actions from '~/vue_shared/security_reports/store/modules/sast/actions'; -import * as types from '~/vue_shared/security_reports/store/modules/sast/mutation_types'; -import createState from '~/vue_shared/security_reports/store/modules/sast/state'; - -const diffEndpoint = 'diff-endpoint.json'; -const blobPath = 'blob-path.json'; -const reports = { - base: 'base', - head: 'head', - enrichData: 'enrichData', - diff: 'diff', -}; -const error = 'Something went wrong'; -const vulnerabilityFeedbackPath = 'vulnerability-feedback-path'; -const rootState = { vulnerabilityFeedbackPath, blobPath }; - -let state; - -describe('sast report actions', () => { - beforeEach(() => { - state = createState(); - }); - - describe('setDiffEndpoint', () => { - it(`should commit ${types.SET_DIFF_ENDPOINT} with the correct path`, () => { - return testAction( - actions.setDiffEndpoint, - diffEndpoint, - state, - [ - { - type: types.SET_DIFF_ENDPOINT, - payload: diffEndpoint, - }, - ], - [], - ); - }); - }); - - describe('requestDiff', () => { - it(`should commit ${types.REQUEST_DIFF}`, () => { - return testAction(actions.requestDiff, {}, state, [{ type: types.REQUEST_DIFF }], []); - }); - }); - - describe('receiveDiffSuccess', () => { - it(`should commit ${types.RECEIVE_DIFF_SUCCESS} with the correct response`, () => { - return testAction( - actions.receiveDiffSuccess, - reports, - state, - [ - { - type: types.RECEIVE_DIFF_SUCCESS, - payload: reports, - }, - ], - [], - ); - }); - }); - - describe('receiveDiffError', () => { - it(`should commit ${types.RECEIVE_DIFF_ERROR} with the correct response`, () => { - return testAction( - actions.receiveDiffError, - error, - state, - [ - { - type: types.RECEIVE_DIFF_ERROR, - payload: error, - }, - ], - [], - ); - }); - }); - - describe('fetchDiff', () => { - let mock; - - beforeEach(() => { - mock = new MockAdapter(axios); - state.paths.diffEndpoint = diffEndpoint; - rootState.canReadVulnerabilityFeedback = true; - }); - - afterEach(() => { - mock.restore(); - }); - - describe('when diff and vulnerability feedback endpoints respond successfully', () => { - beforeEach(() => { - mock - .onGet(diffEndpoint) - .replyOnce(HTTP_STATUS_OK, reports.diff) - .onGet(vulnerabilityFeedbackPath) - .replyOnce(HTTP_STATUS_OK, reports.enrichData); - }); - - it('should dispatch the `receiveDiffSuccess` action', () => { - const { diff, enrichData } = reports; - return testAction( - actions.fetchDiff, - {}, - { ...rootState, ...state }, - [], - [ - { type: 'requestDiff' }, - { - type: 'receiveDiffSuccess', - payload: { - diff, - enrichData, - }, - }, - ], - ); - }); - }); - - describe('when diff endpoint responds successfully and fetching vulnerability feedback is not authorized', () => { - beforeEach(() => { - rootState.canReadVulnerabilityFeedback = false; - mock.onGet(diffEndpoint).replyOnce(HTTP_STATUS_OK, reports.diff); - }); - - it('should dispatch the `receiveDiffSuccess` action with empty enrich data', () => { - const { diff } = reports; - const enrichData = []; - return testAction( - actions.fetchDiff, - {}, - { ...rootState, ...state }, - [], - [ - { type: 'requestDiff' }, - { - type: 'receiveDiffSuccess', - payload: { - diff, - enrichData, - }, - }, - ], - ); - }); - }); - - describe('when the vulnerability feedback endpoint fails', () => { - beforeEach(() => { - mock - .onGet(diffEndpoint) - .replyOnce(HTTP_STATUS_OK, reports.diff) - .onGet(vulnerabilityFeedbackPath) - .replyOnce(HTTP_STATUS_NOT_FOUND); - }); - - it('should dispatch the `receiveError` action', () => { - return testAction( - actions.fetchDiff, - {}, - { ...rootState, ...state }, - [], - [{ type: 'requestDiff' }, { type: 'receiveDiffError' }], - ); - }); - }); - - describe('when the diff endpoint fails', () => { - beforeEach(() => { - mock - .onGet(diffEndpoint) - .replyOnce(HTTP_STATUS_NOT_FOUND) - .onGet(vulnerabilityFeedbackPath) - .replyOnce(HTTP_STATUS_OK, reports.enrichData); - }); - - it('should dispatch the `receiveDiffError` action', () => { - return testAction( - actions.fetchDiff, - {}, - { ...rootState, ...state }, - [], - [{ type: 'requestDiff' }, { type: 'receiveDiffError' }], - ); - }); - }); - }); -}); diff --git a/spec/frontend/vue_shared/security_reports/store/modules/sast/mutations_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/sast/mutations_spec.js deleted file mode 100644 index d6119f44619..00000000000 --- a/spec/frontend/vue_shared/security_reports/store/modules/sast/mutations_spec.js +++ /dev/null @@ -1,84 +0,0 @@ -import * as types from '~/vue_shared/security_reports/store/modules/sast/mutation_types'; -import mutations from '~/vue_shared/security_reports/store/modules/sast/mutations'; -import createState from '~/vue_shared/security_reports/store/modules/sast/state'; - -const createIssue = ({ ...config }) => ({ changed: false, ...config }); - -describe('sast module mutations', () => { - const path = 'path'; - let state; - - beforeEach(() => { - state = createState(); - }); - - describe(types.SET_DIFF_ENDPOINT, () => { - it('should set the SAST diff endpoint', () => { - mutations[types.SET_DIFF_ENDPOINT](state, path); - - expect(state.paths.diffEndpoint).toBe(path); - }); - }); - - describe(types.REQUEST_DIFF, () => { - it('should set the `isLoading` status to `true`', () => { - mutations[types.REQUEST_DIFF](state); - - expect(state.isLoading).toBe(true); - }); - }); - - describe(types.RECEIVE_DIFF_SUCCESS, () => { - beforeEach(() => { - const reports = { - diff: { - added: [ - createIssue({ cve: 'CVE-1' }), - createIssue({ cve: 'CVE-2' }), - createIssue({ cve: 'CVE-3' }), - ], - fixed: [createIssue({ cve: 'CVE-4' }), createIssue({ cve: 'CVE-5' })], - existing: [createIssue({ cve: 'CVE-6' })], - base_report_out_of_date: true, - }, - }; - state.isLoading = true; - mutations[types.RECEIVE_DIFF_SUCCESS](state, reports); - }); - - it('should set the `isLoading` status to `false`', () => { - expect(state.isLoading).toBe(false); - }); - - it('should set the `baseReportOutofDate` status to `false`', () => { - expect(state.baseReportOutofDate).toBe(true); - }); - - it('should have the relevant `new` issues', () => { - expect(state.newIssues).toHaveLength(3); - }); - - it('should have the relevant `resolved` issues', () => { - expect(state.resolvedIssues).toHaveLength(2); - }); - - it('should have the relevant `all` issues', () => { - expect(state.allIssues).toHaveLength(1); - }); - }); - - describe(types.RECEIVE_DIFF_ERROR, () => { - beforeEach(() => { - state.isLoading = true; - mutations[types.RECEIVE_DIFF_ERROR](state); - }); - - it('should set the `isLoading` status to `false`', () => { - expect(state.isLoading).toBe(false); - }); - - it('should set the `hasError` status to `true`', () => { - expect(state.hasError).toBe(true); - }); - }); -}); diff --git a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js deleted file mode 100644 index 7197784c3e8..00000000000 --- a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js +++ /dev/null @@ -1,198 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import testAction from 'helpers/vuex_action_helper'; - -import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status'; -import * as actions from '~/vue_shared/security_reports/store/modules/secret_detection/actions'; -import * as types from '~/vue_shared/security_reports/store/modules/secret_detection/mutation_types'; -import createState from '~/vue_shared/security_reports/store/modules/secret_detection/state'; - -const diffEndpoint = 'diff-endpoint.json'; -const blobPath = 'blob-path.json'; -const reports = { - base: 'base', - head: 'head', - enrichData: 'enrichData', - diff: 'diff', -}; -const error = 'Something went wrong'; -const vulnerabilityFeedbackPath = 'vulnerability-feedback-path'; -const rootState = { vulnerabilityFeedbackPath, blobPath }; - -let state; - -describe('secret detection report actions', () => { - beforeEach(() => { - state = createState(); - }); - - describe('setDiffEndpoint', () => { - it(`should commit ${types.SET_DIFF_ENDPOINT} with the correct path`, () => { - return testAction( - actions.setDiffEndpoint, - diffEndpoint, - state, - [ - { - type: types.SET_DIFF_ENDPOINT, - payload: diffEndpoint, - }, - ], - [], - ); - }); - }); - - describe('requestDiff', () => { - it(`should commit ${types.REQUEST_DIFF}`, () => { - return testAction(actions.requestDiff, {}, state, [{ type: types.REQUEST_DIFF }], []); - }); - }); - - describe('receiveDiffSuccess', () => { - it(`should commit ${types.RECEIVE_DIFF_SUCCESS} with the correct response`, () => { - return testAction( - actions.receiveDiffSuccess, - reports, - state, - [ - { - type: types.RECEIVE_DIFF_SUCCESS, - payload: reports, - }, - ], - [], - ); - }); - }); - - describe('receiveDiffError', () => { - it(`should commit ${types.RECEIVE_DIFF_ERROR} with the correct response`, () => { - return testAction( - actions.receiveDiffError, - error, - state, - [ - { - type: types.RECEIVE_DIFF_ERROR, - payload: error, - }, - ], - [], - ); - }); - }); - - describe('fetchDiff', () => { - let mock; - - beforeEach(() => { - mock = new MockAdapter(axios); - state.paths.diffEndpoint = diffEndpoint; - rootState.canReadVulnerabilityFeedback = true; - }); - - afterEach(() => { - mock.restore(); - }); - - describe('when diff and vulnerability feedback endpoints respond successfully', () => { - beforeEach(() => { - mock - .onGet(diffEndpoint) - .replyOnce(HTTP_STATUS_OK, reports.diff) - .onGet(vulnerabilityFeedbackPath) - .replyOnce(HTTP_STATUS_OK, reports.enrichData); - }); - - it('should dispatch the `receiveDiffSuccess` action', () => { - const { diff, enrichData } = reports; - - return testAction( - actions.fetchDiff, - {}, - { ...rootState, ...state }, - [], - [ - { type: 'requestDiff' }, - { - type: 'receiveDiffSuccess', - payload: { - diff, - enrichData, - }, - }, - ], - ); - }); - }); - - describe('when diff endpoint responds successfully and fetching vulnerability feedback is not authorized', () => { - beforeEach(() => { - rootState.canReadVulnerabilityFeedback = false; - mock.onGet(diffEndpoint).replyOnce(HTTP_STATUS_OK, reports.diff); - }); - - it('should dispatch the `receiveDiffSuccess` action with empty enrich data', () => { - const { diff } = reports; - const enrichData = []; - return testAction( - actions.fetchDiff, - {}, - { ...rootState, ...state }, - [], - [ - { type: 'requestDiff' }, - { - type: 'receiveDiffSuccess', - payload: { - diff, - enrichData, - }, - }, - ], - ); - }); - }); - - describe('when the vulnerability feedback endpoint fails', () => { - beforeEach(() => { - mock - .onGet(diffEndpoint) - .replyOnce(HTTP_STATUS_OK, reports.diff) - .onGet(vulnerabilityFeedbackPath) - .replyOnce(HTTP_STATUS_NOT_FOUND); - }); - - it('should dispatch the `receiveDiffError` action', () => { - return testAction( - actions.fetchDiff, - {}, - { ...rootState, ...state }, - [], - [{ type: 'requestDiff' }, { type: 'receiveDiffError' }], - ); - }); - }); - - describe('when the diff endpoint fails', () => { - beforeEach(() => { - mock - .onGet(diffEndpoint) - .replyOnce(HTTP_STATUS_NOT_FOUND) - .onGet(vulnerabilityFeedbackPath) - .replyOnce(HTTP_STATUS_OK, reports.enrichData); - }); - - it('should dispatch the `receiveDiffError` action', () => { - return testAction( - actions.fetchDiff, - {}, - { ...rootState, ...state }, - [], - [{ type: 'requestDiff' }, { type: 'receiveDiffError' }], - ); - }); - }); - }); -}); diff --git a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/mutations_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/mutations_spec.js deleted file mode 100644 index 42da7476a40..00000000000 --- a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/mutations_spec.js +++ /dev/null @@ -1,84 +0,0 @@ -import * as types from '~/vue_shared/security_reports/store/modules/secret_detection/mutation_types'; -import mutations from '~/vue_shared/security_reports/store/modules/secret_detection/mutations'; -import createState from '~/vue_shared/security_reports/store/modules/secret_detection/state'; - -const createIssue = ({ ...config }) => ({ changed: false, ...config }); - -describe('secret detection module mutations', () => { - const path = 'path'; - let state; - - beforeEach(() => { - state = createState(); - }); - - describe(types.SET_DIFF_ENDPOINT, () => { - it('should set the secret detection diff endpoint', () => { - mutations[types.SET_DIFF_ENDPOINT](state, path); - - expect(state.paths.diffEndpoint).toBe(path); - }); - }); - - describe(types.REQUEST_DIFF, () => { - it('should set the `isLoading` status to `true`', () => { - mutations[types.REQUEST_DIFF](state); - - expect(state.isLoading).toBe(true); - }); - }); - - describe(types.RECEIVE_DIFF_SUCCESS, () => { - beforeEach(() => { - const reports = { - diff: { - added: [ - createIssue({ cve: 'CVE-1' }), - createIssue({ cve: 'CVE-2' }), - createIssue({ cve: 'CVE-3' }), - ], - fixed: [createIssue({ cve: 'CVE-4' }), createIssue({ cve: 'CVE-5' })], - existing: [createIssue({ cve: 'CVE-6' })], - base_report_out_of_date: true, - }, - }; - state.isLoading = true; - mutations[types.RECEIVE_DIFF_SUCCESS](state, reports); - }); - - it('should set the `isLoading` status to `false`', () => { - expect(state.isLoading).toBe(false); - }); - - it('should set the `baseReportOutofDate` status to `true`', () => { - expect(state.baseReportOutofDate).toBe(true); - }); - - it('should have the relevant `new` issues', () => { - expect(state.newIssues).toHaveLength(3); - }); - - it('should have the relevant `resolved` issues', () => { - expect(state.resolvedIssues).toHaveLength(2); - }); - - it('should have the relevant `all` issues', () => { - expect(state.allIssues).toHaveLength(1); - }); - }); - - describe(types.RECEIVE_DIFF_ERROR, () => { - beforeEach(() => { - state.isLoading = true; - mutations[types.RECEIVE_DIFF_ERROR](state); - }); - - it('should set the `isLoading` status to `false`', () => { - expect(state.isLoading).toBe(false); - }); - - it('should set the `hasError` status to `true`', () => { - expect(state.hasError).toBe(true); - }); - }); -}); diff --git a/spec/frontend/vue_shared/security_reports/store/utils_spec.js b/spec/frontend/vue_shared/security_reports/store/utils_spec.js deleted file mode 100644 index c8750cd58a0..00000000000 --- a/spec/frontend/vue_shared/security_reports/store/utils_spec.js +++ /dev/null @@ -1,63 +0,0 @@ -import { enrichVulnerabilityWithFeedback } from '~/vue_shared/security_reports/store/utils'; -import { - FEEDBACK_TYPE_DISMISSAL, - FEEDBACK_TYPE_ISSUE, - FEEDBACK_TYPE_MERGE_REQUEST, -} from '~/vue_shared/security_reports/constants'; - -describe('security reports store utils', () => { - const vulnerability = { uuid: 1 }; - - describe('enrichVulnerabilityWithFeedback', () => { - const dismissalFeedback = { - feedback_type: FEEDBACK_TYPE_DISMISSAL, - finding_uuid: vulnerability.uuid, - }; - const dismissalVuln = { ...vulnerability, isDismissed: true, dismissalFeedback }; - - const issueFeedback = { - feedback_type: FEEDBACK_TYPE_ISSUE, - issue_iid: 1, - finding_uuid: vulnerability.uuid, - }; - const issueVuln = { ...vulnerability, hasIssue: true, issue_feedback: issueFeedback }; - const mrFeedback = { - feedback_type: FEEDBACK_TYPE_MERGE_REQUEST, - merge_request_iid: 1, - finding_uuid: vulnerability.uuid, - }; - const mrVuln = { - ...vulnerability, - hasMergeRequest: true, - merge_request_feedback: mrFeedback, - }; - - it.each` - feedbacks | expected - ${[dismissalFeedback]} | ${dismissalVuln} - ${[{ ...issueFeedback, issue_iid: null }]} | ${vulnerability} - ${[issueFeedback]} | ${issueVuln} - ${[{ ...mrFeedback, merge_request_iid: null }]} | ${vulnerability} - ${[mrFeedback]} | ${mrVuln} - ${[dismissalFeedback, issueFeedback, mrFeedback]} | ${{ ...dismissalVuln, ...issueVuln, ...mrVuln }} - `('returns expected enriched vulnerability: $expected', ({ feedbacks, expected }) => { - const enrichedVulnerability = enrichVulnerabilityWithFeedback(vulnerability, feedbacks); - - expect(enrichedVulnerability).toEqual(expected); - }); - - it('matches correct feedback objects to vulnerability', () => { - const feedbacks = [ - dismissalFeedback, - issueFeedback, - mrFeedback, - { ...dismissalFeedback, finding_uuid: 2 }, - { ...issueFeedback, finding_uuid: 2 }, - { ...mrFeedback, finding_uuid: 2 }, - ]; - const enrichedVulnerability = enrichVulnerabilityWithFeedback(vulnerability, feedbacks); - - expect(enrichedVulnerability).toEqual({ ...dismissalVuln, ...issueVuln, ...mrVuln }); - }); - }); -}); diff --git a/spec/frontend/vue_shared/security_reports/utils_spec.js b/spec/frontend/vue_shared/security_reports/utils_spec.js deleted file mode 100644 index b7129ece698..00000000000 --- a/spec/frontend/vue_shared/security_reports/utils_spec.js +++ /dev/null @@ -1,48 +0,0 @@ -import { - REPORT_TYPE_SAST, - REPORT_TYPE_SECRET_DETECTION, - REPORT_FILE_TYPES, -} from '~/vue_shared/security_reports/constants'; -import { - extractSecurityReportArtifactsFromMergeRequest, - extractSecurityReportArtifactsFromPipeline, -} from '~/vue_shared/security_reports/utils'; -import { - securityReportMergeRequestDownloadPathsQueryResponse, - securityReportPipelineDownloadPathsQueryResponse, - sastArtifacts, - secretDetectionArtifacts, - archiveArtifacts, - traceArtifacts, - metadataArtifacts, -} from './mock_data'; - -describe.each([ - [ - 'extractSecurityReportArtifactsFromMergeRequest', - extractSecurityReportArtifactsFromMergeRequest, - securityReportMergeRequestDownloadPathsQueryResponse, - ], - [ - 'extractSecurityReportArtifactsFromPipelines', - extractSecurityReportArtifactsFromPipeline, - securityReportPipelineDownloadPathsQueryResponse, - ], -])('%s', (funcName, extractFunc, response) => { - it.each` - reportTypes | expectedArtifacts - ${[]} | ${[]} - ${['foo']} | ${[]} - ${[REPORT_TYPE_SAST]} | ${sastArtifacts} - ${[REPORT_TYPE_SECRET_DETECTION]} | ${secretDetectionArtifacts} - ${[REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION]} | ${[...secretDetectionArtifacts, ...sastArtifacts]} - ${[REPORT_FILE_TYPES.ARCHIVE]} | ${archiveArtifacts} - ${[REPORT_FILE_TYPES.TRACE]} | ${traceArtifacts} - ${[REPORT_FILE_TYPES.METADATA]} | ${metadataArtifacts} - `( - 'returns the expected artifacts given report types $reportTypes', - ({ reportTypes, expectedArtifacts }) => { - expect(extractFunc(reportTypes, response)).toEqual(expectedArtifacts); - }, - ); -}); diff --git a/spec/graphql/gitlab_schema_spec.rb b/spec/graphql/gitlab_schema_spec.rb index 2e0711fe18c..885bbc82ecc 100644 --- a/spec/graphql/gitlab_schema_spec.rb +++ b/spec/graphql/gitlab_schema_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe GitlabSchema do - let_it_be(:connections) { GitlabSchema.connections.all_wrappers } + let_it_be(:connections) { described_class.connections.all_wrappers } let_it_be(:tracers) { described_class.tracers } let(:user) { build :user } diff --git a/spec/graphql/graphql_triggers_spec.rb b/spec/graphql/graphql_triggers_spec.rb index 864818351a1..3f58f2678d8 100644 --- a/spec/graphql/graphql_triggers_spec.rb +++ b/spec/graphql/graphql_triggers_spec.rb @@ -20,7 +20,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do issuable ) - GraphqlTriggers.issuable_assignees_updated(issuable) + described_class.issuable_assignees_updated(issuable) end end @@ -32,7 +32,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do issuable ).and_call_original - GraphqlTriggers.issuable_title_updated(issuable) + described_class.issuable_title_updated(issuable) end end @@ -44,7 +44,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do issuable ).and_call_original - GraphqlTriggers.issuable_description_updated(issuable) + described_class.issuable_description_updated(issuable) end end @@ -62,7 +62,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do issuable ) - GraphqlTriggers.issuable_labels_updated(issuable) + described_class.issuable_labels_updated(issuable) end end @@ -74,7 +74,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do issuable ).and_call_original - GraphqlTriggers.issuable_dates_updated(issuable) + described_class.issuable_dates_updated(issuable) end end @@ -86,7 +86,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do issuable ).and_call_original - GraphqlTriggers.issuable_milestone_updated(issuable) + described_class.issuable_milestone_updated(issuable) end end @@ -100,7 +100,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do merge_request ).and_call_original - GraphqlTriggers.merge_request_reviewers_updated(merge_request) + described_class.merge_request_reviewers_updated(merge_request) end end @@ -114,7 +114,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do merge_request ).and_call_original - GraphqlTriggers.merge_request_merge_status_updated(merge_request) + described_class.merge_request_merge_status_updated(merge_request) end end @@ -128,7 +128,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do merge_request ).and_call_original - GraphqlTriggers.merge_request_approval_state_updated(merge_request) + described_class.merge_request_approval_state_updated(merge_request) end end @@ -140,7 +140,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do issuable ).and_call_original - GraphqlTriggers.work_item_updated(issuable) + described_class.work_item_updated(issuable) end context 'when triggered with an Issue' do @@ -154,7 +154,7 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do work_item ).and_call_original - GraphqlTriggers.work_item_updated(issue) + described_class.work_item_updated(issue) end end end diff --git a/spec/graphql/types/global_id_type_spec.rb b/spec/graphql/types/global_id_type_spec.rb index fa0b34113bc..8ce0bc2b70a 100644 --- a/spec/graphql/types/global_id_type_spec.rb +++ b/spec/graphql/types/global_id_type_spec.rb @@ -105,12 +105,12 @@ RSpec.describe Types::GlobalIDType do around do |example| # Unset all previously memoized GlobalIDTypes to allow us to define one # that will use the constants stubbed in the `before` block. - previous_id_types = Types::GlobalIDType.instance_variable_get(:@id_types) - Types::GlobalIDType.instance_variable_set(:@id_types, {}) + previous_id_types = described_class.instance_variable_get(:@id_types) + described_class.instance_variable_set(:@id_types, {}) example.run ensure - Types::GlobalIDType.instance_variable_set(:@id_types, previous_id_types) + described_class.instance_variable_set(:@id_types, previous_id_types) end before do diff --git a/spec/initializers/google_api_client_spec.rb b/spec/initializers/google_api_client_spec.rb index b3c4ac5e23b..cd3e3cf0328 100644 --- a/spec/initializers/google_api_client_spec.rb +++ b/spec/initializers/google_api_client_spec.rb @@ -8,7 +8,7 @@ require 'google/apis/core/base_service' RSpec.describe Google::Apis::Core::HttpCommand do # rubocop:disable RSpec/FilePath context('with a successful response') do let(:client) { Google::Apis::Core::BaseService.new('', '').client } - let(:command) { Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals') } + let(:command) { described_class.new(:get, 'https://www.googleapis.com/zoo/animals') } before do stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(body: %(Hello world)) diff --git a/spec/lib/gitlab/checks/file_size_check/any_oversized_blob_spec.rb b/spec/lib/gitlab/checks/file_size_check/any_oversized_blob_spec.rb new file mode 100644 index 00000000000..bf24d6b63c6 --- /dev/null +++ b/spec/lib/gitlab/checks/file_size_check/any_oversized_blob_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Checks::FileSizeCheck::AnyOversizedBlob, feature_category: :source_code_management do + let_it_be(:project) { create(:project, :public, :repository) } + let(:any_blob) do + described_class.new( + project: project, + changes: [{ newrev: 'bf12d2567099e26f59692896f73ac819bae45b00' }], + file_size_limit_megabytes: 1) + end + + describe '#find!' do + subject { any_blob.find! } + + # SHA of the 2-mb-file branch + let(:newrev) { 'bf12d2567099e26f59692896f73ac819bae45b00' } + let(:timeout) { nil } + + before do + # Delete branch so Repository#new_blobs can return results + project.repository.delete_branch('2-mb-file') + end + + it 'returns the blob exceeding the file size limit' do + blob = subject + expect(blob).to be_kind_of(Gitlab::Git::Blob) + expect(blob.path).to eq('file.bin') + end + end +end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index e5406be0f97..aa7d4b84fb5 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -143,6 +143,7 @@ milestone: - boards - milestone_releases - releases +- user_agent_detail snippets: - author - project diff --git a/spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb b/spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb index cc85c897019..28f6919e6bc 100644 --- a/spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb +++ b/spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb @@ -33,6 +33,18 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder ] end + let_it_be(:ignored_column_model) do + Class.new(ApplicationRecord) do + self.table_name = 'issues' + + include IgnorableColumns + + ignore_column :title, remove_with: '16.4', remove_after: '2023-08-22' + end + end + + let(:scope_model) { Issue } + let(:created_records) { issues } let(:iterator) do Gitlab::Pagination::Keyset::Iterator.new( scope: scope.limit(batch_size), @@ -79,6 +91,55 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder end end + context 'when the scope model has ignored columns' do + let(:scope) { ignored_column_model.order(id: :desc) } + let(:expected_order) { ignored_column_model.where(id: issues.map(&:id)).sort_by(&:id).reverse } + + let(:in_operator_optimization_options) do + { + array_scope: Project.where(namespace_id: top_level_group.self_and_descendants.select(:id)).select(:id), + array_mapping_scope: -> (id_expression) { ignored_column_model.where(ignored_column_model.arel_table[:project_id].eq(id_expression)) }, + finder_query: -> (id_expression) { ignored_column_model.where(ignored_column_model.arel_table[:id].eq(id_expression)) } + } + end + + context 'when iterating records one by one' do + let(:batch_size) { 1 } + + it_behaves_like 'correct ordering examples' + + context 'when scope selects only some columns' do + let(:scope) { ignored_column_model.order(id: :desc).select(:id) } + + it_behaves_like 'correct ordering examples' + end + end + + context 'when iterating records with LIMIT 3' do + let(:batch_size) { 3 } + + it_behaves_like 'correct ordering examples' + + context 'when scope selects only some columns' do + let(:scope) { ignored_column_model.order(id: :desc).select(:id) } + + it_behaves_like 'correct ordering examples' + end + end + + context 'when loading records at once' do + let(:batch_size) { issues.size + 1 } + + it_behaves_like 'correct ordering examples' + + context 'when scope selects only some columns' do + let(:scope) { ignored_column_model.order(id: :desc).select(:id) } + + it_behaves_like 'correct ordering examples' + end + end + end + context 'when ordering by issues.id DESC' do let(:scope) { Issue.order(id: :desc) } let(:expected_order) { issues.sort_by(&:id).reverse } @@ -95,6 +156,14 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder let(:batch_size) { 1 } it_behaves_like 'correct ordering examples' + + context 'when key_set_optimizer_ignored_columns feature flag is disabled' do + before do + stub_feature_flags(key_set_optimizer_ignored_columns: false) + end + + it_behaves_like 'correct ordering examples' + end end context 'when iterating records with LIMIT 3' do @@ -332,7 +401,7 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder end context 'when ordering by JOIN-ed columns' do - let(:scope) { cte_with_issues_and_projects.apply_to(Issue.where({})).reorder(order) } + let(:scope) { cte_with_issues_and_projects.apply_to(Issue.where({}).select(Arel.star)).reorder(order) } let(:cte_with_issues_and_projects) do cte_query = Issue.select('issues.id AS id', 'project_id', 'projects.id AS projects_id', 'projects.name AS projects_name').joins(:project) diff --git a/spec/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy_spec.rb b/spec/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy_spec.rb index 5180403b493..c20f3c96734 100644 --- a/spec/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy_spec.rb +++ b/spec/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy_spec.rb @@ -3,12 +3,12 @@ require 'spec_helper' RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::Strategies::RecordLoaderStrategy do - let(:finder_query) { -> (created_at_value, id_value) { Project.where(Project.arel_table[:id].eq(id_value)) } } + let(:finder_query) { -> (created_at_value, id_value) { model.where(model.arel_table[:id].eq(id_value)) } } let(:model) { Project } let(:keyset_scope) do scope, _ = Gitlab::Pagination::Keyset::SimpleOrderBuilder.build( - Project.order(:created_at, :id) + model.order(:created_at, :id) ) scope @@ -22,6 +22,16 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::Strategies::R Gitlab::Pagination::Keyset::InOperatorOptimization::OrderByColumns.new(keyset_order.column_definitions, model.arel_table) end + let_it_be(:ignored_column_model) do + Class.new(ApplicationRecord) do + self.table_name = 'projects' + + include IgnorableColumns + + ignore_column :name, remove_with: '16.4', remove_after: '2023-08-22' + end + end + subject(:strategy) { described_class.new(finder_query, model, order_by_columns) } describe '#initializer_columns' do @@ -57,4 +67,32 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::Strategies::R expect(strategy.columns).to eq([expected_loader_query.chomp]) end end + + describe '#final_projections' do + context 'when model does not have ignored columns' do + it 'does not specify the selected column names' do + expect(strategy.final_projections).to contain_exactly("(#{described_class::RECORDS_COLUMN}).*") + end + end + + context 'when model has ignored columns' do + let(:model) { ignored_column_model } + + it 'specifies the selected column names' do + expect(strategy.final_projections).to match_array( + model.default_select_columns.map { |column| "(#{described_class::RECORDS_COLUMN}).#{column.name}" } + ) + end + + context 'when the key_set_optimizer_ignored_columns feature flag is disabled' do + before do + stub_feature_flags(key_set_optimizer_ignored_columns: false) + end + + it 'does not specify the selected column names' do + expect(strategy.final_projections).to contain_exactly("(#{described_class::RECORDS_COLUMN}).*") + end + end + end + end end diff --git a/spec/lib/gitlab/pagination/keyset/order_spec.rb b/spec/lib/gitlab/pagination/keyset/order_spec.rb index dda68f6e5ae..a8bdafb1ce8 100644 --- a/spec/lib/gitlab/pagination/keyset/order_spec.rb +++ b/spec/lib/gitlab/pagination/keyset/order_spec.rb @@ -645,6 +645,16 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do let_it_be(:user_2) { create(:user, created_at: five_months_ago) } let_it_be(:user_3) { create(:user, created_at: 1.month.ago) } let_it_be(:user_4) { create(:user, created_at: 2.months.ago) } + let_it_be(:ignored_column_model) do + Class.new(ApplicationRecord) do + self.table_name = 'users' + + include IgnorableColumns + include FromUnion + + ignore_column :username, remove_with: '16.4', remove_after: '2023-08-22' + end + end let(:expected_results) { [user_3, user_4, user_2, user_1] } let(:scope) { User.order(created_at: :desc, id: :desc) } @@ -672,6 +682,36 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do iterator_options[:use_union_optimization] = true end + context 'when the scope model has ignored columns' do + let(:ignored_expected_results) { expected_results.map { |r| r.becomes(ignored_column_model) } } # rubocop:disable Cop/AvoidBecomes + + context 'when scope selects all columns' do + let(:scope) { ignored_column_model.order(created_at: :desc, id: :desc) } + + it 'returns items in the correct order' do + expect(items).to eq(ignored_expected_results) + end + end + + context 'when scope selects only specific columns' do + let(:scope) { ignored_column_model.order(created_at: :desc, id: :desc).select(:id, :created_at) } + + it 'returns items in the correct order' do + expect(items).to eq(ignored_expected_results) + end + end + end + + context 'when key_set_optimizer_ignored_columns feature flag is disabled' do + before do + stub_feature_flags(key_set_optimizer_ignored_columns: false) + end + + it 'returns items in the correct order' do + expect(items).to eq(expected_results) + end + end + it 'returns items in the correct order' do expect(items).to eq(expected_results) end diff --git a/spec/lib/slack/manifest_spec.rb b/spec/lib/slack/manifest_spec.rb new file mode 100644 index 00000000000..f602f05d260 --- /dev/null +++ b/spec/lib/slack/manifest_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Slack::Manifest, feature_category: :integrations do + describe '.to_h' do + it 'creates the correct manifest' do + expect(described_class.to_h).to eq({ + display_information: { + name: "GitLab (#{Gitlab.config.gitlab.host})", + description: s_('SlackIntegration|Interact with GitLab without leaving your Slack workspace!'), + background_color: '#171321', + long_description: "Generated for #{Gitlab.config.gitlab.host} by GitLab #{Gitlab::VERSION}.\r\n\r\n" \ + "- *Notifications:* Get notifications to your team's Slack channel about events " \ + "happening inside your GitLab projects.\r\n\r\n- *Slash commands:* Quickly open, " \ + 'access, or close issues from Slack using the `/gitlab` command. Streamline your ' \ + 'GitLab deployments with ChatOps.' + }, + features: { + app_home: { + home_tab_enabled: true, + messages_tab_enabled: false, + messages_tab_read_only_enabled: true + }, + bot_user: { + display_name: 'GitLab', + always_online: true + }, + slash_commands: [ + { + command: '/gitlab', + url: "#{Gitlab.config.gitlab.url}/api/v4/slack/trigger", + description: 'GitLab slash commands', + usage_hint: 'your-project-name-or-alias command', + should_escape: false + } + ] + }, + oauth_config: { + redirect_urls: [ + Gitlab.config.gitlab.url + ], + scopes: { + bot: %w[ + commands + chat:write + chat:write.public + ] + } + }, + settings: { + event_subscriptions: { + request_url: "#{Gitlab.config.gitlab.url}/api/v4/integrations/slack/events", + bot_events: %w[ + app_home_opened + ] + }, + interactivity: { + is_enabled: true, + request_url: "#{Gitlab.config.gitlab.url}/api/v4/integrations/slack/interactions", + message_menu_options_url: "#{Gitlab.config.gitlab.url}/api/v4/integrations/slack/options" + }, + org_deploy_enabled: false, + socket_mode_enabled: false, + token_rotation_enabled: false + } + }) + end + end + + describe '.to_json' do + subject(:to_json) { described_class.to_json } + + shared_examples 'a manifest that matches the JSON schema' do + it { is_expected.to match_schema('slack/manifest') } + end + + it_behaves_like 'a manifest that matches the JSON schema' + + context 'when the host name is very long' do + before do + allow(Gitlab.config.gitlab).to receive(:host).and_return('abc' * 20) + end + + it_behaves_like 'a manifest that matches the JSON schema' + end + end + + describe '.share_url' do + it 'URI encodes the manifest' do + allow(described_class).to receive(:to_h).and_return({ foo: 'bar' }) + + expect(described_class.share_url).to eq('https://api.slack.com/apps?new_app=1&manifest_json=%7B%22foo%22%3A%22bar%22%7D') + end + end +end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 76771360e1f..629dfdaf55e 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -109,8 +109,14 @@ RSpec.describe Notify do is_expected.to have_body_text issue.description end - it 'does not add a reason header' do - is_expected.not_to have_header('X-GitLab-NotificationReason', /.+/) + context 'when issue is confidential' do + before do + issue.update_attribute(:confidential, true) + end + + it 'has a confidential header set to true' do + is_expected.to have_header('X-GitLab-ConfidentialIssue', 'true') + end end context 'when sent with a reason' do @@ -819,6 +825,10 @@ RSpec.describe Notify do let_it_be(:second_note) { create(:discussion_note_on_issue, in_reply_to: first_note, project: project) } let_it_be(:third_note) { create(:discussion_note_on_issue, in_reply_to: second_note, project: project) } + before_all do + first_note.noteable.update_attribute(:confidential, "true") + end + subject { described_class.note_issue_email(recipient.id, third_note.id) } it_behaves_like 'an email sent to a user' @@ -840,17 +850,29 @@ RSpec.describe Notify do it 'has X-GitLab-Discussion-ID header' do expect(subject.header['X-GitLab-Discussion-ID'].value).to eq(third_note.discussion.id) end + + it 'has a confidential header set to true' do + is_expected.to have_header('X-GitLab-ConfidentialIssue', 'true') + end end context 'individual issue comments' do let_it_be(:note) { create(:note_on_issue, project: project) } + before_all do + note.noteable.update_attribute(:confidential, "true") + end + subject { described_class.note_issue_email(recipient.id, note.id) } it_behaves_like 'an email sent to a user' it_behaves_like 'appearance header and footer enabled' it_behaves_like 'appearance header and footer not enabled' + it 'has a confidential header set to true' do + expect(subject.header['X-GitLab-ConfidentialIssue'].value).to eq('true') + end + it 'has In-Reply-To header pointing to the issue' do expect(subject.header['In-Reply-To'].message_ids).to eq(["issue_#{note.noteable.id}@#{host}"]) end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 9d7744a0ffd..1f0f89fea60 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Milestone do +RSpec.describe Milestone, feature_category: :team_planning do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :public) } let_it_be(:group) { create(:group) } @@ -732,4 +732,44 @@ RSpec.describe Milestone do expect(milestone.lock_version).to be_present end end + + describe '#check_for_spam?' do + let_it_be(:milestone) { build_stubbed(:milestone, project: project) } + + subject { milestone.check_for_spam? } + + context 'when spammable attribute title has changed' do + before do + milestone.title = 'New title' + end + + it { is_expected.to eq(true) } + end + + context 'when spammable attribute description has changed' do + before do + milestone.description = 'New description' + end + + it { is_expected.to eq(true) } + end + + context 'when spammable attribute has changed but parent is private' do + before do + milestone.title = 'New title' + milestone.parent.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PRIVATE) + end + + it { is_expected.to eq(false) } + end + + context 'when no spammable attribute has changed' do + before do + milestone.title = milestone.title_was + milestone.description = milestone.description_was + end + + it { is_expected.to eq(false) } + end + end end diff --git a/spec/rubocop/cop/ignored_columns_spec.rb b/spec/rubocop/cop/ignored_columns_spec.rb index 8d2c6b92c70..c8f47f8aee9 100644 --- a/spec/rubocop/cop/ignored_columns_spec.rb +++ b/spec/rubocop/cop/ignored_columns_spec.rb @@ -4,20 +4,20 @@ require 'rubocop_spec_helper' require_relative '../../../rubocop/cop/ignored_columns' RSpec.describe RuboCop::Cop::IgnoredColumns, feature_category: :database do - it 'flags use of `self.ignored_columns +=` instead of the IgnoredColumns concern' do + it 'flags use of `self.ignored_columns +=` instead of the IgnorableColumns concern' do expect_offense(<<~RUBY) class Foo < ApplicationRecord self.ignored_columns += %i[id] - ^^^^^^^^^^^^^^^ Use `IgnoredColumns` concern instead of adding to `self.ignored_columns`. + ^^^^^^^^^^^^^^^ Use `IgnorableColumns` concern instead of adding to `self.ignored_columns`. end RUBY end - it 'flags use of `self.ignored_columns =` instead of the IgnoredColumns concern' do + it 'flags use of `self.ignored_columns =` instead of the IgnorableColumns concern' do expect_offense(<<~RUBY) class Foo < ApplicationRecord self.ignored_columns = %i[id] - ^^^^^^^^^^^^^^^ Use `IgnoredColumns` concern instead of setting `self.ignored_columns`. + ^^^^^^^^^^^^^^^ Use `IgnorableColumns` concern instead of setting `self.ignored_columns`. end RUBY end diff --git a/spec/serializers/prometheus_alert_entity_spec.rb b/spec/serializers/prometheus_alert_entity_spec.rb deleted file mode 100644 index 02da5a5bb88..00000000000 --- a/spec/serializers/prometheus_alert_entity_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe PrometheusAlertEntity do - let(:user) { build_stubbed(:user) } - let(:prometheus_alert) { build_stubbed(:prometheus_alert) } - let(:request) { double('prometheus_alert', current_user: user) } - let(:entity) { described_class.new(prometheus_alert, request: request) } - - subject { entity.as_json } - - context 'when user can read prometheus alerts' do - before do - prometheus_alert.project.add_maintainer(user) - end - - it 'exposes prometheus_alert attributes' do - expect(subject).to include(:id, :title, :query, :operator, :threshold, :runbook_url) - end - end -end diff --git a/spec/services/milestones/create_service_spec.rb b/spec/services/milestones/create_service_spec.rb index 78cb05532eb..70010d88fbd 100644 --- a/spec/services/milestones/create_service_spec.rb +++ b/spec/services/milestones/create_service_spec.rb @@ -3,24 +3,70 @@ require 'spec_helper' RSpec.describe Milestones::CreateService, feature_category: :team_planning do - let(:project) { create(:project) } - let(:user) { create(:user) } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:params) { { title: 'New Milestone', description: 'Description' } } + + subject(:create_milestone) { described_class.new(project, user, params) } describe '#execute' do - context "valid params" do - before do - project.add_maintainer(user) - - opts = { - title: 'v2.1.9', - description: 'Patch release to fix security issue' - } - - @milestone = described_class.new(project, user, opts).execute + context 'when milestone is saved successfully' do + it 'creates a new milestone' do + expect { create_milestone.execute }.to change { Milestone.count }.by(1) end - it { expect(@milestone).to be_valid } - it { expect(@milestone.title).to eq('v2.1.9') } + it 'opens the milestone if it is a project milestone' do + expect_next_instance_of(EventCreateService) do |instance| + expect(instance).to receive(:open_milestone) + end + + create_milestone.execute + end + + it 'returns the created milestone' do + milestone = create_milestone.execute + expect(milestone).to be_a(Milestone) + expect(milestone.title).to eq('New Milestone') + expect(milestone.description).to eq('Description') + end + end + + context 'when milestone fails to save' do + before do + allow_next_instance_of(Milestone) do |instance| + allow(instance).to receive(:save).and_return(false) + end + end + + it 'does not create a new milestone' do + expect { create_milestone.execute }.not_to change { Milestone.count } + end + + it 'does not open the milestone' do + expect(EventCreateService).not_to receive(:open_milestone) + + create_milestone.execute + end + + it 'returns the unsaved milestone' do + milestone = create_milestone.execute + expect(milestone).to be_a(Milestone) + expect(milestone.title).to eq('New Milestone') + expect(milestone.persisted?).to be_falsey + end + end + + it 'calls before_create method' do + expect(create_milestone).to receive(:before_create) + create_milestone.execute + end + end + + describe '#before_create' do + it 'checks for spam' do + milestone = build(:milestone) + expect(milestone).to receive(:check_for_spam).with(user: user, action: :create) + subject.send(:before_create, milestone) end end end diff --git a/spec/services/milestones/update_service_spec.rb b/spec/services/milestones/update_service_spec.rb index 76110af2514..44de49960d4 100644 --- a/spec/services/milestones/update_service_spec.rb +++ b/spec/services/milestones/update_service_spec.rb @@ -2,40 +2,86 @@ require 'spec_helper' RSpec.describe Milestones::UpdateService, feature_category: :team_planning do - let(:project) { create(:project) } - let(:user) { build(:user) } - let(:milestone) { create(:milestone, project: project) } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:milestone) { create(:milestone, project: project) } + let_it_be(:params) { { title: 'New Title' } } + + subject(:update_milestone) { described_class.new(project, user, params) } describe '#execute' do - context "valid params" do - let(:inner_service) { double(:service) } + context 'when state_event is "activate"' do + let(:params) { { state_event: 'activate' } } + it 'calls Milestones::ReopenService' do + reopen_service = instance_double(Milestones::ReopenService) + expect(Milestones::ReopenService).to receive(:new).with(project, user, {}).and_return(reopen_service) + expect(reopen_service).to receive(:execute).with(milestone) + + update_milestone.execute(milestone) + end + end + + context 'when state_event is "close"' do + let(:params) { { state_event: 'close' } } + + it 'calls Milestones::CloseService' do + close_service = instance_double(Milestones::CloseService) + expect(Milestones::CloseService).to receive(:new).with(project, user, {}).and_return(close_service) + expect(close_service).to receive(:execute).with(milestone) + + update_milestone.execute(milestone) + end + end + + context 'when params are present' do + it 'assigns the params to the milestone' do + expect(milestone).to receive(:assign_attributes).with(params.except(:state_event)) + + update_milestone.execute(milestone) + end + end + + context 'when milestone is changed' do before do - project.add_maintainer(user) + allow(milestone).to receive(:changed?).and_return(true) end - subject { described_class.new(project, user, { title: 'new_title' }).execute(milestone) } + it 'calls before_update' do + expect(update_milestone).to receive(:before_update).with(milestone) - it { expect(subject).to be_valid } - it { expect(subject.title).to eq('new_title') } + update_milestone.execute(milestone) + end + end - context 'state_event is activate' do - it 'calls ReopenService' do - expect(Milestones::ReopenService).to receive(:new).with(project, user, {}).and_return(inner_service) - expect(inner_service).to receive(:execute).with(milestone) - - described_class.new(project, user, { state_event: 'activate' }).execute(milestone) - end + context 'when milestone is not changed' do + before do + allow(milestone).to receive(:changed?).and_return(false) end - context 'state_event is close' do - it 'calls ReopenService' do - expect(Milestones::CloseService).to receive(:new).with(project, user, {}).and_return(inner_service) - expect(inner_service).to receive(:execute).with(milestone) + it 'does not call before_update' do + expect(update_milestone).not_to receive(:before_update) - described_class.new(project, user, { state_event: 'close' }).execute(milestone) - end + update_milestone.execute(milestone) end end + + it 'saves the milestone' do + expect(milestone).to receive(:save) + + update_milestone.execute(milestone) + end + + it 'returns the milestone' do + expect(update_milestone.execute(milestone)).to eq(milestone) + end + end + + describe '#before_update' do + it 'checks for spam' do + expect(milestone).to receive(:check_for_spam).with(user: user, action: :update) + + update_milestone.send(:before_update, milestone) + end end end diff --git a/spec/views/admin/application_settings/_slack.html.haml_spec.rb b/spec/views/admin/application_settings/_slack.html.haml_spec.rb new file mode 100644 index 00000000000..6f89d2b7de4 --- /dev/null +++ b/spec/views/admin/application_settings/_slack.html.haml_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'admin/application_settings/_slack.html.haml', feature_category: :integrations do + let(:app_settings) { build(:application_setting) } + + before do + assign(:application_setting, app_settings) + end + + it 'renders the form correctly', :aggregate_failures do + render + + expect(rendered).to have_field('Client ID', type: 'text') + expect(rendered).to have_field('Client secret', type: 'text') + expect(rendered).to have_field('Signing secret', type: 'text') + expect(rendered).to have_field('Verification token', type: 'text') + expect(rendered).to have_link( + 'Create Slack app', + href: slack_app_manifest_share_admin_application_settings_path + ) + expect(rendered).to have_link( + 'Download latest manifest file', + href: slack_app_manifest_download_admin_application_settings_path + ) + end + + context 'when GitLab.com', :saas do + it 'renders the form correctly', :aggregate_failures do + render + + expect(rendered).to have_field('Client ID', type: 'text') + expect(rendered).to have_field('Client secret', type: 'text') + expect(rendered).to have_field('Signing secret', type: 'text') + expect(rendered).to have_field('Verification token', type: 'text') + + expect(rendered).not_to have_link('Create Slack app') + expect(rendered).not_to have_link('Download latest manifest file') + end + end +end