summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop_todo.yml2
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_table.vue20
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/alert.fragment.graphql6
-rw-r--r--app/assets/javascripts/integrations/edit/components/confirmation_modal.vue2
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form.vue10
-rw-r--r--app/assets/javascripts/pages/search/show/index.js4
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue5
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue18
-rw-r--r--app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue26
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue19
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue1
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue4
-rw-r--r--app/controllers/projects/ci/pipeline_editor_controller.rb1
-rw-r--r--app/finders/merge_requests/oldest_per_commit_finder.rb32
-rw-r--r--app/graphql/resolvers/alert_management/alert_resolver.rb3
-rw-r--r--app/graphql/types/alert_management/alert_type.rb6
-rw-r--r--app/models/bulk_imports/entity.rb10
-rw-r--r--app/models/error_tracking/project_error_tracking_setting.rb10
-rw-r--r--app/models/merge_request.rb10
-rw-r--r--app/services/members/invite_service.rb97
-rw-r--r--app/services/merge_requests/after_create_service.rb7
-rw-r--r--app/services/merge_requests/create_service.rb2
-rw-r--r--app/views/notify/merged_merge_request_email.text.haml1
-rw-r--r--app/workers/error_tracking_issue_link_worker.rb4
-rw-r--r--changelogs/unreleased/228733-alert-issue-status.yml5
-rw-r--r--changelogs/unreleased/291012-preparing-mr-state.yml5
-rw-r--r--changelogs/unreleased/300750-add-missing-reviewers-information-to-merged-merge-request-email.yml5
-rw-r--r--changelogs/unreleased/changelog-include-merge-commits.yml5
-rw-r--r--changelogs/unreleased/move-cancel-btn-integrations.yml5
-rw-r--r--config/feature_flags/development/pipeline_editor_empty_state_action.yml8
-rw-r--r--doc/administration/gitaly/index.md2
-rw-r--r--doc/administration/reference_architectures/troubleshooting.md2
-rw-r--r--doc/administration/troubleshooting/debug.md6
-rw-r--r--doc/api/graphql/reference/index.md3
-rw-r--r--doc/api/merge_request_approvals.md29
-rw-r--r--doc/ci/docker/using_docker_build.md95
-rw-r--r--doc/development/architecture.md2
-rw-r--r--doc/development/internal_api.md10
-rw-r--r--doc/development/maintenance_mode.md2
-rw-r--r--doc/topics/autodevops/upgrading_auto_deploy_dependencies.md2
-rw-r--r--doc/user/clusters/agent/index.md75
-rw-r--r--lib/error_tracking/sentry_client.rb (renamed from lib/sentry/client.rb)18
-rw-r--r--lib/error_tracking/sentry_client/api_urls.rb41
-rw-r--r--lib/error_tracking/sentry_client/event.rb (renamed from lib/sentry/client/event.rb)4
-rw-r--r--lib/error_tracking/sentry_client/issue.rb (renamed from lib/sentry/client/issue.rb)8
-rw-r--r--lib/error_tracking/sentry_client/issue_link.rb (renamed from lib/sentry/client/issue_link.rb)4
-rw-r--r--lib/error_tracking/sentry_client/pagination_parser.rb25
-rw-r--r--lib/error_tracking/sentry_client/projects.rb (renamed from lib/sentry/client/projects.rb)4
-rw-r--r--lib/error_tracking/sentry_client/repo.rb (renamed from lib/sentry/client/repo.rb)4
-rw-r--r--lib/gitlab/error_tracking/detailed_error.rb2
-rw-r--r--lib/gitlab/error_tracking/error.rb2
-rw-r--r--lib/gitlab/error_tracking/error_collection.rb2
-rw-r--r--lib/gitlab/error_tracking/error_event.rb2
-rw-r--r--lib/gitlab/error_tracking/project.rb2
-rw-r--r--lib/gitlab/error_tracking/repo.rb2
-rw-r--r--lib/gitlab/error_tracking/stack_trace_highlight_decorator.rb2
-rw-r--r--lib/gitlab/relative_positioning/closed_range.rb13
-rw-r--r--lib/gitlab/relative_positioning/ending_at.rb18
-rw-r--r--lib/gitlab/relative_positioning/range.rb34
-rw-r--r--lib/gitlab/relative_positioning/starting_from.rb18
-rw-r--r--lib/sentry/api_urls.rb39
-rw-r--r--lib/sentry/pagination_parser.rb23
-rw-r--r--locale/gitlab.pot36
-rw-r--r--spec/finders/merge_requests/oldest_per_commit_finder_spec.rb61
-rw-r--r--spec/frontend/.eslintrc.yml1
-rw-r--r--spec/frontend/__helpers__/fake_date/fixtures.js4
-rw-r--r--spec/frontend/__helpers__/fake_date/index.js1
-rw-r--r--spec/frontend/alert_management/components/alert_management_table_spec.js82
-rw-r--r--spec/frontend/authentication/u2f/authenticate_spec.js2
-rw-r--r--spec/frontend/authentication/u2f/register_spec.js2
-rw-r--r--spec/frontend/authentication/webauthn/authenticate_spec.js1
-rw-r--r--spec/frontend/authentication/webauthn/register_spec.js1
-rw-r--r--spec/frontend/awards_handler_spec.js1
-rw-r--r--spec/frontend/behaviors/quick_submit_spec.js2
-rw-r--r--spec/frontend/behaviors/requires_input_spec.js1
-rw-r--r--spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js2
-rw-r--r--spec/frontend/blob/blob_file_dropzone_spec.js1
-rw-r--r--spec/frontend/blob/sketch/index_spec.js2
-rw-r--r--spec/frontend/blob/viewer/index_spec.js2
-rw-r--r--spec/frontend/bootstrap_linked_tabs_spec.js2
-rw-r--r--spec/frontend/ci_variable_list/ci_variable_list/ci_variable_list_spec.js3
-rw-r--r--spec/frontend/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js2
-rw-r--r--spec/frontend/collapsed_sidebar_todo_spec.js3
-rw-r--r--spec/frontend/commit/pipelines/pipelines_spec.js2
-rw-r--r--spec/frontend/create_item_dropdown_spec.js2
-rw-r--r--spec/frontend/deprecated_jquery_dropdown_spec.js2
-rw-r--r--spec/frontend/diffs/mock_data/diff_with_commit.js2
-rw-r--r--spec/frontend/filtered_search/dropdown_user_spec.js1
-rw-r--r--spec/frontend/filtered_search/dropdown_utils_spec.js1
-rw-r--r--spec/frontend/filtered_search/visual_token_value_spec.js2
-rw-r--r--spec/frontend/gl_field_errors_spec.js2
-rw-r--r--spec/frontend/header_spec.js1
-rw-r--r--spec/frontend/integrations/integration_settings_form_spec.js1
-rw-r--r--spec/frontend/issue_spec.js5
-rw-r--r--spec/frontend/line_highlighter_spec.js1
-rw-r--r--spec/frontend/merge_request_spec.js1
-rw-r--r--spec/frontend/merge_request_tabs_spec.js5
-rw-r--r--spec/frontend/mini_pipeline_graph_dropdown_spec.js2
-rw-r--r--spec/frontend/new_branch_spec.js2
-rw-r--r--spec/frontend/notes/components/diff_discussion_header_spec.js4
-rw-r--r--spec/frontend/notes/components/noteable_discussion_spec.js2
-rw-r--r--spec/frontend/notes/stores/getters_spec.js2
-rw-r--r--spec/frontend/oauth_remember_me_spec.js2
-rw-r--r--spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js2
-rw-r--r--spec/frontend/pages/admin/application_settings/account_and_limits_spec.js1
-rw-r--r--spec/frontend/pages/admin/users/new/index_spec.js2
-rw-r--r--spec/frontend/pages/dashboard/todos/index/todos_spec.js1
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js2
-rw-r--r--spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js2
-rw-r--r--spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js2
-rw-r--r--spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js5
-rw-r--r--spec/frontend/pipeline_editor/components/header/validation_segment_spec.js21
-rw-r--r--spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js69
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_app_spec.js46
-rw-r--r--spec/frontend/pipelines/pipelines_table_row_spec.js2
-rw-r--r--spec/frontend/pipelines/pipelines_table_spec.js2
-rw-r--r--spec/frontend/pipelines_spec.js2
-rw-r--r--spec/frontend/project_select_combo_button_spec.js2
-rw-r--r--spec/frontend/prometheus_metrics/custom_metrics_spec.js1
-rw-r--r--spec/frontend/prometheus_metrics/prometheus_metrics_spec.js1
-rw-r--r--spec/frontend/read_more_spec.js2
-rw-r--r--spec/frontend/right_sidebar_spec.js1
-rw-r--r--spec/frontend/search/highlight_blob_search_result_spec.js2
-rw-r--r--spec/frontend/search_autocomplete_spec.js1
-rw-r--r--spec/frontend/settings_panels_spec.js2
-rw-r--r--spec/frontend/shortcuts_spec.js2
-rw-r--r--spec/frontend/test_setup.js5
-rw-r--r--spec/frontend/user_popovers_spec.js1
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_details_spec.js14
-rw-r--r--spec/frontend/vue_shared/alert_details/mocks/alerts.json2
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js1
-rw-r--r--spec/frontend/zen_mode_spec.js2
-rw-r--r--spec/graphql/types/alert_management/alert_type_spec.rb3
-rw-r--r--spec/lib/error_tracking/sentry_client/api_urls_spec.rb (renamed from spec/lib/sentry/api_urls_spec.rb)4
-rw-r--r--spec/lib/error_tracking/sentry_client/event_spec.rb (renamed from spec/lib/sentry/client/event_spec.rb)2
-rw-r--r--spec/lib/error_tracking/sentry_client/issue_link_spec.rb (renamed from spec/lib/sentry/client/issue_link_spec.rb)2
-rw-r--r--spec/lib/error_tracking/sentry_client/issue_spec.rb (renamed from spec/lib/sentry/client/issue_spec.rb)10
-rw-r--r--spec/lib/error_tracking/sentry_client/pagination_parser_spec.rb (renamed from spec/lib/sentry/pagination_parser_spec.rb)2
-rw-r--r--spec/lib/error_tracking/sentry_client/projects_spec.rb (renamed from spec/lib/sentry/client/projects_spec.rb)6
-rw-r--r--spec/lib/error_tracking/sentry_client/repo_spec.rb (renamed from spec/lib/sentry/client/repo_spec.rb)4
-rw-r--r--spec/lib/error_tracking/sentry_client_spec.rb (renamed from spec/lib/sentry/client_spec.rb)4
-rw-r--r--spec/mailers/emails/merge_requests_spec.rb39
-rw-r--r--spec/mailers/notify_spec.rb31
-rw-r--r--spec/models/bulk_imports/entity_spec.rb16
-rw-r--r--spec/models/error_tracking/project_error_tracking_setting_spec.rb14
-rw-r--r--spec/models/merge_request_spec.rb13
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb6
-rw-r--r--spec/requests/api/graphql/project/alert_management/alert/issue_spec.rb71
-rw-r--r--spec/requests/api/graphql/project/alert_management/alerts_spec.rb4
-rw-r--r--spec/requests/api/invitations_spec.rb12
-rw-r--r--spec/services/members/invite_service_spec.rb173
-rw-r--r--spec/services/merge_requests/after_create_service_spec.rb22
-rw-r--r--spec/services/merge_requests/create_service_spec.rb4
-rw-r--r--spec/services/repositories/changelog_service_spec.rb2
-rw-r--r--spec/support/helpers/javascript_fixtures_helpers.rb1
-rw-r--r--spec/support/shared_contexts/features/error_tracking_shared_context.rb2
-rw-r--r--spec/support/shared_examples/lib/sentry/client_shared_examples.rb4
-rw-r--r--spec/workers/error_tracking_issue_link_worker_spec.rb12
-rw-r--r--[-rwxr-xr-x]vendor/gitignore/C++.gitignore0
-rw-r--r--[-rwxr-xr-x]vendor/gitignore/Java.gitignore0
161 files changed, 1054 insertions, 691 deletions
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index f15256cae0c..f1d3409c614 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -382,7 +382,6 @@ Performance/DeleteSuffix:
- 'app/workers/concerns/application_worker.rb'
- 'ee/app/models/geo/upload_registry.rb'
- 'ee/app/workers/geo/file_download_dispatch_worker/attachment_job_finder.rb'
- - 'lib/sentry/client/issue.rb'
# Offense count: 13
# Cop supports --auto-correct.
@@ -1042,4 +1041,3 @@ Style/StringLiteralsInInterpolation:
# IgnoredMethods: respond_to, define_method
Style/SymbolProc:
Enabled: false
-
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 08f2908ad47..d282ad48073 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-55bed7acf3bb27ab627272d903d99573c5f009e7
+cdb02af5b322de1f4091b39c349579b2e335b914
diff --git a/app/assets/javascripts/alert_management/components/alert_management_table.vue b/app/assets/javascripts/alert_management/components/alert_management_table.vue
index dd702c4a5d3..96279b65452 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_table.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_table.vue
@@ -42,6 +42,7 @@ export default {
"AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear.",
),
unassigned: __('Unassigned'),
+ closed: __('closed'),
},
fields: [
{
@@ -75,7 +76,7 @@ export default {
{
key: 'issue',
label: s__('AlertManagement|Incident'),
- thClass: 'gl-w-12 gl-pointer-events-none',
+ thClass: 'gl-w-15p gl-pointer-events-none',
tdClass,
},
{
@@ -221,8 +222,11 @@ export default {
hasAssignees(assignees) {
return Boolean(assignees.nodes?.length);
},
- getIssueLink(item) {
- return joinPaths('/', this.projectPath, '-', 'issues', item.issueIid);
+ getIssueMeta({ issue: { iid, state } }) {
+ return {
+ state: state === 'closed' ? `(${this.$options.i18n.closed})` : '',
+ link: joinPaths('/', this.projectPath, '-', 'issues', iid),
+ };
},
tbodyTrClass(item) {
return {
@@ -343,8 +347,14 @@ export default {
</template>
<template #cell(issue)="{ item }">
- <gl-link v-if="item.issueIid" data-testid="issueField" :href="getIssueLink(item)">
- #{{ item.issueIid }}
+ <gl-link
+ v-if="item.issue"
+ v-gl-tooltip
+ :title="item.issue.title"
+ data-testid="issueField"
+ :href="getIssueMeta(item).link"
+ >
+ #{{ item.issue.iid }} {{ getIssueMeta(item).state }}
</gl-link>
<div v-else data-testid="issueField">{{ s__('AlertManagement|None') }}</div>
</template>
diff --git a/app/assets/javascripts/graphql_shared/fragments/alert.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/alert.fragment.graphql
index 62119177887..101633ef7a7 100644
--- a/app/assets/javascripts/graphql_shared/fragments/alert.fragment.graphql
+++ b/app/assets/javascripts/graphql_shared/fragments/alert.fragment.graphql
@@ -5,7 +5,11 @@ fragment AlertListItem on AlertManagementAlert {
status
startedAt
eventCount
- issueIid
+ issue {
+ iid
+ state
+ title
+ }
assignees {
nodes {
name
diff --git a/app/assets/javascripts/integrations/edit/components/confirmation_modal.vue b/app/assets/javascripts/integrations/edit/components/confirmation_modal.vue
index bcf4b036fd2..89f7e3b7a89 100644
--- a/app/assets/javascripts/integrations/edit/components/confirmation_modal.vue
+++ b/app/assets/javascripts/integrations/edit/components/confirmation_modal.vue
@@ -13,7 +13,7 @@ export default {
return {
text: __('Save'),
attributes: [
- { variant: 'success' },
+ { variant: 'confirm' },
{ category: 'primary' },
{ disabled: this.isDisabled },
],
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue
index acf6fed0aee..f9551b43c4a 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_form.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue
@@ -138,7 +138,7 @@ export default {
<gl-button
v-gl-modal.confirmSaveIntegration
category="primary"
- variant="success"
+ variant="confirm"
:loading="isSaving"
:disabled="isDisabled"
data-qa-selector="save_changes_button"
@@ -162,6 +162,8 @@ export default {
<gl-button
v-if="propsSource.canTest"
+ category="secondary"
+ variant="confirm"
:loading="isTesting"
:disabled="isDisabled"
:href="propsSource.testPath"
@@ -174,7 +176,7 @@ export default {
<gl-button
v-gl-modal.confirmResetIntegration
category="secondary"
- variant="default"
+ variant="confirm"
:loading="isResetting"
:disabled="isDisabled"
data-testid="reset-button"
@@ -184,9 +186,7 @@ export default {
<reset-confirmation-modal @reset="onResetClick" />
</template>
- <gl-button class="btn-cancel" :href="propsSource.cancelPath">{{
- __('Cancel')
- }}</gl-button>
+ <gl-button :href="propsSource.cancelPath">{{ __('Cancel') }}</gl-button>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/pages/search/show/index.js b/app/assets/javascripts/pages/search/show/index.js
index a8c288c3663..2ee33584ee1 100644
--- a/app/assets/javascripts/pages/search/show/index.js
+++ b/app/assets/javascripts/pages/search/show/index.js
@@ -1,5 +1,3 @@
import { initSearchApp } from '~/search';
-document.addEventListener('DOMContentLoaded', () => {
- initSearchApp();
-});
+initSearchApp();
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue
index 1381cd2f6c3..7a35e31e9ce 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue
@@ -31,6 +31,10 @@ export default {
},
mixins: [glFeatureFlagsMixin()],
props: {
+ ciFileContent: {
+ type: String,
+ required: true,
+ },
ciConfigData: {
type: Object,
required: true,
@@ -60,6 +64,7 @@ export default {
<validation-segment
:class="validationStyling"
:loading="isCiConfigDataLoading"
+ :ci-file-content="ciFileContent"
:ci-config="ciConfigData"
/>
</div>
diff --git a/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue b/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue
index 94fb3a66fdd..541ab74b177 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue
@@ -5,6 +5,9 @@ import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import { CI_CONFIG_STATUS_VALID } from '../../constants';
export const i18n = {
+ empty: __(
+ "We'll continuously validate your pipeline configuration. The validation results will appear here.",
+ ),
learnMore: __('Learn more'),
loading: s__('Pipelines|Validating GitLab CI configuration…'),
invalid: s__('Pipelines|This GitLab CI configuration is invalid.'),
@@ -26,6 +29,10 @@ export default {
},
},
props: {
+ ciFileContent: {
+ type: String,
+ required: true,
+ },
ciConfig: {
type: Object,
required: false,
@@ -38,17 +45,22 @@ export default {
},
},
computed: {
+ isEmpty() {
+ return !this.ciFileContent;
+ },
isValid() {
return this.ciConfig?.status === CI_CONFIG_STATUS_VALID;
},
icon() {
- if (this.isValid) {
+ if (this.isValid || this.isEmpty) {
return 'check';
}
return 'warning-solid';
},
message() {
- if (this.isValid) {
+ if (this.isEmpty) {
+ return this.$options.i18n.empty;
+ } else if (this.isValid) {
return this.$options.i18n.valid;
}
@@ -74,7 +86,7 @@ export default {
<tooltip-on-truncate :title="message" class="gl-text-truncate">
<gl-icon :name="icon" /> <span data-testid="validationMsg">{{ message }}</span>
</tooltip-on-truncate>
- <span class="gl-flex-shrink-0 gl-pl-2">
+ <span v-if="!isEmpty" class="gl-flex-shrink-0 gl-pl-2">
<gl-link data-testid="learnMoreLink" :href="ymlHelpPagePath">
{{ $options.i18n.learnMore }}
</gl-link>
diff --git a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue b/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue
index 2864e69505c..d4f04a0d055 100644
--- a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue
+++ b/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue
@@ -1,9 +1,11 @@
<script>
-import { GlSprintf } from '@gitlab/ui';
+import { GlButton, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
+ GlButton,
GlSprintf,
},
i18n: {
@@ -11,24 +13,44 @@ export default {
body: __(
'Create a new %{codeStart}.gitlab-ci.yml%{codeEnd} file at the root of the repository to get started.',
),
+ btnText: __('Create new CI/CD pipeline'),
},
+ mixins: [glFeatureFlagsMixin()],
inject: {
emptyStateIllustrationPath: {
default: '',
},
},
+ computed: {
+ showCTAButton() {
+ return this.glFeatures.pipelineEditorEmptyStateAction;
+ },
+ },
+ methods: {
+ createEmptyConfigFile() {
+ this.$emit('createEmptyConfigFile');
+ },
+ },
};
</script>
<template>
<div class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-mt-11">
<img :src="emptyStateIllustrationPath" />
<h1 class="gl-font-size-h1">{{ $options.i18n.title }}</h1>
- <p>
+ <p class="gl-mt-3">
<gl-sprintf :message="$options.i18n.body">
<template #code="{ content }">
<code>{{ content }}</code>
</template>
</gl-sprintf>
</p>
+ <gl-button
+ v-if="showCTAButton"
+ variant="confirm"
+ class="gl-mt-3"
+ @click="createEmptyConfigFile"
+ >
+ {{ $options.i18n.btnText }}
+ </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
index f0e8a232f71..0145a377598 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
@@ -36,7 +36,8 @@ export default {
// Success and failure state
failureType: null,
failureReasons: [],
- hasNoCiConfigFile: false,
+ showStartScreen: false,
+ isNewConfigFile: false,
initialCiFileContent: '',
lastCommittedContent: '',
currentCiFileContent: '',
@@ -48,6 +49,11 @@ export default {
apollo: {
initialCiFileContent: {
query: getBlobContent,
+ // If we are working off a new file, we don't want to fetch
+ // the base data as there is nothing to fetch.
+ skip({ isNewConfigFile }) {
+ return isNewConfigFile;
+ },
variables() {
return {
projectPath: this.projectFullPath,
@@ -157,7 +163,7 @@ export default {
response?.status === httpStatusCodes.NOT_FOUND ||
response?.status === httpStatusCodes.BAD_REQUEST
) {
- this.hasNoCiConfigFile = true;
+ this.showStartScreen = true;
} else {
this.reportFailure(LOAD_FAILURE_UNKNOWN);
}
@@ -183,6 +189,10 @@ export default {
resetContent() {
this.currentCiFileContent = this.lastCommittedContent;
},
+ setNewEmptyCiConfigFile() {
+ this.showStartScreen = false;
+ this.isNewConfigFile = true;
+ },
showErrorAlert({ type, reasons = [] }) {
this.reportFailure(type, reasons);
},
@@ -202,7 +212,10 @@ export default {
<template>
<div class="gl-mt-4 gl-relative">
<gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" />
- <pipeline-editor-empty-state v-else-if="hasNoCiConfigFile" />
+ <pipeline-editor-empty-state
+ v-else-if="showStartScreen"
+ @createEmptyConfigFile="setNewEmptyCiConfigFile"
+ />
<div v-else>
<gl-alert v-if="showSuccessAlert" :variant="success.variant" @dismiss="dismissSuccess">
{{ success.text }}
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
index 8c9aad6ed87..ef46040153f 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
@@ -45,6 +45,7 @@ export default {
<template>
<div>
<pipeline-editor-header
+ :ci-file-content="ciFileContent"
:ci-config-data="ciConfigData"
:is-ci-config-data-loading="isCiConfigDataLoading"
/>
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
index fa1cb311b34..920ed5c6b81 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
@@ -268,10 +268,10 @@ export default {
</span>
</div>
<gl-button
- v-if="alert.issueIid"
+ v-if="alert.issue"
class="gl-mt-3 mt-sm-0 align-self-center align-self-sm-baseline alert-details-incident-button"
data-testid="viewIncidentBtn"
- :href="incidentPath(alert.issueIid)"
+ :href="incidentPath(alert.issue.iid)"
category="primary"
variant="success"
>
diff --git a/app/controllers/projects/ci/pipeline_editor_controller.rb b/app/controllers/projects/ci/pipeline_editor_controller.rb
index 4514bf288c5..4d491b33aa0 100644
--- a/app/controllers/projects/ci/pipeline_editor_controller.rb
+++ b/app/controllers/projects/ci/pipeline_editor_controller.rb
@@ -6,6 +6,7 @@ class Projects::Ci::PipelineEditorController < Projects::ApplicationController
push_frontend_feature_flag(:ci_config_visualization_tab, @project, default_enabled: :yaml)
push_frontend_feature_flag(:ci_config_merged_tab, @project, default_enabled: :yaml)
push_frontend_feature_flag(:pipeline_status_for_pipeline_editor, @project, default_enabled: :yaml)
+ push_frontend_feature_flag(:pipeline_editor_empty_state_action, @project, default_enabled: :yaml)
end
feature_category :pipeline_authoring
diff --git a/app/finders/merge_requests/oldest_per_commit_finder.rb b/app/finders/merge_requests/oldest_per_commit_finder.rb
index f50db43d7d2..5360f301036 100644
--- a/app/finders/merge_requests/oldest_per_commit_finder.rb
+++ b/app/finders/merge_requests/oldest_per_commit_finder.rb
@@ -15,19 +15,45 @@ module MergeRequests
# Returns a Hash that maps a commit ID to the oldest merge request that
# introduced that commit.
def execute(commits)
+ mapping = {}
+ shas = commits.map(&:id)
+
+ # To include merge requests by the commit SHA, we don't need to go through
+ # any diff rows.
+ #
+ # We can't squeeze all this into a single query, as the diff based data
+ # relies on a GROUP BY. On the other hand, retrieving MRs by their merge
+ # SHAs separately is much easier, and plenty fast.
+ @project
+ .merge_requests
+ .preload_target_project
+ .by_merge_commit_sha(shas)
+ .each do |mr|
+ # Merge SHAs can't be in the merge request itself. It _is_ possible a
+ # newer merge request includes the merge commit, but in that case we
+ # still want the oldest merge request.
+ mapping[mr.merge_commit_sha] = mr
+ end
+
+ remaining = shas - mapping.keys
+
+ return mapping if remaining.empty?
+
id_rows = MergeRequestDiffCommit
- .oldest_merge_request_id_per_commit(@project.id, commits.map(&:id))
+ .oldest_merge_request_id_per_commit(@project.id, remaining)
mrs = MergeRequest
.preload_target_project
.id_in(id_rows.map { |r| r[:merge_request_id] })
.index_by(&:id)
- id_rows.each_with_object({}) do |row, hash|
+ id_rows.each do |row|
if (mr = mrs[row[:merge_request_id]])
- hash[row[:sha]] = mr
+ mapping[row[:sha]] = mr
end
end
+
+ mapping
end
end
end
diff --git a/app/graphql/resolvers/alert_management/alert_resolver.rb b/app/graphql/resolvers/alert_management/alert_resolver.rb
index d60cabde62b..008641ed88a 100644
--- a/app/graphql/resolvers/alert_management/alert_resolver.rb
+++ b/app/graphql/resolvers/alert_management/alert_resolver.rb
@@ -43,7 +43,8 @@ module Resolvers
def preloads
{
assignees: [:assignees],
- notes: [:ordered_notes, { ordered_notes: [:system_note_metadata, :project, :noteable] }]
+ notes: [:ordered_notes, { ordered_notes: [:system_note_metadata, :project, :noteable] }],
+ issue: [:issue]
}
end
end
diff --git a/app/graphql/types/alert_management/alert_type.rb b/app/graphql/types/alert_management/alert_type.rb
index 6b7e7030c1f..5a2a5c68c8d 100644
--- a/app/graphql/types/alert_management/alert_type.rb
+++ b/app/graphql/types/alert_management/alert_type.rb
@@ -20,8 +20,14 @@ module Types
field :issue_iid,
GraphQL::ID_TYPE,
null: true,
+ deprecated: { reason: 'Use issue field', milestone: '13.10' },
description: 'Internal ID of the GitLab issue attached to the alert.'
+ field :issue,
+ Types::IssueType,
+ null: true,
+ description: 'Issue attached to the alert.'
+
field :title,
GraphQL::STRING_TYPE,
null: true,
diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb
index 37a8e24eeac..9127dab56a6 100644
--- a/app/models/bulk_imports/entity.rb
+++ b/app/models/bulk_imports/entity.rb
@@ -66,16 +66,6 @@ class BulkImports::Entity < ApplicationRecord
event :fail_op do
transition any => :failed
end
-
- after_transition any => [:finished, :failed] do |entity|
- Gitlab::Redis::Cache.with do |redis|
- pattern = "bulk_import:#{entity.bulk_import.id}:entity:#{entity.id}:*"
-
- redis.scan_each(match: pattern).each do |key|
- redis.del(key)
- end
- end
- end
end
def update_tracker_for(relation:, has_next_page:, next_page: nil)
diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb
index fa32c8a5450..9a9fbc6a801 100644
--- a/app/models/error_tracking/project_error_tracking_setting.rb
+++ b/app/models/error_tracking/project_error_tracking_setting.rb
@@ -77,7 +77,7 @@ module ErrorTracking
def sentry_client
strong_memoize(:sentry_client) do
- Sentry::Client.new(api_url, token)
+ ErrorTracking::SentryClient.new(api_url, token)
end
end
@@ -168,13 +168,13 @@ module ErrorTracking
def handle_exceptions
yield
- rescue Sentry::Client::Error => e
+ rescue ErrorTracking::SentryClient::Error => e
{ error: e.message, error_type: SENTRY_API_ERROR_TYPE_NON_20X_RESPONSE }
- rescue Sentry::Client::MissingKeysError => e
+ rescue ErrorTracking::SentryClient::MissingKeysError => e
{ error: e.message, error_type: SENTRY_API_ERROR_TYPE_MISSING_KEYS }
- rescue Sentry::Client::ResponseInvalidSizeError => e
+ rescue ErrorTracking::SentryClient::ResponseInvalidSizeError => e
{ error: e.message, error_type: SENTRY_API_ERROR_INVALID_SIZE }
- rescue Sentry::Client::BadRequestError => e
+ rescue ErrorTracking::SentryClient::BadRequestError => e
{ error: e.message, error_type: SENTRY_API_ERROR_TYPE_BAD_REQUEST }
rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e)
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 290a9af859b..b1f4decf304 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -191,12 +191,8 @@ class MergeRequest < ApplicationRecord
end
state_machine :merge_status, initial: :unchecked do
- event :mark_as_preparing do
- transition unchecked: :preparing
- end
-
event :mark_as_unchecked do
- transition [:preparing, :can_be_merged, :checking] => :unchecked
+ transition [:can_be_merged, :checking] => :unchecked
transition [:cannot_be_merged, :cannot_be_merged_rechecking] => :cannot_be_merged_recheck
end
@@ -241,7 +237,7 @@ class MergeRequest < ApplicationRecord
# Returns current merge_status except it returns `cannot_be_merged_rechecking` as `checking`
# to avoid exposing unnecessary internal state
def public_merge_status
- cannot_be_merged_rechecking? || preparing? ? 'checking' : merge_status
+ cannot_be_merged_rechecking? ? 'checking' : merge_status
end
validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_or_merged_without_fork?]
@@ -1058,8 +1054,6 @@ class MergeRequest < ApplicationRecord
end
def mergeable?(skip_ci_check: false, skip_discussions_check: false)
- return false if preparing?
-
return false unless mergeable_state?(skip_ci_check: skip_ci_check,
skip_discussions_check: skip_discussions_check)
diff --git a/app/services/members/invite_service.rb b/app/services/members/invite_service.rb
index 60ebbaface2..169500d08f0 100644
--- a/app/services/members/invite_service.rb
+++ b/app/services/members/invite_service.rb
@@ -2,112 +2,97 @@
module Members
class InviteService < Members::BaseService
- DEFAULT_LIMIT = 100
+ BlankEmailsError = Class.new(StandardError)
+ TooManyEmailsError = Class.new(StandardError)
- attr_reader :errors
+ def initialize(*args)
+ super
- def initialize(current_user, params)
- @current_user, @params = current_user, params.dup
@errors = {}
+ @emails = params[:email]&.split(',')&.uniq&.flatten
end
def execute(source)
- return error(s_('Email cannot be blank')) if params[:email].blank?
+ validate_emails!
- emails = params[:email].split(',').uniq.flatten
- return error(s_("Too many users specified (limit is %{user_limit})") % { user_limit: user_limit }) if
- user_limit && emails.size > user_limit
-
- emails.each do |email|
- next if existing_member?(source, email)
- next if existing_invite?(source, email)
- next if existing_request?(source, email)
-
- if existing_user?(email)
- add_existing_user_as_member(current_user, source, params, email)
- next
- end
-
- invite_new_member_and_user(current_user, source, params, email)
- end
-
- return success unless errors.any?
-
- error(errors)
+ @source = source
+ emails.each(&method(:process_email))
+ result
+ rescue BlankEmailsError, TooManyEmailsError => e
+ error(e.message)
end
private
- def invite_new_member_and_user(current_user, source, params, email)
- new_member = (source.class.name + 'Member').constantize.create(source_id: source.id,
- user_id: nil,
- access_level: params[:access_level],
- invite_email: email,
- created_by_id: current_user.id,
- expires_at: params[:expires_at])
-
- unless new_member.valid? && new_member.persisted?
- errors[params[:email]] = new_member.errors.full_messages.to_sentence
- end
- end
+ attr_reader :source, :errors, :emails
- def add_existing_user_as_member(current_user, source, params, email)
- new_member = create_member(current_user, existing_user(email), source, params.merge({ invite_email: email }))
+ def validate_emails!
+ raise BlankEmailsError, s_('AddMember|Email cannot be blank') if emails.blank?
- unless new_member.valid? && new_member.persisted?
- errors[email] = new_member.errors.full_messages.to_sentence
+ if user_limit && emails.size > user_limit
+ raise TooManyEmailsError, s_("AddMember|Too many users specified (limit is %{user_limit})") % { user_limit: user_limit }
end
end
- def create_member(current_user, user, source, params)
- source.add_user(user, params[:access_level], current_user: current_user, expires_at: params[:expires_at])
+ def user_limit
+ limit = params.fetch(:limit, Members::CreateService::DEFAULT_LIMIT)
+
+ limit < 0 ? nil : limit
end
- def user_limit
- limit = params.fetch(:limit, DEFAULT_LIMIT)
+ def process_email(email)
+ return if existing_member?(email)
+ return if existing_invite?(email)
+ return if existing_request?(email)
- limit && limit < 0 ? nil : limit
+ add_member(email)
end
- def existing_member?(source, email)
+ def existing_member?(email)
existing_member = source.members.with_user_by_email(email).exists?
if existing_member
- errors[email] = "Already a member of #{source.name}"
+ errors[email] = s_("AddMember|Already a member of %{source_name}") % { source_name: source.name }
return true
end
false
end
- def existing_invite?(source, email)
+ def existing_invite?(email)
existing_invite = source.members.search_invite_email(email).exists?
if existing_invite
- errors[email] = "Member already invited to #{source.name}"
+ errors[email] = s_("AddMember|Member already invited to %{source_name}") % { source_name: source.name }
return true
end
false
end
- def existing_request?(source, email)
+ def existing_request?(email)
existing_request = source.requesters.with_user_by_email(email).exists?
if existing_request
- errors[email] = "Member cannot be invited because they already requested to join #{source.name}"
+ errors[email] = s_("AddMember|Member cannot be invited because they already requested to join %{source_name}") % { source_name: source.name }
return true
end
false
end
- def existing_user(email)
- User.find_by_email(email)
+ def add_member(email)
+ new_member = source.add_user(email, params[:access_level], current_user: current_user, expires_at: params[:expires_at])
+
+ errors[email] = new_member.errors.full_messages.to_sentence if new_member.invalid?
end
- def existing_user?(email)
- existing_user(email).present?
+ def result
+ if errors.any?
+ error(errors)
+ else
+ success
+ end
end
end
end
diff --git a/app/services/merge_requests/after_create_service.rb b/app/services/merge_requests/after_create_service.rb
index 9bbb47b8058..03fcb5a4c1b 100644
--- a/app/services/merge_requests/after_create_service.rb
+++ b/app/services/merge_requests/after_create_service.rb
@@ -3,13 +3,6 @@
module MergeRequests
class AfterCreateService < MergeRequests::BaseService
def execute(merge_request)
- prepare_merge_request(merge_request)
- merge_request.mark_as_unchecked! if merge_request.preparing?
- end
-
- private
-
- def prepare_merge_request(merge_request)
event_service.open_mr(merge_request, current_user)
merge_request_activity_counter.track_create_mr_action(user: current_user)
notification_service.new_merge_request(merge_request, current_user)
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index f4d14aaf7d1..ac84a13f437 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -14,8 +14,6 @@ module MergeRequests
end
def after_create(issuable)
- issuable.mark_as_preparing
-
# Add new items to MergeRequests::AfterCreateService if they can
# be performed in Sidekiq
NewMergeRequestWorker.perform_async(issuable.id, current_user.id)
diff --git a/app/views/notify/merged_merge_request_email.text.haml b/app/views/notify/merged_merge_request_email.text.haml
index 74e6f86f603..a8e07fa8d1c 100644
--- a/app/views/notify/merged_merge_request_email.text.haml
+++ b/app/views/notify/merged_merge_request_email.text.haml
@@ -6,3 +6,4 @@ Merge Request URL: #{project_merge_request_url(@merge_request.target_project, @m
Author: #{sanitize_name(@merge_request.author_name)}
= assignees_label(@merge_request)
+= reviewers_label(@merge_request)
diff --git a/app/workers/error_tracking_issue_link_worker.rb b/app/workers/error_tracking_issue_link_worker.rb
index fa8af4f1822..4ad80d57f6b 100644
--- a/app/workers/error_tracking_issue_link_worker.rb
+++ b/app/workers/error_tracking_issue_link_worker.rb
@@ -26,7 +26,7 @@ class ErrorTrackingIssueLinkWorker # rubocop:disable Scalability/IdempotentWorke
logger.info("Linking Sentry issue #{sentry_issue_id} to GitLab issue #{issue.id}")
sentry_client.create_issue_link(integration_id, sentry_issue_id, issue)
- rescue Sentry::Client::Error => e
+ rescue ErrorTracking::SentryClient::Error => e
logger.info("Failed to link Sentry issue #{sentry_issue_id} to GitLab issue #{issue.id} with error: #{e.message}")
end
end
@@ -63,7 +63,7 @@ class ErrorTrackingIssueLinkWorker # rubocop:disable Scalability/IdempotentWorke
sentry_client
.repos(organization_slug)
.find { |repo| repo.project_id == issue.project_id && repo.status == 'active' }
- rescue Sentry::Client::Error => e
+ rescue ErrorTracking::SentryClient::Error => e
logger.info("Unable to retrieve Sentry repo for organization #{organization_slug}, id #{sentry_issue_id}, with error: #{e.message}")
nil
diff --git a/changelogs/unreleased/228733-alert-issue-status.yml b/changelogs/unreleased/228733-alert-issue-status.yml
new file mode 100644
index 00000000000..265e4955195
--- /dev/null
+++ b/changelogs/unreleased/228733-alert-issue-status.yml
@@ -0,0 +1,5 @@
+---
+title: 'Incident management: add issue state to alerts table'
+merge_request: 55185
+author:
+type: added
diff --git a/changelogs/unreleased/291012-preparing-mr-state.yml b/changelogs/unreleased/291012-preparing-mr-state.yml
deleted file mode 100644
index d52200f6697..00000000000
--- a/changelogs/unreleased/291012-preparing-mr-state.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Implement new preparing internal merge_status
-merge_request: 54900
-author:
-type: other
diff --git a/changelogs/unreleased/300750-add-missing-reviewers-information-to-merged-merge-request-email.yml b/changelogs/unreleased/300750-add-missing-reviewers-information-to-merged-merge-request-email.yml
new file mode 100644
index 00000000000..e0ab1b8153f
--- /dev/null
+++ b/changelogs/unreleased/300750-add-missing-reviewers-information-to-merged-merge-request-email.yml
@@ -0,0 +1,5 @@
+---
+title: Add reviewers detail to merged merge request email
+merge_request: 55589
+author:
+type: added
diff --git a/changelogs/unreleased/changelog-include-merge-commits.yml b/changelogs/unreleased/changelog-include-merge-commits.yml
new file mode 100644
index 00000000000..547806b717f
--- /dev/null
+++ b/changelogs/unreleased/changelog-include-merge-commits.yml
@@ -0,0 +1,5 @@
+---
+title: Include MRs for merge commits for changelogs
+merge_request: 55371
+author:
+type: fixed
diff --git a/changelogs/unreleased/move-cancel-btn-integrations.yml b/changelogs/unreleased/move-cancel-btn-integrations.yml
new file mode 100644
index 00000000000..d69652631f1
--- /dev/null
+++ b/changelogs/unreleased/move-cancel-btn-integrations.yml
@@ -0,0 +1,5 @@
+---
+title: Group integration settings buttons to the left
+merge_request: 55139
+author:
+type: changed
diff --git a/config/feature_flags/development/pipeline_editor_empty_state_action.yml b/config/feature_flags/development/pipeline_editor_empty_state_action.yml
new file mode 100644
index 00000000000..a17f3c0363b
--- /dev/null
+++ b/config/feature_flags/development/pipeline_editor_empty_state_action.yml
@@ -0,0 +1,8 @@
+---
+name: pipeline_editor_empty_state_action
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55414
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323229
+milestone: '13.10'
+type: development
+group: group::pipeline authoring
+default_enabled: false
diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md
index 7bb99bd1018..c442fd6cda2 100644
--- a/doc/administration/gitaly/index.md
+++ b/doc/administration/gitaly/index.md
@@ -1198,7 +1198,7 @@ Confirm the following are all true:
successfully creates the project, but doesn't create the README.
- When [tailing the logs](https://docs.gitlab.com/omnibus/settings/logs.html#tail-logs-in-a-console-on-the-server)
on a Gitaly client and reproducing the error, you get `401` errors
- when reaching the `/api/v4/internal/allowed` endpoint:
+ when reaching the [`/api/v4/internal/allowed`](../../development/internal_api.md) endpoint:
```shell
# api_json.log
diff --git a/doc/administration/reference_architectures/troubleshooting.md b/doc/administration/reference_architectures/troubleshooting.md
index cab45a99ee4..42d9651ca91 100644
--- a/doc/administration/reference_architectures/troubleshooting.md
+++ b/doc/administration/reference_architectures/troubleshooting.md
@@ -301,7 +301,7 @@ Confirm the following are all true:
- Creating a new project and [initializing it with a README](../../user/project/working_with_projects.md#blank-projects)
successfully creates the project but doesn't create the README.
- When [tailing the logs](https://docs.gitlab.com/omnibus/settings/logs.html#tail-logs-in-a-console-on-the-server) on an app node and reproducing the error, you get `401` errors
- when reaching the `/api/v4/internal/allowed` endpoint:
+ when reaching the [`/api/v4/internal/allowed`](../../development/internal_api.md) endpoint:
```shell
# api_json.log
diff --git a/doc/administration/troubleshooting/debug.md b/doc/administration/troubleshooting/debug.md
index a6073e34d58..5a8ee1c5c94 100644
--- a/doc/administration/troubleshooting/debug.md
+++ b/doc/administration/troubleshooting/debug.md
@@ -255,7 +255,7 @@ separate Rails process to debug the issue:
### GitLab: API is not accessible
This often occurs when GitLab Shell attempts to request authorization via the
-internal API (e.g., `http://localhost:8080/api/v4/internal/allowed`), and
+[internal API](../../development/internal_api.md) (e.g., `http://localhost:8080/api/v4/internal/allowed`), and
something in the check fails. There are many reasons why this may happen:
1. Timeout connecting to a database (e.g., PostgreSQL or Redis)
@@ -271,8 +271,8 @@ strace -ttTfyyy -s 1024 -p <PID of unicorn worker> -o /tmp/unicorn.txt
```
If you cannot isolate which Unicorn worker is the issue, try to run `strace`
-on all the Unicorn workers to see where the `/internal/allowed` endpoint gets
-stuck:
+on all the Unicorn workers to see where the
+[`/internal/allowed`](../../development/internal_api.md) endpoint gets stuck:
```shell
ps auwx | grep unicorn | awk '{ print " -p " $2}' | xargs strace -ttTfyyy -s 1024 -o /tmp/unicorn.txt
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index e329da46509..0e9750094d9 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -427,7 +427,8 @@ Describes an alert from the project's Alert Management.
| `eventCount` | Int | Number of events of this alert. |
| `hosts` | String! => Array | List of hosts the alert came from. |
| `iid` | ID! | Internal ID of the alert. |
-| `issueIid` | ID | Internal ID of the GitLab issue attached to the alert. |
+| `issue` | Issue | Issue attached to the alert. |
+| `issueIid` **{warning-solid}** | ID | **Deprecated:** Use issue field. Deprecated in 13.10. |
| `metricsDashboardUrl` | String | URL for metrics embed for the alert. |
| `monitoringTool` | String | Monitoring tool the alert came from. |
| `notes` | NoteConnection! | All notes on this noteable. |
diff --git a/doc/api/merge_request_approvals.md b/doc/api/merge_request_approvals.md
index 86bc56b7149..bf0fed81aec 100644
--- a/doc/api/merge_request_approvals.md
+++ b/doc/api/merge_request_approvals.md
@@ -627,6 +627,35 @@ POST /projects/:id/external_approval_rules
| `external_url` | string | yes | URL of external approval resource |
| `protected_branch_ids` | array<Integer> | no | The ids of protected branches to scope the rule by |
+### Delete external approval rule **(ULTIMATE)**
+
+You can delete an external approval rule for a project using the following endpoint:
+
+```plaintext
+DELETE /projects/:id/external_approval_rules/:rule_id
+```
+
+| Attribute | Type | Required | Description |
+|------------------------|----------------|----------|----------------------------------------------------|
+| `rule_id` | integer | yes | The ID of an approval rule |
+| `id` | integer | yes | The ID of a project |
+
+### Update external approval rule **(ULTIMATE)**
+
+You can update an existing external approval rule for a project using the following endpoint:
+
+```plaintext
+PATCH /projects/:id/external_approval_rules/:rule_id
+```
+
+| Attribute | Type | Required | Description |
+|------------------------|----------------|----------|----------------------------------------------------|
+| `id` | integer | yes | The ID of a project |
+| `rule_id` | integer | yes | The ID of an external approval rule |
+| `name` | string | no | Display name of approval rule |
+| `external_url` | string | no | URL of external approval resource |
+| `protected_branch_ids` | array<Integer> | no | The ids of protected branches to scope the rule by |
+
### Enable or disable External Project-level MR approvals **(ULTIMATE SELF)**
Enable or disable External Project-level MR approvals is under development and not ready for production use. It is
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index a3ee1c7daa0..2091a80bdf2 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -7,32 +7,16 @@ type: concepts, howto
# Use Docker to build Docker images
-You can use GitLab CI/CD with Docker to build and test Docker images.
-
-For example, you might want to:
-
-1. Create a Docker image of your application.
-1. Run tests against the image.
-1. Push the image to a remote registry.
-1. Use the image to deploy your application to a server.
-
-Or, if your application already has a `Dockerfile`, you can
-use it to create and test an image:
-
-```shell
-docker build -t my-image dockerfiles/
-docker run my-image /script/to/run/tests
-docker tag my-image my-registry:5000/my-image
-docker push my-registry:5000/my-image
-```
+You can use GitLab CI/CD with Docker to create Docker images.
+For example, you can create a Docker image of your application,
+test it, and publish it to a container registry.
To run Docker commands in your CI/CD jobs, you must configure
GitLab Runner to support `docker` commands.
## Enable Docker commands in your CI/CD jobs
-There are three ways to enable the use of `docker build` and `docker run`
-during jobs, each with their own tradeoffs. You can use:
+To enable Docker commands for your CI/CD jobs, you can use:
- [The shell executor](#use-the-shell-executor)
- [The Docker executor with the Docker image (Docker-in-Docker)](#use-the-docker-executor-with-the-docker-image-docker-in-docker)
@@ -47,12 +31,9 @@ to learn more about how these runners are configured.
### Use the shell executor
-One way to configure GitLab Runner for `docker` support is to use the
-`shell` executor.
-
-After you register a runner and select the `shell` executor,
-your job scripts are executed as the `gitlab-runner` user.
-This user needs permission to run Docker commands.
+You can include Docker commands in your CI/CD jobs if your runner is configured to
+use the `shell` executor. The `gitlab-runner` user runs the Docker commands, but
+needs permission to run them.
1. [Install](https://gitlab.com/gitlab-org/gitlab-runner/#installation) GitLab Runner.
1. [Register](https://docs.gitlab.com/runner/register/) a runner.
@@ -100,9 +81,11 @@ Learn more about the [security of the `docker` group](https://blog.zopyx.com/on-
### Use the Docker executor with the Docker image (Docker-in-Docker)
-Another way to configure GitLab Runner for `docker` support is to
-register a runner with the Docker executor and use the [Docker image](https://hub.docker.com/_/docker/)
-to run your job scripts. This configuration is referred to as "Docker-in-Docker."
+You can use "Docker-in-Docker" to run commands in your CI/CD jobs:
+
+- Register a runner that uses the Docker executor.
+- Use the [Docker image](https://hub.docker.com/_/docker/) provided by Docker to
+ run the jobs that need Docker commands.
The Docker image has all of the `docker` tools installed
and can run the job script in context of the image in privileged mode.
@@ -111,14 +94,18 @@ The `docker-compose` command is not available in this configuration by default.
To use `docker-compose` in your job scripts, follow the `docker-compose`
[installation instructions](https://docs.docker.com/compose/install/).
+An example project that uses this approach can be found here: <https://gitlab.com/gitlab-examples/docker>.
+
WARNING:
When you enable `--docker-privileged`, you are effectively disabling all of
the security mechanisms of containers and exposing your host to privilege
-escalation which can lead to container breakout. For more information, check
+escalation. Doing this can lead to container breakout. For more information, check
out the official Docker documentation on
[runtime privilege and Linux capabilities](https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities).
-Docker-in-Docker works well, and is the recommended configuration, but it is
+#### Limitations of Docker-in-Docker
+
+Docker-in-Docker is the recommended configuration, but it is
not without its own challenges:
- When using Docker-in-Docker, each job is in a clean environment without the past
@@ -144,8 +131,6 @@ not without its own challenges:
- docker run -v "$MOUNT_POINT:/mnt" my-docker-image
```
-An example project using this approach can be found here: <https://gitlab.com/gitlab-examples/docker>.
-
In the examples below, we are using Docker images tags to specify a
specific version, such as `docker:19.03.12`. If tags like `docker:stable`
are used, you have no control over what version is used. This can lead to
@@ -373,9 +358,8 @@ build:
### Use Docker socket binding
-Another way to configure GitLab Runner for `docker` support is to
-bind-mount `/var/run/docker.sock` into the
-container so that Docker is available in the context of the image.
+To use Docker commands in your CI/CD jobs, you can bind-mount `/var/run/docker.sock` into the
+container. Docker is then available in the context of the image.
NOTE:
If you bind the Docker socket and you are
@@ -478,13 +462,10 @@ services:
> [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/27173) in GitLab Runner 13.6.
-If you are an administrator of GitLab Runner and you have the `dind`
-service defined for the [Docker
-executor](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnersdockerservices-section),
-or the [Kubernetes
-executor](https://docs.gitlab.com/runner/executors/kubernetes.html#using-services)
-you can specify the `command` to configure the registry mirror for the
-Docker daemon.
+If you are a GitLab Runner administrator, you can specify the `command` to configure the registry mirror
+for the Docker daemon. The `dind` service must be defined for the
+[Docker](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnersdockerservices-section)
+or [Kubernetes executor](https://docs.gitlab.com/runner/executors/kubernetes.html#using-services).
Docker:
@@ -516,11 +497,10 @@ Kubernetes:
##### Docker executor inside GitLab Runner configuration
-If you are an administrator of GitLab Runner and you want to use
-the mirror for every `dind` service, update the
+If you are a GitLab Runner administrator, you can use
+the mirror for every `dind` service. Update the
[configuration](https://docs.gitlab.com/runner/configuration/advanced-configuration.html)
-to specify a [volume
-mount](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#volumes-in-the-runnersdocker-section).
+to specify a [volume mount](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#volumes-in-the-runnersdocker-section).
For example, if you have a `/opt/docker/daemon.json` file with the following
content:
@@ -552,11 +532,10 @@ picked up by the `dind` service.
> [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/3223) in GitLab Runner 13.6.
-If you are an administrator of GitLab Runner and you want to use
-the mirror for every `dind` service, update the
+If you are a GitLab Runner administrator, you can use
+the mirror for every `dind` service. Update the
[configuration](https://docs.gitlab.com/runner/configuration/advanced-configuration.html)
-to specify a [ConfigMap volume
-mount](https://docs.gitlab.com/runner/executors/kubernetes.html#using-volumes).
+to specify a [ConfigMap volume mount](https://docs.gitlab.com/runner/executors/kubernetes.html#using-volumes).
For example, if you have a `/tmp/daemon.json` file with the following
content:
@@ -602,7 +581,7 @@ The configuration is picked up by the `dind` service.
When you use Docker-in-Docker, the [normal authentication
methods](using_docker_images.html#define-an-image-from-a-private-container-registry)
-won't work because a fresh Docker daemon is started with the service.
+don't work because a fresh Docker daemon is started with the service.
### Option 1: Run `docker login`
@@ -634,14 +613,14 @@ empty or remove it.
If you are an administrator for GitLab Runner, you can mount a file
with the authentication configuration to `~/.docker/config.json`.
-Then every job that the runner picks up will be authenticated already. If you
+Then every job that the runner picks up is authenticated already. If you
are using the official `docker:19.03.13` image, the home directory is
under `/root`.
If you mount the configuration file, any `docker` command
that modifies the `~/.docker/config.json` (for example, `docker login`)
fails, because the file is mounted as read-only. Do not change it from
-read-only, because other problems will occur.
+read-only, because problems occur.
Here is an example of `/opt/.docker/config.json` that follows the
[`DOCKER_AUTH_CONFIG`](using_docker_images.md#determining-your-docker_auth_config-data)
@@ -743,8 +722,8 @@ build:
When using Docker-in-Docker, Docker downloads all layers of your image every
time you create a build. Recent versions of Docker (Docker 1.13 and above) can
-use a pre-existing image as a cache during the `docker build` step, considerably
-speeding up the build process.
+use a pre-existing image as a cache during the `docker build` step. This considerably
+speeds up the build process.
### How Docker caching works
@@ -754,8 +733,8 @@ any changes. Change in one layer causes all subsequent layers to be recreated.
You can specify a tagged image to be used as a cache source for the `docker build`
command by using the `--cache-from` argument. Multiple images can be specified
-as a cache source by using multiple `--cache-from` arguments. Keep in mind that
-any image that's used with the `--cache-from` argument must first be pulled
+as a cache source by using multiple `--cache-from` arguments. Any image that's used
+with the `--cache-from` argument must first be pulled
(using `docker pull`) before it can be used as a cache source.
### Using Docker caching
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index b577b4e93b7..7754d9f0b3c 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -899,7 +899,7 @@ in Rails, scheduled to run whenever an SSH key is modified by a user.
instead of keys. In this case, `AuthorizedKeysCommand` is replaced with an
`AuthorizedPrincipalsCommand`. This extracts a username from the certificate
without using the Rails internal API, which is used instead of `key_id` in the
-`/api/internal/allowed` call later.
+[`/api/internal/allowed`](internal_api.md) call later.
GitLab Shell also has a few operations that do not involve Gitaly, such as
resetting two-factor authentication codes. These are handled in the same way,
diff --git a/doc/development/internal_api.md b/doc/development/internal_api.md
index 78b59940613..4710ae4606c 100644
--- a/doc/development/internal_api.md
+++ b/doc/development/internal_api.md
@@ -35,12 +35,12 @@ This is called by [Gitaly](https://gitlab.com/gitlab-org/gitaly) and
[GitLab Shell](https://gitlab.com/gitlab-org/gitlab-shell) to check access to a
repository.
-When called from GitLab Shell no changes are passed and the internal
-API replies with the information needed to pass the request on to
-Gitaly.
+- **When called from GitLab Shell**: No changes are passed, and the internal
+ API replies with the information needed to pass the request on to Gitaly.
+- **When called from Gitaly in a `pre-receive` hook**: The changes are passed
+ and validated to determine if the push is allowed.
-When called from Gitaly in a `pre-receive` hook the changes are passed
-and those are validated to determine if the push is allowed.
+Calls are limited to 50 seconds each.
```plaintext
POST /internal/allowed
diff --git a/doc/development/maintenance_mode.md b/doc/development/maintenance_mode.md
index 6b5a6045bb9..f05a731a331 100644
--- a/doc/development/maintenance_mode.md
+++ b/doc/development/maintenance_mode.md
@@ -13,7 +13,7 @@ GitLab Maintenance Mode **only** blocks writes from HTTP and SSH requests at the
- [the read-only database method](https://gitlab.com/gitlab-org/gitlab/blob/2425e9de50c678413ceaad6ee3bf66f42b7e228c/ee/lib/ee/gitlab/database.rb#L13), which toggles special behavior when we are not allowed to write to the database. [Search the codebase for `Gitlab::Database.read_only?`.](https://gitlab.com/search?utf8=%E2%9C%93&search=Gitlab%3A%3ADatabase.read_only%3F&group_id=9970&project_id=278964&scope=blobs&search_code=false&snippets=false&repository_ref=)
- [the read-only middleware](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/ee/gitlab/middleware/read_only/controller.rb), where HTTP requests that cause database writes are blocked, unless explicitly allowed.
-- [Git push access via SSH is denied](https://gitlab.com/gitlab-org/gitlab/-/blob/2425e9de50c678413ceaad6ee3bf66f42b7e228c/ee/lib/ee/gitlab/git_access.rb#L13) by returning 401 when `gitlab-shell` POSTs to `/internal/allowed` to [check if access is allowed](internal_api.md#git-authentication).
+- [Git push access via SSH is denied](https://gitlab.com/gitlab-org/gitlab/-/blob/2425e9de50c678413ceaad6ee3bf66f42b7e228c/ee/lib/ee/gitlab/git_access.rb#L13) by returning 401 when `gitlab-shell` POSTs to [`/internal/allowed`](internal_api.md) to [check if access is allowed](internal_api.md#git-authentication).
- [Container registry authentication service](https://gitlab.com/gitlab-org/gitlab/-/blob/2425e9de50c678413ceaad6ee3bf66f42b7e228c/ee/app/services/ee/auth/container_registry_authentication_service.rb#L12), where updates to the container registry are blocked.
The database itself is not in read-only mode (except in a Geo secondary site) and can be written by sources other than the ones blocked.
diff --git a/doc/topics/autodevops/upgrading_auto_deploy_dependencies.md b/doc/topics/autodevops/upgrading_auto_deploy_dependencies.md
index 1fff935880c..0dabb80204a 100644
--- a/doc/topics/autodevops/upgrading_auto_deploy_dependencies.md
+++ b/doc/topics/autodevops/upgrading_auto_deploy_dependencies.md
@@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
type: reference
---
-# Upgrading deployments for newer Auto Deploy dependencies (Auto Deploy template, auto-deploy-image and auto-deploy-app chart)
+# Upgrading deployments for newer Auto Deploy dependencies
[Auto Deploy](stages.md#auto-deploy) is a feature that deploys your application to a Kubernetes cluster.
It consists of several dependencies:
diff --git a/doc/user/clusters/agent/index.md b/doc/user/clusters/agent/index.md
index 9ead60183de..34249ac8fde 100644
--- a/doc/user/clusters/agent/index.md
+++ b/doc/user/clusters/agent/index.md
@@ -281,7 +281,7 @@ example [`resources.yml` file](#example-resourcesyml-file) in the following ways
after you install the `gitlab-kas` sub-chart, or enable `gitlab-kas` for Omnibus GitLab.
When using the sub-chart, you must set `wss://kas.host.tld:443` as
`kas-address`, where `host.tld` is the domain you've setup for your GitLab installation.
- When using Omnibus GitLab, you must set `wss://GitLab.host.tld:443/-/kubernetes-agent` as
+ When using Omnibus GitLab, you must set `wss://GitLab.host.tld:443/-/kubernetes-agent/` as
`kas-address`, where `GitLab.host.tld` is your GitLab hostname.
- When using the sub-chart, specify the `ws` scheme (such as `ws://kas.host.tld:80`)
to use an unencrypted WebSockets connection.
@@ -346,7 +346,7 @@ spec:
- --token-file=/config/token
- --kas-address
- wss://kas.host.tld:443 # change this line for the one below if using Omnibus GitLab
- # - wss://gitlab.host.tld:443/-/kubernetes-agent
+ # - wss://gitlab.host.tld:443/-/kubernetes-agent/
volumeMounts:
- name: token-volume
mountPath: /config
@@ -569,7 +569,7 @@ This error is shown if there are some connectivity issues between the address
specified as `kas-address`, and your Agent pod. To fix it, make sure that you
specified the `kas-address` correctly.
-### Agent logs - ValidationError(Deployment.metadata
+### Agent logs - ValidationError(Deployment.metadata)
```plaintext
{"level":"info","time":"2020-10-30T08:56:54.329Z","msg":"Synced","project_id":"root/kas-manifest001","resource_key":"apps/Deployment/kas-test001/nginx-deployment","sync_result":"error validating data: [ValidationError(Deployment.metadata): unknown field \"replicas\" in io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta, ValidationError(Deployment.metadata): unknown field \"selector\" in io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta, ValidationError(Deployment.metadata): unknown field \"template\" in io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta]"}
@@ -603,3 +603,72 @@ issue is in progress, directly edit the deployment with the
This error is shown if the version of the agent is newer that the version of KAS.
To fix it, make sure that both `agentk` and KAS use the same versions.
+
+### Agent logs - Certificate signed by unknown authority
+
+```plaintext
+{"level":"error","time":"2021-02-25T07:22:37.158Z","msg":"Reverse tunnel","mod_name":"reverse_tunnel","error":"Connect(): rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing failed to WebSocket dial: failed to send handshake request: Get \\\"https://GitLabhost.tld:443/-/kubernetes-agent/\\\": x509: certificate signed by unknown authority\""}
+```
+
+This error is shown if your GitLab instance is using a certificate signed by an internal CA that
+is unknown to the agent. One approach to fixing it is to present the CA certificate file to the agent
+via a Kubernetes `configmap` and mount the file in the agent `/etc/ssl/certs` directory from where it
+will be picked up automatically.
+
+For example, if your internal CA certifciate is `myCA.pem`:
+
+```plaintext
+kubectl -n gitlab-agent create configmap ca-pemstore --from-file=myCA.pem
+```
+
+Then in `resources.yml`:
+
+```plaintext
+ spec:
+ serviceAccountName: gitlab-agent
+ containers:
+ - name: agent
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/gitlab-agent/agentk:latest"
+ args:
+ - --token-file=/config/token
+ - --kas-address
+ - wss://kas.host.tld:443 # change this line for the one below if using Omnibus GitLab
+ # - wss://gitlab.host.tld:443/-/kubernetes-agent
+ volumeMounts:
+ - name: token-volume
+ mountPath: /config
+ - name: ca-pemstore-volume
+ mountPath: /etc/ssl/certs/myCA.pem
+ subPath: myCA.pem
+ volumes:
+ - name: token-volume
+ secret:
+ secretName: gitlab-agent-token
+ - name: ca-pemstore-volume
+ configMap:
+ name: ca-pemstore
+ items:
+ - key: myCA.pem
+ path: myCA.pem
+```
+
+Alternatively, you can mount the certificate file at a different location and include it using the
+`--ca-cert-file` agent parameter:
+
+```plaintext
+ containers:
+ - name: agent
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/gitlab-agent/agentk:latest"
+ args:
+ - --ca-cert-file=/tmp/myCA.pem
+ - --token-file=/config/token
+ - --kas-address
+ - wss://kas.host.tld:443 # change this line for the one below if using Omnibus GitLab
+ # - wss://gitlab.host.tld:443/-/kubernetes-agent
+ volumeMounts:
+ - name: token-volume
+ mountPath: /config
+ - name: ca-pemstore-volume
+ mountPath: /tmp/myCA.pem
+ subPath: myCA.pem
+```
diff --git a/lib/sentry/client.rb b/lib/error_tracking/sentry_client.rb
index dbf54a65081..68e64fba093 100644
--- a/lib/sentry/client.rb
+++ b/lib/error_tracking/sentry_client.rb
@@ -1,12 +1,12 @@
# frozen_string_literal: true
-module Sentry
- class Client
- include Sentry::Client::Event
- include Sentry::Client::Projects
- include Sentry::Client::Issue
- include Sentry::Client::Repo
- include Sentry::Client::IssueLink
+module ErrorTracking
+ class SentryClient
+ include SentryClient::Event
+ include SentryClient::Projects
+ include SentryClient::Issue
+ include SentryClient::Repo
+ include SentryClient::IssueLink
Error = Class.new(StandardError)
MissingKeysError = Class.new(StandardError)
@@ -21,7 +21,7 @@ module Sentry
private
def api_urls
- @api_urls ||= Sentry::ApiUrls.new(@url)
+ @api_urls ||= SentryClient::ApiUrls.new(@url)
end
def handle_mapping_exceptions(&block)
@@ -94,7 +94,7 @@ module Sentry
end
def raise_error(message)
- raise Client::Error, message
+ raise SentryClient::Error, message
end
end
end
diff --git a/lib/error_tracking/sentry_client/api_urls.rb b/lib/error_tracking/sentry_client/api_urls.rb
new file mode 100644
index 00000000000..387309bfbdb
--- /dev/null
+++ b/lib/error_tracking/sentry_client/api_urls.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class SentryClient
+ class ApiUrls
+ def initialize(url_base)
+ @uri = URI(url_base).freeze
+ end
+
+ def issues_url
+ with_path(File.join(@uri.path, '/issues/'))
+ end
+
+ def issue_url(issue_id)
+ with_path("/api/0/issues/#{escape(issue_id)}/")
+ end
+
+ def projects_url
+ with_path('/api/0/projects/')
+ end
+
+ def issue_latest_event_url(issue_id)
+ with_path("/api/0/issues/#{escape(issue_id)}/events/latest/")
+ end
+
+ private
+
+ def with_path(new_path)
+ new_uri = @uri.dup
+ # Sentry API returns 404 if there are extra slashes in the URL
+ new_uri.path = new_path.squeeze('/')
+
+ new_uri
+ end
+
+ def escape(param)
+ CGI.escape(param.to_s)
+ end
+ end
+ end
+end
diff --git a/lib/sentry/client/event.rb b/lib/error_tracking/sentry_client/event.rb
index 01dfaa25969..93449344d6c 100644
--- a/lib/sentry/client/event.rb
+++ b/lib/error_tracking/sentry_client/event.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-module Sentry
- class Client
+module ErrorTracking
+ class SentryClient
module Event
def issue_latest_event(issue_id:)
latest_event = http_get(api_urls.issue_latest_event_url(issue_id))[:body]
diff --git a/lib/sentry/client/issue.rb b/lib/error_tracking/sentry_client/issue.rb
index f714bda49fd..513fb3daabe 100644
--- a/lib/sentry/client/issue.rb
+++ b/lib/error_tracking/sentry_client/issue.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-module Sentry
- class Client
+module ErrorTracking
+ class SentryClient
module Issue
BadRequestError = Class.new(StandardError)
ResponseInvalidSizeError = Class.new(StandardError)
@@ -49,7 +49,7 @@ module Sentry
{
issues: response[:body],
- pagination: Sentry::PaginationParser.parse(response[:headers])
+ pagination: SentryClient::PaginationParser.parse(response[:headers])
}
end
@@ -113,7 +113,7 @@ module Sentry
uri = URI(url)
uri.path.squeeze!('/')
# Remove trailing slash
- uri = uri.to_s.gsub(/\/\z/, '')
+ uri = uri.to_s.delete_suffix('/')
uri
end
diff --git a/lib/sentry/client/issue_link.rb b/lib/error_tracking/sentry_client/issue_link.rb
index 91498c19f8b..1c2e8c4147a 100644
--- a/lib/sentry/client/issue_link.rb
+++ b/lib/error_tracking/sentry_client/issue_link.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-module Sentry
- class Client
+module ErrorTracking
+ class SentryClient
module IssueLink
# Creates a link in Sentry corresponding to the provided
# Sentry issue and GitLab issue
diff --git a/lib/error_tracking/sentry_client/pagination_parser.rb b/lib/error_tracking/sentry_client/pagination_parser.rb
new file mode 100644
index 00000000000..362a5d098f7
--- /dev/null
+++ b/lib/error_tracking/sentry_client/pagination_parser.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class SentryClient
+ module PaginationParser
+ PATTERN = /rel=\"(?<direction>\w+)\";\sresults=\"(?<results>\w+)\";\scursor=\"(?<cursor>.+)\"/.freeze
+
+ def self.parse(headers)
+ links = headers['link'].to_s.split(',')
+
+ links.map { |link| parse_link(link) }.compact.to_h
+ end
+
+ def self.parse_link(link)
+ match = link.match(PATTERN)
+
+ return unless match
+ return if match['results'] != "true"
+
+ [match['direction'], { 'cursor' => match['cursor'] }]
+ end
+ private_class_method :parse_link
+ end
+ end
+end
diff --git a/lib/sentry/client/projects.rb b/lib/error_tracking/sentry_client/projects.rb
index e686d4ff715..9b8daa226b0 100644
--- a/lib/sentry/client/projects.rb
+++ b/lib/error_tracking/sentry_client/projects.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-module Sentry
- class Client
+module ErrorTracking
+ class SentryClient
module Projects
def projects
projects = get_projects
diff --git a/lib/sentry/client/repo.rb b/lib/error_tracking/sentry_client/repo.rb
index 9a0ed3c7342..3baa7e69be6 100644
--- a/lib/sentry/client/repo.rb
+++ b/lib/error_tracking/sentry_client/repo.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-module Sentry
- class Client
+module ErrorTracking
+ class SentryClient
module Repo
def repos(organization_slug)
repos_url = repos_api_url(organization_slug)
diff --git a/lib/gitlab/error_tracking/detailed_error.rb b/lib/gitlab/error_tracking/detailed_error.rb
index 5d272efa64a..d0b3fc176aa 100644
--- a/lib/gitlab/error_tracking/detailed_error.rb
+++ b/lib/gitlab/error_tracking/detailed_error.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+# This should be in the ErrorTracking namespace. For more details, see:
+# https://gitlab.com/gitlab-org/gitlab/-/issues/323342
module Gitlab
module ErrorTracking
class DetailedError
diff --git a/lib/gitlab/error_tracking/error.rb b/lib/gitlab/error_tracking/error.rb
index 6bfb9dae610..a256f87ec3d 100644
--- a/lib/gitlab/error_tracking/error.rb
+++ b/lib/gitlab/error_tracking/error.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+# This should be in the ErrorTracking namespace. For more details, see:
+# https://gitlab.com/gitlab-org/gitlab/-/issues/323342
module Gitlab
module ErrorTracking
class Error
diff --git a/lib/gitlab/error_tracking/error_collection.rb b/lib/gitlab/error_tracking/error_collection.rb
index 56bcb671363..d01064bb677 100644
--- a/lib/gitlab/error_tracking/error_collection.rb
+++ b/lib/gitlab/error_tracking/error_collection.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+# This should be in the ErrorTracking namespace. For more details, see:
+# https://gitlab.com/gitlab-org/gitlab/-/issues/323342
module Gitlab
module ErrorTracking
class ErrorCollection
diff --git a/lib/gitlab/error_tracking/error_event.rb b/lib/gitlab/error_tracking/error_event.rb
index 015d2c0ead0..d80289f6bc9 100644
--- a/lib/gitlab/error_tracking/error_event.rb
+++ b/lib/gitlab/error_tracking/error_event.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+# This should be in the ErrorTracking namespace. For more details, see:
+# https://gitlab.com/gitlab-org/gitlab/-/issues/323342
module Gitlab
module ErrorTracking
class ErrorEvent
diff --git a/lib/gitlab/error_tracking/project.rb b/lib/gitlab/error_tracking/project.rb
index 93e81da5034..a4ed8831e38 100644
--- a/lib/gitlab/error_tracking/project.rb
+++ b/lib/gitlab/error_tracking/project.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+# This should be in the ErrorTracking namespace. For more details, see:
+# https://gitlab.com/gitlab-org/gitlab/-/issues/323342
module Gitlab
module ErrorTracking
class Project
diff --git a/lib/gitlab/error_tracking/repo.rb b/lib/gitlab/error_tracking/repo.rb
index 50611943bac..e88ac58ff0f 100644
--- a/lib/gitlab/error_tracking/repo.rb
+++ b/lib/gitlab/error_tracking/repo.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+# This should be in the ErrorTracking namespace. For more details, see:
+# https://gitlab.com/gitlab-org/gitlab/-/issues/323342
module Gitlab
module ErrorTracking
class Repo
diff --git a/lib/gitlab/error_tracking/stack_trace_highlight_decorator.rb b/lib/gitlab/error_tracking/stack_trace_highlight_decorator.rb
index 1e490e52c43..24f4c2a2dcf 100644
--- a/lib/gitlab/error_tracking/stack_trace_highlight_decorator.rb
+++ b/lib/gitlab/error_tracking/stack_trace_highlight_decorator.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+# This should be in the ErrorTracking namespace. For more details, see:
+# https://gitlab.com/gitlab-org/gitlab/-/issues/323342
module Gitlab
module ErrorTracking
module StackTraceHighlightDecorator
diff --git a/lib/gitlab/relative_positioning/closed_range.rb b/lib/gitlab/relative_positioning/closed_range.rb
new file mode 100644
index 00000000000..8916d1face5
--- /dev/null
+++ b/lib/gitlab/relative_positioning/closed_range.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module RelativePositioning
+ class ClosedRange < RelativePositioning::Range
+ def initialize(lhs, rhs)
+ @lhs, @rhs = lhs, rhs
+ raise IllegalRange, 'Either lhs or rhs is missing' unless lhs && rhs
+ raise IllegalRange, 'lhs and rhs cannot be the same object' if lhs == rhs
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/relative_positioning/ending_at.rb b/lib/gitlab/relative_positioning/ending_at.rb
new file mode 100644
index 00000000000..61060638ee6
--- /dev/null
+++ b/lib/gitlab/relative_positioning/ending_at.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module RelativePositioning
+ class EndingAt < RelativePositioning::Range
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(rhs)
+ @rhs = rhs
+ raise IllegalRange, 'rhs is required' unless rhs
+ end
+
+ def lhs
+ strong_memoize(:lhs) { rhs.lhs_neighbour }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/relative_positioning/range.rb b/lib/gitlab/relative_positioning/range.rb
index 0b0ccdf5be4..3214c72eb8b 100644
--- a/lib/gitlab/relative_positioning/range.rb
+++ b/lib/gitlab/relative_positioning/range.rb
@@ -31,39 +31,5 @@ module Gitlab
other.is_a?(RelativePositioning::Range) && lhs == other.lhs && rhs == other.rhs
end
end
-
- class ClosedRange < RelativePositioning::Range
- def initialize(lhs, rhs)
- @lhs, @rhs = lhs, rhs
- raise IllegalRange, 'Either lhs or rhs is missing' unless lhs && rhs
- raise IllegalRange, 'lhs and rhs cannot be the same object' if lhs == rhs
- end
- end
-
- class StartingFrom < RelativePositioning::Range
- include Gitlab::Utils::StrongMemoize
-
- def initialize(lhs)
- @lhs = lhs
- raise IllegalRange, 'lhs is required' unless lhs
- end
-
- def rhs
- strong_memoize(:rhs) { lhs.rhs_neighbour }
- end
- end
-
- class EndingAt < RelativePositioning::Range
- include Gitlab::Utils::StrongMemoize
-
- def initialize(rhs)
- @rhs = rhs
- raise IllegalRange, 'rhs is required' unless rhs
- end
-
- def lhs
- strong_memoize(:lhs) { rhs.lhs_neighbour }
- end
- end
end
end
diff --git a/lib/gitlab/relative_positioning/starting_from.rb b/lib/gitlab/relative_positioning/starting_from.rb
new file mode 100644
index 00000000000..6ddd35a39ad
--- /dev/null
+++ b/lib/gitlab/relative_positioning/starting_from.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module RelativePositioning
+ class StartingFrom < RelativePositioning::Range
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(lhs)
+ @lhs = lhs
+ raise IllegalRange, 'lhs is required' unless lhs
+ end
+
+ def rhs
+ strong_memoize(:rhs) { lhs.rhs_neighbour }
+ end
+ end
+ end
+end
diff --git a/lib/sentry/api_urls.rb b/lib/sentry/api_urls.rb
deleted file mode 100644
index 388d0531da1..00000000000
--- a/lib/sentry/api_urls.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-
-module Sentry
- class ApiUrls
- def initialize(url_base)
- @uri = URI(url_base).freeze
- end
-
- def issues_url
- with_path(File.join(@uri.path, '/issues/'))
- end
-
- def issue_url(issue_id)
- with_path("/api/0/issues/#{escape(issue_id)}/")
- end
-
- def projects_url
- with_path('/api/0/projects/')
- end
-
- def issue_latest_event_url(issue_id)
- with_path("/api/0/issues/#{escape(issue_id)}/events/latest/")
- end
-
- private
-
- def with_path(new_path)
- new_uri = @uri.dup
- # Sentry API returns 404 if there are extra slashes in the URL
- new_uri.path = new_path.squeeze('/')
-
- new_uri
- end
-
- def escape(param)
- CGI.escape(param.to_s)
- end
- end
-end
diff --git a/lib/sentry/pagination_parser.rb b/lib/sentry/pagination_parser.rb
deleted file mode 100644
index fa9c1dd8694..00000000000
--- a/lib/sentry/pagination_parser.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-module Sentry
- module PaginationParser
- PATTERN = /rel=\"(?<direction>\w+)\";\sresults=\"(?<results>\w+)\";\scursor=\"(?<cursor>.+)\"/.freeze
-
- def self.parse(headers)
- links = headers['link'].to_s.split(',')
-
- links.map { |link| parse_link(link) }.compact.to_h
- end
-
- def self.parse_link(link)
- match = link.match(PATTERN)
-
- return unless match
- return if match['results'] != "true"
-
- [match['direction'], { 'cursor' => match['cursor'] }]
- end
- private_class_method :parse_link
- end
-end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 7c8f5624147..93d07256f1a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1965,9 +1965,21 @@ msgstr ""
msgid "AddContextCommits|Add/remove"
msgstr ""
+msgid "AddMember|Already a member of %{source_name}"
+msgstr ""
+
+msgid "AddMember|Email cannot be blank"
+msgstr ""
+
msgid "AddMember|Invite limit of %{daily_invites} per day exceeded"
msgstr ""
+msgid "AddMember|Member already invited to %{source_name}"
+msgstr ""
+
+msgid "AddMember|Member cannot be invited because they already requested to join %{source_name}"
+msgstr ""
+
msgid "AddMember|No users specified."
msgstr ""
@@ -8673,6 +8685,9 @@ msgstr ""
msgid "Create new %{name} by email"
msgstr ""
+msgid "Create new CI/CD pipeline"
+msgstr ""
+
msgid "Create new Value Stream"
msgstr ""
@@ -11111,9 +11126,6 @@ msgstr ""
msgid "Email address to use for Support Desk"
msgstr ""
-msgid "Email cannot be blank"
-msgstr ""
-
msgid "Email could not be sent"
msgstr ""
@@ -17368,6 +17380,9 @@ msgstr ""
msgid "Keep editing"
msgstr ""
+msgid "Keeping all SAST analyzers enabled future-proofs the project in case new languages are added later on. Determining which analyzers apply is a process that consumes minimal resources and adds minimal time to the pipeline. Leaving all SAST analyzers enabled ensures maximum coverage."
+msgstr ""
+
msgid "Kerberos access denied"
msgstr ""
@@ -31412,9 +31427,6 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Too many users specified (limit is %{user_limit})"
-msgstr ""
-
msgid "Too much data"
msgstr ""
@@ -33422,6 +33434,9 @@ msgstr ""
msgid "We recommend cloud-based mobile authenticator apps such as Authy, Duo Mobile, and LastPass. They can restore access if you lose your hardware device."
msgstr ""
+msgid "We recommend leaving all SAST analyzers enabled"
+msgstr ""
+
msgid "We recommend that you buy more Pipeline minutes to avoid any interruption of service."
msgstr ""
@@ -33443,6 +33458,9 @@ msgstr ""
msgid "We would like to inform you that your subscription GitLab Enterprise Edition %{plan_name} is nearing its user limit. You have %{active_user_count} active users, which is almost at the user limit of %{maximum_user_count}."
msgstr ""
+msgid "We'll continuously validate your pipeline configuration. The validation results will appear here."
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -34816,6 +34834,9 @@ msgstr ""
msgid "[No reason]"
msgstr ""
+msgid "[Unchanged]"
+msgstr ""
+
msgid "`end_time` should not exceed one month after `start_time`"
msgstr ""
@@ -35189,6 +35210,9 @@ msgstr ""
msgid "ciReport|is loading, errors when loading results"
msgstr ""
+msgid "closed"
+msgstr ""
+
msgid "closed issue"
msgstr ""
diff --git a/spec/finders/merge_requests/oldest_per_commit_finder_spec.rb b/spec/finders/merge_requests/oldest_per_commit_finder_spec.rb
index 4e9d021fa5d..4724a8eb5c7 100644
--- a/spec/finders/merge_requests/oldest_per_commit_finder_spec.rb
+++ b/spec/finders/merge_requests/oldest_per_commit_finder_spec.rb
@@ -6,12 +6,20 @@ RSpec.describe MergeRequests::OldestPerCommitFinder do
describe '#execute' do
it 'returns a Hash mapping commit SHAs to their oldest merge requests' do
project = create(:project)
+ sha1 = Digest::SHA1.hexdigest('foo')
+ sha2 = Digest::SHA1.hexdigest('bar')
+ sha3 = Digest::SHA1.hexdigest('baz')
mr1 = create(:merge_request, :merged, target_project: project)
mr2 = create(:merge_request, :merged, target_project: project)
+ mr3 = create(
+ :merge_request,
+ :merged,
+ target_project: project,
+ merge_commit_sha: sha3
+ )
+
mr1_diff = create(:merge_request_diff, merge_request: mr1)
mr2_diff = create(:merge_request_diff, merge_request: mr2)
- sha1 = Digest::SHA1.hexdigest('foo')
- sha2 = Digest::SHA1.hexdigest('bar')
create(:merge_request_diff_commit, merge_request_diff: mr1_diff, sha: sha1)
create(:merge_request_diff_commit, merge_request_diff: mr2_diff, sha: sha1)
@@ -22,11 +30,16 @@ RSpec.describe MergeRequests::OldestPerCommitFinder do
relative_order: 1
)
- commits = [double(:commit, id: sha1), double(:commit, id: sha2)]
+ commits = [
+ double(:commit, id: sha1),
+ double(:commit, id: sha2),
+ double(:commit, id: sha3)
+ ]
expect(described_class.new(project).execute(commits)).to eq(
sha1 => mr1,
- sha2 => mr2
+ sha2 => mr2,
+ sha3 => mr3
)
end
@@ -42,5 +55,45 @@ RSpec.describe MergeRequests::OldestPerCommitFinder do
expect(described_class.new(mr.target_project).execute(commits))
.to be_empty
end
+
+ it 'includes the merge request for a merge commit' do
+ project = create(:project)
+ sha = Digest::SHA1.hexdigest('foo')
+ mr = create(
+ :merge_request,
+ :merged,
+ target_project: project,
+ merge_commit_sha: sha
+ )
+
+ commits = [double(:commit, id: sha)]
+
+ # This expectation is set so we're certain that the merge commit SHAs (if
+ # a matching merge request is found) aren't also used for finding MRs
+ # according to diffs.
+ expect(MergeRequestDiffCommit)
+ .not_to receive(:oldest_merge_request_id_per_commit)
+
+ expect(described_class.new(project).execute(commits)).to eq(sha => mr)
+ end
+
+ it 'includes the oldest merge request when a merge commit is present in a newer merge request' do
+ project = create(:project)
+ sha = Digest::SHA1.hexdigest('foo')
+ mr1 = create(
+ :merge_request,
+ :merged,
+ target_project: project, merge_commit_sha: sha
+ )
+
+ mr2 = create(:merge_request, :merged, target_project: project)
+ mr_diff = create(:merge_request_diff, merge_request: mr2)
+
+ create(:merge_request_diff_commit, merge_request_diff: mr_diff, sha: sha)
+
+ commits = [double(:commit, id: sha)]
+
+ expect(described_class.new(project).execute(commits)).to eq(sha => mr1)
+ end
end
end
diff --git a/spec/frontend/.eslintrc.yml b/spec/frontend/.eslintrc.yml
index d0e585e844a..145e6c8961a 100644
--- a/spec/frontend/.eslintrc.yml
+++ b/spec/frontend/.eslintrc.yml
@@ -14,7 +14,6 @@ settings:
globals:
getJSONFixture: false
loadFixtures: false
- preloadFixtures: false
setFixtures: false
rules:
jest/expect-expect:
diff --git a/spec/frontend/__helpers__/fake_date/fixtures.js b/spec/frontend/__helpers__/fake_date/fixtures.js
new file mode 100644
index 00000000000..fcf9d4a9c64
--- /dev/null
+++ b/spec/frontend/__helpers__/fake_date/fixtures.js
@@ -0,0 +1,4 @@
+import { useFakeDate } from './jest';
+
+// Also see spec/support/helpers/javascript_fixtures_helpers.rb
+export const useFixturesFakeDate = () => useFakeDate(2015, 6, 3, 10);
diff --git a/spec/frontend/__helpers__/fake_date/index.js b/spec/frontend/__helpers__/fake_date/index.js
index 3d1b124ce79..9d00349bd26 100644
--- a/spec/frontend/__helpers__/fake_date/index.js
+++ b/spec/frontend/__helpers__/fake_date/index.js
@@ -1,2 +1,3 @@
export * from './fake_date';
export * from './jest';
+export * from './fixtures';
diff --git a/spec/frontend/alert_management/components/alert_management_table_spec.js b/spec/frontend/alert_management/components/alert_management_table_spec.js
index cea665aa50d..233585735b6 100644
--- a/spec/frontend/alert_management/components/alert_management_table_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_table_spec.js
@@ -2,6 +2,8 @@ import { GlTable, GlAlert, GlLoadingIcon, GlDropdown, GlIcon, GlAvatar } from '@
import { mount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import mockAlerts from 'jest/vue_shared/alert_details/mocks/alerts.json';
import AlertManagementTable from '~/alert_management/components/alert_management_table.vue';
import { visitUrl } from '~/lib/utils/url_utility';
@@ -18,19 +20,18 @@ describe('AlertManagementTable', () => {
let wrapper;
let mock;
- const findAlertsTable = () => wrapper.find(GlTable);
+ const findAlertsTable = () => wrapper.findComponent(GlTable);
const findAlerts = () => wrapper.findAll('table tbody tr');
- const findAlert = () => wrapper.find(GlAlert);
- const findLoader = () => wrapper.find(GlLoadingIcon);
- const findStatusDropdown = () => wrapper.find(GlDropdown);
- const findDateFields = () => wrapper.findAll(TimeAgo);
- const findSearch = () => wrapper.find(FilteredSearchBar);
- const findSeverityColumnHeader = () =>
- wrapper.find('[data-testid="alert-management-severity-sort"]');
- const findFirstIDField = () => wrapper.findAll('[data-testid="idField"]').at(0);
- const findAssignees = () => wrapper.findAll('[data-testid="assigneesField"]');
- const findSeverityFields = () => wrapper.findAll('[data-testid="severityField"]');
- const findIssueFields = () => wrapper.findAll('[data-testid="issueField"]');
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findLoader = () => wrapper.findComponent(GlLoadingIcon);
+ const findStatusDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDateFields = () => wrapper.findAllComponents(TimeAgo);
+ const findSearch = () => wrapper.findComponent(FilteredSearchBar);
+ const findSeverityColumnHeader = () => wrapper.findByTestId('alert-management-severity-sort');
+ const findFirstIDField = () => wrapper.findAllByTestId('idField').at(0);
+ const findAssignees = () => wrapper.findAllByTestId('assigneesField');
+ const findSeverityFields = () => wrapper.findAllByTestId('severityField');
+ const findIssueFields = () => wrapper.findAllByTestId('issueField');
const alertsCount = {
open: 24,
triggered: 20,
@@ -40,29 +41,34 @@ describe('AlertManagementTable', () => {
};
function mountComponent({ provide = {}, data = {}, loading = false, stubs = {} } = {}) {
- wrapper = mount(AlertManagementTable, {
- provide: {
- ...defaultProvideValues,
- alertManagementEnabled: true,
- userCanEnableAlertManagement: true,
- ...provide,
- },
- data() {
- return data;
- },
- mocks: {
- $apollo: {
- mutate: jest.fn(),
- query: jest.fn(),
- queries: {
- alerts: {
- loading,
+ wrapper = extendedWrapper(
+ mount(AlertManagementTable, {
+ provide: {
+ ...defaultProvideValues,
+ alertManagementEnabled: true,
+ userCanEnableAlertManagement: true,
+ ...provide,
+ },
+ data() {
+ return data;
+ },
+ mocks: {
+ $apollo: {
+ mutate: jest.fn(),
+ query: jest.fn(),
+ queries: {
+ alerts: {
+ loading,
+ },
},
},
},
- },
- stubs,
- });
+ stubs,
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ }),
+ );
}
beforeEach(() => {
@@ -72,7 +78,6 @@ describe('AlertManagementTable', () => {
afterEach(() => {
if (wrapper) {
wrapper.destroy();
- wrapper = null;
}
mock.restore();
});
@@ -241,9 +246,14 @@ describe('AlertManagementTable', () => {
expect(findIssueFields().at(0).text()).toBe('None');
});
- it('renders a link when one exists', () => {
- expect(findIssueFields().at(1).text()).toBe('#1');
- expect(findIssueFields().at(1).attributes('href')).toBe('/gitlab-org/gitlab/-/issues/1');
+ it('renders a link when one exists with the issue state and title tooltip', () => {
+ const issueField = findIssueFields().at(1);
+ const tooltip = getBinding(issueField.element, 'gl-tooltip');
+
+ expect(issueField.text()).toBe(`#1 (closed)`);
+ expect(issueField.attributes('href')).toBe('/gitlab-org/gitlab/-/issues/1');
+ expect(issueField.attributes('title')).toBe('My test issue');
+ expect(tooltip).not.toBe(undefined);
});
});
diff --git a/spec/frontend/authentication/u2f/authenticate_spec.js b/spec/frontend/authentication/u2f/authenticate_spec.js
index bf50ee88035..153d4be56af 100644
--- a/spec/frontend/authentication/u2f/authenticate_spec.js
+++ b/spec/frontend/authentication/u2f/authenticate_spec.js
@@ -8,8 +8,6 @@ describe('U2FAuthenticate', () => {
let container;
let component;
- preloadFixtures('u2f/authenticate.html');
-
beforeEach(() => {
loadFixtures('u2f/authenticate.html');
u2fDevice = new MockU2FDevice();
diff --git a/spec/frontend/authentication/u2f/register_spec.js b/spec/frontend/authentication/u2f/register_spec.js
index 9cbadbc2fef..a814144ac7a 100644
--- a/spec/frontend/authentication/u2f/register_spec.js
+++ b/spec/frontend/authentication/u2f/register_spec.js
@@ -8,8 +8,6 @@ describe('U2FRegister', () => {
let container;
let component;
- preloadFixtures('u2f/register.html');
-
beforeEach((done) => {
loadFixtures('u2f/register.html');
u2fDevice = new MockU2FDevice();
diff --git a/spec/frontend/authentication/webauthn/authenticate_spec.js b/spec/frontend/authentication/webauthn/authenticate_spec.js
index 0a82adfd0ee..8b27560bbbe 100644
--- a/spec/frontend/authentication/webauthn/authenticate_spec.js
+++ b/spec/frontend/authentication/webauthn/authenticate_spec.js
@@ -13,7 +13,6 @@ const mockResponse = {
};
describe('WebAuthnAuthenticate', () => {
- preloadFixtures('webauthn/authenticate.html');
useMockNavigatorCredentials();
let fallbackElement;
diff --git a/spec/frontend/authentication/webauthn/register_spec.js b/spec/frontend/authentication/webauthn/register_spec.js
index 1de952d176d..43cd3d7ca34 100644
--- a/spec/frontend/authentication/webauthn/register_spec.js
+++ b/spec/frontend/authentication/webauthn/register_spec.js
@@ -5,7 +5,6 @@ import MockWebAuthnDevice from './mock_webauthn_device';
import { useMockNavigatorCredentials } from './util';
describe('WebAuthnRegister', () => {
- preloadFixtures('webauthn/register.html');
useMockNavigatorCredentials();
const mockResponse = {
diff --git a/spec/frontend/awards_handler_spec.js b/spec/frontend/awards_handler_spec.js
index edd17cfd810..988f5c98e2b 100644
--- a/spec/frontend/awards_handler_spec.js
+++ b/spec/frontend/awards_handler_spec.js
@@ -60,7 +60,6 @@ describe('AwardsHandler', () => {
u: '6.0',
},
};
- preloadFixtures('snippets/show.html');
const openAndWaitForEmojiMenu = (sel = '.js-add-award') => {
$(sel).eq(0).click();
diff --git a/spec/frontend/behaviors/quick_submit_spec.js b/spec/frontend/behaviors/quick_submit_spec.js
index d3d65892aff..86a85831c6b 100644
--- a/spec/frontend/behaviors/quick_submit_spec.js
+++ b/spec/frontend/behaviors/quick_submit_spec.js
@@ -6,8 +6,6 @@ describe('Quick Submit behavior', () => {
const keydownEvent = (options = { keyCode: 13, metaKey: true }) => $.Event('keydown', options);
- preloadFixtures('snippets/show.html');
-
beforeEach(() => {
loadFixtures('snippets/show.html');
diff --git a/spec/frontend/behaviors/requires_input_spec.js b/spec/frontend/behaviors/requires_input_spec.js
index 0f27f89d6dc..bb22133ae44 100644
--- a/spec/frontend/behaviors/requires_input_spec.js
+++ b/spec/frontend/behaviors/requires_input_spec.js
@@ -3,7 +3,6 @@ import '~/behaviors/requires_input';
describe('requiresInput', () => {
let submitButton;
- preloadFixtures('branches/new_branch.html');
beforeEach(() => {
loadFixtures('branches/new_branch.html');
diff --git a/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js b/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js
index 94ba1615c89..26d38b115b6 100644
--- a/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js
+++ b/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js
@@ -13,8 +13,6 @@ describe('ShortcutsIssuable', () => {
const snippetShowFixtureName = 'snippets/show.html';
const mrShowFixtureName = 'merge_requests/merge_request_of_current_user.html';
- preloadFixtures(snippetShowFixtureName, mrShowFixtureName);
-
beforeAll((done) => {
initCopyAsGFM();
diff --git a/spec/frontend/blob/blob_file_dropzone_spec.js b/spec/frontend/blob/blob_file_dropzone_spec.js
index 95520577e9a..e387ab7c18a 100644
--- a/spec/frontend/blob/blob_file_dropzone_spec.js
+++ b/spec/frontend/blob/blob_file_dropzone_spec.js
@@ -7,7 +7,6 @@ jest.mock('~/projects/upload_file_experiment', () => ({
}));
describe('BlobFileDropzone', () => {
- preloadFixtures('blob/show.html');
let dropzone;
let replaceFileButton;
diff --git a/spec/frontend/blob/sketch/index_spec.js b/spec/frontend/blob/sketch/index_spec.js
index a24e7de9037..7424897b22c 100644
--- a/spec/frontend/blob/sketch/index_spec.js
+++ b/spec/frontend/blob/sketch/index_spec.js
@@ -4,8 +4,6 @@ import SketchLoader from '~/blob/sketch';
jest.mock('jszip');
describe('Sketch viewer', () => {
- preloadFixtures('static/sketch_viewer.html');
-
beforeEach(() => {
loadFixtures('static/sketch_viewer.html');
});
diff --git a/spec/frontend/blob/viewer/index_spec.js b/spec/frontend/blob/viewer/index_spec.js
index d793b5eb190..e4f145ae81b 100644
--- a/spec/frontend/blob/viewer/index_spec.js
+++ b/spec/frontend/blob/viewer/index_spec.js
@@ -16,8 +16,6 @@ describe('Blob viewer', () => {
setTestTimeout(2000);
- preloadFixtures('blob/show_readme.html');
-
beforeEach(() => {
$.fn.extend(jQueryMock);
mock = new MockAdapter(axios);
diff --git a/spec/frontend/bootstrap_linked_tabs_spec.js b/spec/frontend/bootstrap_linked_tabs_spec.js
index 2d8939e6480..30fb140bc69 100644
--- a/spec/frontend/bootstrap_linked_tabs_spec.js
+++ b/spec/frontend/bootstrap_linked_tabs_spec.js
@@ -1,8 +1,6 @@
import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs';
describe('Linked Tabs', () => {
- preloadFixtures('static/linked_tabs.html');
-
beforeEach(() => {
loadFixtures('static/linked_tabs.html');
});
diff --git a/spec/frontend/ci_variable_list/ci_variable_list/ci_variable_list_spec.js b/spec/frontend/ci_variable_list/ci_variable_list/ci_variable_list_spec.js
index ad1bdec1735..1bca21b1d57 100644
--- a/spec/frontend/ci_variable_list/ci_variable_list/ci_variable_list_spec.js
+++ b/spec/frontend/ci_variable_list/ci_variable_list/ci_variable_list_spec.js
@@ -4,9 +4,6 @@ import VariableList from '~/ci_variable_list/ci_variable_list';
const HIDE_CLASS = 'hide';
describe('VariableList', () => {
- preloadFixtures('pipeline_schedules/edit.html');
- preloadFixtures('pipeline_schedules/edit_with_variables.html');
-
let $wrapper;
let variableList;
diff --git a/spec/frontend/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js b/spec/frontend/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js
index 4982b68fa81..eee1362440d 100644
--- a/spec/frontend/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js
+++ b/spec/frontend/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js
@@ -2,8 +2,6 @@ import $ from 'jquery';
import setupNativeFormVariableList from '~/ci_variable_list/native_form_variable_list';
describe('NativeFormVariableList', () => {
- preloadFixtures('pipeline_schedules/edit.html');
-
let $wrapper;
beforeEach(() => {
diff --git a/spec/frontend/collapsed_sidebar_todo_spec.js b/spec/frontend/collapsed_sidebar_todo_spec.js
index ef53cc9e103..7c659822672 100644
--- a/spec/frontend/collapsed_sidebar_todo_spec.js
+++ b/spec/frontend/collapsed_sidebar_todo_spec.js
@@ -14,9 +14,6 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
const jsonFixtureName = 'todos/todos.json';
let mock;
- preloadFixtures(fixtureName);
- preloadFixtures(jsonFixtureName);
-
beforeEach(() => {
const todoData = getJSONFixture(jsonFixtureName);
new Sidebar();
diff --git a/spec/frontend/commit/pipelines/pipelines_spec.js b/spec/frontend/commit/pipelines/pipelines_spec.js
index 3d6debda520..c8c8f6408b7 100644
--- a/spec/frontend/commit/pipelines/pipelines_spec.js
+++ b/spec/frontend/commit/pipelines/pipelines_spec.js
@@ -17,8 +17,6 @@ describe('Pipelines table in Commits and Merge requests', () => {
errorStateSvgPath: 'foo',
};
- preloadFixtures(jsonFixtureName);
-
const findRunPipelineBtn = () => vm.$el.querySelector('[data-testid="run_pipeline_button"]');
const findRunPipelineBtnMobile = () =>
vm.$el.querySelector('[data-testid="run_pipeline_button_mobile"]');
diff --git a/spec/frontend/create_item_dropdown_spec.js b/spec/frontend/create_item_dropdown_spec.js
index 7314eb5eee8..56c09cd731e 100644
--- a/spec/frontend/create_item_dropdown_spec.js
+++ b/spec/frontend/create_item_dropdown_spec.js
@@ -20,8 +20,6 @@ const DROPDOWN_ITEM_DATA = [
];
describe('CreateItemDropdown', () => {
- preloadFixtures('static/create_item_dropdown.html');
-
let $wrapperEl;
let createItemDropdown;
diff --git a/spec/frontend/deprecated_jquery_dropdown_spec.js b/spec/frontend/deprecated_jquery_dropdown_spec.js
index 6070532a1bf..7858f88f8c3 100644
--- a/spec/frontend/deprecated_jquery_dropdown_spec.js
+++ b/spec/frontend/deprecated_jquery_dropdown_spec.js
@@ -10,8 +10,6 @@ jest.mock('~/lib/utils/url_utility', () => ({
}));
describe('deprecatedJQueryDropdown', () => {
- preloadFixtures('static/deprecated_jquery_dropdown.html');
-
const NON_SELECTABLE_CLASSES =
'.divider, .separator, .dropdown-header, .dropdown-menu-empty-item';
const SEARCH_INPUT_SELECTOR = '.dropdown-input-field';
diff --git a/spec/frontend/diffs/mock_data/diff_with_commit.js b/spec/frontend/diffs/mock_data/diff_with_commit.js
index d646294ee84..f3b39bd3577 100644
--- a/spec/frontend/diffs/mock_data/diff_with_commit.js
+++ b/spec/frontend/diffs/mock_data/diff_with_commit.js
@@ -1,7 +1,5 @@
const FIXTURE = 'merge_request_diffs/with_commit.json';
-preloadFixtures(FIXTURE);
-
export default function getDiffWithCommit() {
return getJSONFixture(FIXTURE);
}
diff --git a/spec/frontend/filtered_search/dropdown_user_spec.js b/spec/frontend/filtered_search/dropdown_user_spec.js
index 0e2d2ee6c09..961587f7146 100644
--- a/spec/frontend/filtered_search/dropdown_user_spec.js
+++ b/spec/frontend/filtered_search/dropdown_user_spec.js
@@ -78,7 +78,6 @@ describe('Dropdown User', () => {
describe('hideCurrentUser', () => {
const fixtureTemplate = 'issues/issue_list.html';
- preloadFixtures(fixtureTemplate);
let dropdown;
let authorFilterDropdownElement;
diff --git a/spec/frontend/filtered_search/dropdown_utils_spec.js b/spec/frontend/filtered_search/dropdown_utils_spec.js
index 32d1f909d0b..49e14f58630 100644
--- a/spec/frontend/filtered_search/dropdown_utils_spec.js
+++ b/spec/frontend/filtered_search/dropdown_utils_spec.js
@@ -5,7 +5,6 @@ import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered
describe('Dropdown Utils', () => {
const issueListFixture = 'issues/issue_list.html';
- preloadFixtures(issueListFixture);
describe('getEscapedText', () => {
it('should return same word when it has no space', () => {
diff --git a/spec/frontend/filtered_search/visual_token_value_spec.js b/spec/frontend/filtered_search/visual_token_value_spec.js
index a2082271efe..772fa7d07ed 100644
--- a/spec/frontend/filtered_search/visual_token_value_spec.js
+++ b/spec/frontend/filtered_search/visual_token_value_spec.js
@@ -133,8 +133,6 @@ describe('Filtered Search Visual Tokens', () => {
const jsonFixtureName = 'labels/project_labels.json';
const dummyEndpoint = '/dummy/endpoint';
- preloadFixtures(jsonFixtureName);
-
let labelData;
beforeAll(() => {
diff --git a/spec/frontend/gl_field_errors_spec.js b/spec/frontend/gl_field_errors_spec.js
index a1737211252..ada3b34e6b1 100644
--- a/spec/frontend/gl_field_errors_spec.js
+++ b/spec/frontend/gl_field_errors_spec.js
@@ -8,8 +8,6 @@ describe('GL Style Field Errors', () => {
testContext = {};
});
- preloadFixtures('static/gl_field_errors.html');
-
beforeEach(() => {
loadFixtures('static/gl_field_errors.html');
const $form = $('form.gl-show-field-errors');
diff --git a/spec/frontend/header_spec.js b/spec/frontend/header_spec.js
index 27305abfafa..4ca6d7259bd 100644
--- a/spec/frontend/header_spec.js
+++ b/spec/frontend/header_spec.js
@@ -15,7 +15,6 @@ describe('Header', () => {
$(document).trigger('todo:toggle', newCount);
}
- preloadFixtures(fixtureTemplate);
beforeEach(() => {
initTodoToggle();
loadFixtures(fixtureTemplate);
diff --git a/spec/frontend/integrations/integration_settings_form_spec.js b/spec/frontend/integrations/integration_settings_form_spec.js
index 348b942703f..cbb2ef380ba 100644
--- a/spec/frontend/integrations/integration_settings_form_spec.js
+++ b/spec/frontend/integrations/integration_settings_form_spec.js
@@ -7,7 +7,6 @@ jest.mock('~/vue_shared/plugins/global_toast');
describe('IntegrationSettingsForm', () => {
const FIXTURE = 'services/edit_service.html';
- preloadFixtures(FIXTURE);
beforeEach(() => {
loadFixtures(FIXTURE);
diff --git a/spec/frontend/issue_spec.js b/spec/frontend/issue_spec.js
index 970fdacd492..952ef54d286 100644
--- a/spec/frontend/issue_spec.js
+++ b/spec/frontend/issue_spec.js
@@ -8,11 +8,6 @@ describe('Issue', () => {
let testContext;
let mock;
- beforeAll(() => {
- preloadFixtures('issues/closed-issue.html');
- preloadFixtures('issues/open-issue.html');
- });
-
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(/(.*)\/related_branches$/).reply(200, {});
diff --git a/spec/frontend/line_highlighter_spec.js b/spec/frontend/line_highlighter_spec.js
index 8318f63ab3e..b5a0adc9d49 100644
--- a/spec/frontend/line_highlighter_spec.js
+++ b/spec/frontend/line_highlighter_spec.js
@@ -7,7 +7,6 @@ import LineHighlighter from '~/line_highlighter';
describe('LineHighlighter', () => {
const testContext = {};
- preloadFixtures('static/line_highlighter.html');
const clickLine = (number, eventData = {}) => {
if ($.isEmptyObject(eventData)) {
return $(`#L${number}`).click();
diff --git a/spec/frontend/merge_request_spec.js b/spec/frontend/merge_request_spec.js
index 84647a108b2..0b7ed349507 100644
--- a/spec/frontend/merge_request_spec.js
+++ b/spec/frontend/merge_request_spec.js
@@ -9,7 +9,6 @@ describe('MergeRequest', () => {
describe('task lists', () => {
let mock;
- preloadFixtures('merge_requests/merge_request_with_task_list.html');
beforeEach(() => {
loadFixtures('merge_requests/merge_request_with_task_list.html');
diff --git a/spec/frontend/merge_request_tabs_spec.js b/spec/frontend/merge_request_tabs_spec.js
index fd2c240aff3..23e9bf8b447 100644
--- a/spec/frontend/merge_request_tabs_spec.js
+++ b/spec/frontend/merge_request_tabs_spec.js
@@ -21,11 +21,6 @@ describe('MergeRequestTabs', () => {
$.extend(stubLocation, defaults, stubs || {});
};
- preloadFixtures(
- 'merge_requests/merge_request_with_task_list.html',
- 'merge_requests/diff_comment.html',
- );
-
beforeEach(() => {
initMrPage();
diff --git a/spec/frontend/mini_pipeline_graph_dropdown_spec.js b/spec/frontend/mini_pipeline_graph_dropdown_spec.js
index 3ff34c967e4..ccd5a4ea142 100644
--- a/spec/frontend/mini_pipeline_graph_dropdown_spec.js
+++ b/spec/frontend/mini_pipeline_graph_dropdown_spec.js
@@ -5,8 +5,6 @@ import axios from '~/lib/utils/axios_utils';
import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
describe('Mini Pipeline Graph Dropdown', () => {
- preloadFixtures('static/mini_dropdown_graph.html');
-
beforeEach(() => {
loadFixtures('static/mini_dropdown_graph.html');
});
diff --git a/spec/frontend/new_branch_spec.js b/spec/frontend/new_branch_spec.js
index 7e6b8a78d4f..66b28a8c0dc 100644
--- a/spec/frontend/new_branch_spec.js
+++ b/spec/frontend/new_branch_spec.js
@@ -9,8 +9,6 @@ describe('Branch', () => {
});
describe('create a new branch', () => {
- preloadFixtures('branches/new_branch.html');
-
function fillNameWith(value) {
$('.js-branch-name').val(value).trigger('blur');
}
diff --git a/spec/frontend/notes/components/diff_discussion_header_spec.js b/spec/frontend/notes/components/diff_discussion_header_spec.js
index fdc89522901..fa34a5e8d39 100644
--- a/spec/frontend/notes/components/diff_discussion_header_spec.js
+++ b/spec/frontend/notes/components/diff_discussion_header_spec.js
@@ -6,14 +6,10 @@ import createStore from '~/notes/stores';
import mockDiffFile from '../../diffs/mock_data/diff_discussions';
import { discussionMock } from '../mock_data';
-const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json';
-
describe('diff_discussion_header component', () => {
let store;
let wrapper;
- preloadFixtures(discussionWithTwoUnresolvedNotes);
-
beforeEach(() => {
window.mrTabs = {};
store = createStore();
diff --git a/spec/frontend/notes/components/noteable_discussion_spec.js b/spec/frontend/notes/components/noteable_discussion_spec.js
index 34df39bf1c7..dd65351ef88 100644
--- a/spec/frontend/notes/components/noteable_discussion_spec.js
+++ b/spec/frontend/notes/components/noteable_discussion_spec.js
@@ -24,8 +24,6 @@ describe('noteable_discussion component', () => {
let wrapper;
let originalGon;
- preloadFixtures(discussionWithTwoUnresolvedNotes);
-
beforeEach(() => {
window.mrTabs = {};
store = createStore();
diff --git a/spec/frontend/notes/stores/getters_spec.js b/spec/frontend/notes/stores/getters_spec.js
index 4ebfc679310..4d2f86a1ecf 100644
--- a/spec/frontend/notes/stores/getters_spec.js
+++ b/spec/frontend/notes/stores/getters_spec.js
@@ -26,8 +26,6 @@ const createDiscussionNeighborParams = (discussionId, diffOrder, step) => ({
describe('Getters Notes Store', () => {
let state;
- preloadFixtures(discussionWithTwoUnresolvedNotes);
-
beforeEach(() => {
state = {
discussions: [individualNote],
diff --git a/spec/frontend/oauth_remember_me_spec.js b/spec/frontend/oauth_remember_me_spec.js
index 910676a97ed..70bda1d9f9e 100644
--- a/spec/frontend/oauth_remember_me_spec.js
+++ b/spec/frontend/oauth_remember_me_spec.js
@@ -6,8 +6,6 @@ describe('OAuthRememberMe', () => {
return $(`#oauth-container .oauth-login${selector}`).parent('form').attr('action');
};
- preloadFixtures('static/oauth_remember_me.html');
-
beforeEach(() => {
loadFixtures('static/oauth_remember_me.html');
diff --git a/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js b/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js
index 2c76adf761f..71c9da238b4 100644
--- a/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js
+++ b/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js
@@ -14,8 +14,6 @@ describe('Abuse Reports', () => {
const findMessage = (searchText) =>
$messages.filter((index, element) => element.innerText.indexOf(searchText) > -1).first();
- preloadFixtures(FIXTURE);
-
beforeEach(() => {
loadFixtures(FIXTURE);
new AbuseReports(); // eslint-disable-line no-new
diff --git a/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js b/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js
index 8816609d1d2..9f326dc33c0 100644
--- a/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js
+++ b/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js
@@ -8,7 +8,6 @@ describe('AccountAndLimits', () => {
const FIXTURE = 'application_settings/accounts_and_limit.html';
let $userDefaultExternal;
let $userInternalRegex;
- preloadFixtures(FIXTURE);
beforeEach(() => {
loadFixtures(FIXTURE);
diff --git a/spec/frontend/pages/admin/users/new/index_spec.js b/spec/frontend/pages/admin/users/new/index_spec.js
index 60482860e84..ec9fe487030 100644
--- a/spec/frontend/pages/admin/users/new/index_spec.js
+++ b/spec/frontend/pages/admin/users/new/index_spec.js
@@ -7,8 +7,6 @@ describe('UserInternalRegexHandler', () => {
let $userEmail;
let $warningMessage;
- preloadFixtures(FIXTURE);
-
beforeEach(() => {
loadFixtures(FIXTURE);
// eslint-disable-next-line no-new
diff --git a/spec/frontend/pages/dashboard/todos/index/todos_spec.js b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
index fb612f17669..de8b29d54fc 100644
--- a/spec/frontend/pages/dashboard/todos/index/todos_spec.js
+++ b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
@@ -14,7 +14,6 @@ const TEST_COUNT_BIG = 2000;
const TEST_DONE_COUNT_BIG = 7300;
describe('Todos', () => {
- preloadFixtures('todos/todos.html');
let todoItem;
let mock;
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
index de63409b181..2a3b07f95f2 100644
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
@@ -6,8 +6,6 @@ import TimezoneDropdown, {
} from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown';
describe('Timezone Dropdown', () => {
- preloadFixtures('pipeline_schedules/edit.html');
-
let $inputEl = null;
let $dropdownEl = null;
let $wrapper = null;
diff --git a/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js b/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js
index 8632c852720..e39a3904613 100644
--- a/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js
+++ b/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js
@@ -6,8 +6,6 @@ describe('preserve_url_fragment', () => {
return $(`.omniauth-container ${selector}`).parent('form').attr('action');
};
- preloadFixtures('sessions/new.html');
-
beforeEach(() => {
loadFixtures('sessions/new.html');
});
diff --git a/spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js b/spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js
index f04c16d2ddb..6aa725fbd7d 100644
--- a/spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js
+++ b/spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js
@@ -18,8 +18,6 @@ describe('SigninTabsMemoizer', () => {
return memo;
}
- preloadFixtures(fixtureTemplate);
-
beforeEach(() => {
loadFixtures(fixtureTemplate);
diff --git a/spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js b/spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js
index 5eeccd78265..ef8ca574e59 100644
--- a/spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js
+++ b/spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js
@@ -3,7 +3,7 @@ import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_e
import PipelineStatus from '~/pipeline_editor/components/header/pipeline_status.vue';
import ValidationSegment from '~/pipeline_editor/components/header/validation_segment.vue';
-import { mockLintResponse } from '../../mock_data';
+import { mockCiYml, mockLintResponse } from '../../mock_data';
describe('Pipeline editor header', () => {
let wrapper;
@@ -19,8 +19,9 @@ describe('Pipeline editor header', () => {
...mockProvide,
...provide,
},
- props: {
+ propsData: {
ciConfigData: mockLintResponse,
+ ciFileContent: mockCiYml,
isCiConfigDataLoading: false,
},
});
diff --git a/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js b/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js
index cf1d89e1d7c..274c2d1b8da 100644
--- a/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js
+++ b/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js
@@ -7,9 +7,9 @@ import ValidationSegment, {
i18n,
} from '~/pipeline_editor/components/header/validation_segment.vue';
import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants';
-import { mockYmlHelpPagePath, mergeUnwrappedCiConfig } from '../../mock_data';
+import { mockYmlHelpPagePath, mergeUnwrappedCiConfig, mockCiYml } from '../../mock_data';
-describe('~/pipeline_editor/components/info/validation_segment.vue', () => {
+describe('Validation segment component', () => {
let wrapper;
const createComponent = (props = {}) => {
@@ -20,6 +20,7 @@ describe('~/pipeline_editor/components/info/validation_segment.vue', () => {
},
propsData: {
ciConfig: mergeUnwrappedCiConfig(),
+ ciFileContent: mockCiYml,
loading: false,
...props,
},
@@ -42,6 +43,20 @@ describe('~/pipeline_editor/components/info/validation_segment.vue', () => {
expect(wrapper.text()).toBe(i18n.loading);
});
+ describe('when config is empty', () => {
+ beforeEach(() => {
+ createComponent({ ciFileContent: '' });
+ });
+
+ it('has check icon', () => {
+ expect(findIcon().props('name')).toBe('check');
+ });
+
+ it('shows a message for empty state', () => {
+ expect(findValidationMsg().text()).toBe(i18n.empty);
+ });
+ });
+
describe('when config is valid', () => {
beforeEach(() => {
createComponent({});
@@ -61,7 +76,7 @@ describe('~/pipeline_editor/components/info/validation_segment.vue', () => {
});
});
- describe('when config is not valid', () => {
+ describe('when config is invalid', () => {
beforeEach(() => {
createComponent({
ciConfig: mergeUnwrappedCiConfig({
diff --git a/spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js b/spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js
index 2210fc87f68..b444d9dcfea 100644
--- a/spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js
+++ b/spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js
@@ -1,42 +1,79 @@
-import { GlSprintf } from '@gitlab/ui';
+import { GlButton, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
describe('Pipeline editor empty state', () => {
let wrapper;
const defaultProvide = {
+ glFeatures: {
+ pipelineEditorEmptyStateAction: false,
+ },
emptyStateIllustrationPath: 'my/svg/path',
};
- const createComponent = () => {
+ const createComponent = ({ provide } = {}) => {
wrapper = shallowMount(PipelineEditorEmptyState, {
- provide: defaultProvide,
+ provide: { ...defaultProvide, ...provide },
});
};
const findSvgImage = () => wrapper.find('img');
const findTitle = () => wrapper.find('h1');
+ const findConfirmButton = () => wrapper.findComponent(GlButton);
const findDescription = () => wrapper.findComponent(GlSprintf);
- beforeEach(() => {
- createComponent();
- });
-
afterEach(() => {
wrapper.destroy();
});
- it('renders an svg image', () => {
- expect(findSvgImage().exists()).toBe(true);
- });
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
- it('renders a title', () => {
- expect(findTitle().exists()).toBe(true);
- expect(findTitle().text()).toBe(wrapper.vm.$options.i18n.title);
+ it('renders an svg image', () => {
+ expect(findSvgImage().exists()).toBe(true);
+ });
+
+ it('renders a title', () => {
+ expect(findTitle().exists()).toBe(true);
+ expect(findTitle().text()).toBe(wrapper.vm.$options.i18n.title);
+ });
+
+ it('renders a description', () => {
+ expect(findDescription().exists()).toBe(true);
+ expect(findDescription().html()).toContain(wrapper.vm.$options.i18n.body);
+ });
+
+ describe('with feature flag off', () => {
+ it('does not renders a CTA button', () => {
+ expect(findConfirmButton().exists()).toBe(false);
+ });
+ });
});
- it('renders a description', () => {
- expect(findDescription().exists()).toBe(true);
- expect(findDescription().html()).toContain(wrapper.vm.$options.i18n.body);
+ describe('with feature flag on', () => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ glFeatures: {
+ pipelineEditorEmptyStateAction: true,
+ },
+ },
+ });
+ });
+
+ it('renders a CTA button', () => {
+ expect(findConfirmButton().exists()).toBe(true);
+ expect(findConfirmButton().text()).toBe(wrapper.vm.$options.i18n.btnText);
+ });
+
+ it('emits an event when clicking on the CTA', async () => {
+ const expectedEvent = 'createEmptyConfigFile';
+ expect(wrapper.emitted(expectedEvent)).toBeUndefined();
+
+ await findConfirmButton().vm.$emit('click');
+ expect(wrapper.emitted(expectedEvent)).toHaveLength(1);
+ });
});
});
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
index 0dfd123db75..887d296222f 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
@@ -7,6 +7,7 @@ import httpStatusCodes from '~/lib/utils/http_status';
import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
import TextEditor from '~/pipeline_editor/components/editor/text_editor.vue';
+import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
import { COMMIT_SUCCESS, COMMIT_FAILURE, LOAD_FAILURE_UNKNOWN } from '~/pipeline_editor/constants';
import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.graphql';
@@ -30,6 +31,9 @@ const MockEditorLite = {
const mockProvide = {
ciConfigPath: mockCiConfigPath,
defaultBranch: mockDefaultBranch,
+ glFeatures: {
+ pipelineEditorEmptyStateAction: false,
+ },
projectFullPath: mockProjectFullPath,
};
@@ -40,14 +44,17 @@ describe('Pipeline editor app component', () => {
let mockBlobContentData;
let mockCiConfigData;
- const createComponent = ({ blobLoading = false, options = {} } = {}) => {
+ const createComponent = ({ blobLoading = false, options = {}, provide = {} } = {}) => {
wrapper = shallowMount(PipelineEditorApp, {
- provide: mockProvide,
+ provide: { ...mockProvide, ...provide },
stubs: {
GlTabs,
GlButton,
CommitForm,
+ PipelineEditorHome,
+ PipelineEditorTabs,
EditorLite: MockEditorLite,
+ PipelineEditorEmptyState,
},
mocks: {
$apollo: {
@@ -65,7 +72,7 @@ describe('Pipeline editor app component', () => {
});
};
- const createComponentWithApollo = ({ props = {} } = {}) => {
+ const createComponentWithApollo = ({ props = {}, provide = {} } = {}) => {
const handlers = [[getCiConfigData, mockCiConfigData]];
const resolvers = {
Query: {
@@ -86,7 +93,7 @@ describe('Pipeline editor app component', () => {
apolloProvider: mockApollo,
};
- createComponent({ props, options });
+ createComponent({ props, provide, options });
};
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
@@ -94,6 +101,8 @@ describe('Pipeline editor app component', () => {
const findEditorHome = () => wrapper.findComponent(PipelineEditorHome);
const findTextEditor = () => wrapper.findComponent(TextEditor);
const findEmptyState = () => wrapper.findComponent(PipelineEditorEmptyState);
+ const findEmptyStateButton = () =>
+ wrapper.findComponent(PipelineEditorEmptyState).findComponent(GlButton);
beforeEach(() => {
mockBlobContentData = jest.fn();
@@ -105,7 +114,6 @@ describe('Pipeline editor app component', () => {
mockCiConfigData.mockReset();
wrapper.destroy();
- wrapper = null;
});
it('displays a loading icon if the blob query is loading', () => {
@@ -196,6 +204,34 @@ describe('Pipeline editor app component', () => {
});
});
+ describe('when landing on the empty state with feature flag on', () => {
+ it('user can click on CTA button and see an empty editor', async () => {
+ mockBlobContentData.mockRejectedValueOnce({
+ response: {
+ status: httpStatusCodes.NOT_FOUND,
+ },
+ });
+
+ createComponentWithApollo({
+ provide: {
+ glFeatures: {
+ pipelineEditorEmptyStateAction: true,
+ },
+ },
+ });
+
+ await waitForPromises();
+
+ expect(findEmptyState().exists()).toBe(true);
+ expect(findTextEditor().exists()).toBe(false);
+
+ await findEmptyStateButton().vm.$emit('click');
+
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findTextEditor().exists()).toBe(true);
+ });
+ });
+
describe('when the user commits', () => {
const updateFailureMessage = 'The GitLab CI configuration could not be updated.';
diff --git a/spec/frontend/pipelines/pipelines_table_row_spec.js b/spec/frontend/pipelines/pipelines_table_row_spec.js
index 56da636bb3d..26767cd3805 100644
--- a/spec/frontend/pipelines/pipelines_table_row_spec.js
+++ b/spec/frontend/pipelines/pipelines_table_row_spec.js
@@ -19,8 +19,6 @@ describe('Pipelines Table Row', () => {
let pipelineWithoutAuthor;
let pipelineWithoutCommit;
- preloadFixtures(jsonFixtureName);
-
beforeEach(() => {
const { pipelines } = getJSONFixture(jsonFixtureName);
diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/pipelines/pipelines_table_spec.js
index 4f4d71e5dab..d72ea7b42a6 100644
--- a/spec/frontend/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/pipelines/pipelines_table_spec.js
@@ -52,8 +52,6 @@ describe('Pipelines Table', () => {
const findTimeAgoTh = () => wrapper.findByTestId('timeago-th');
const findActionsTh = () => wrapper.findByTestId('actions-th');
- preloadFixtures(jsonFixtureName);
-
beforeEach(() => {
const { pipelines } = getJSONFixture(jsonFixtureName);
pipeline = pipelines.find((p) => p.user !== null && p.commit !== null);
diff --git a/spec/frontend/pipelines_spec.js b/spec/frontend/pipelines_spec.js
index 6d4d634c575..add91fbcc23 100644
--- a/spec/frontend/pipelines_spec.js
+++ b/spec/frontend/pipelines_spec.js
@@ -1,8 +1,6 @@
import Pipelines from '~/pipelines';
describe('Pipelines', () => {
- preloadFixtures('static/pipeline_graph.html');
-
beforeEach(() => {
loadFixtures('static/pipeline_graph.html');
});
diff --git a/spec/frontend/project_select_combo_button_spec.js b/spec/frontend/project_select_combo_button_spec.js
index c47db71b4ac..5cdc3d174a1 100644
--- a/spec/frontend/project_select_combo_button_spec.js
+++ b/spec/frontend/project_select_combo_button_spec.js
@@ -10,8 +10,6 @@ describe('Project Select Combo Button', () => {
testContext = {};
});
- preloadFixtures(fixturePath);
-
beforeEach(() => {
testContext.defaults = {
label: 'Select project to create issue',
diff --git a/spec/frontend/prometheus_metrics/custom_metrics_spec.js b/spec/frontend/prometheus_metrics/custom_metrics_spec.js
index 3e3d4ee361a..20593351ee5 100644
--- a/spec/frontend/prometheus_metrics/custom_metrics_spec.js
+++ b/spec/frontend/prometheus_metrics/custom_metrics_spec.js
@@ -9,7 +9,6 @@ describe('PrometheusMetrics', () => {
const customMetricsEndpoint =
'http://test.host/frontend-fixtures/services-project/prometheus/metrics';
let mock;
- preloadFixtures(FIXTURE);
beforeEach(() => {
mock = new MockAdapter(axios);
diff --git a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
index 722a5274ad4..a703dc0a66f 100644
--- a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
+++ b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
@@ -6,7 +6,6 @@ import { metrics2 as metrics, missingVarMetrics } from './mock_data';
describe('PrometheusMetrics', () => {
const FIXTURE = 'services/prometheus/prometheus_service.html';
- preloadFixtures(FIXTURE);
beforeEach(() => {
loadFixtures(FIXTURE);
diff --git a/spec/frontend/read_more_spec.js b/spec/frontend/read_more_spec.js
index d1d01272403..16f0d7fb075 100644
--- a/spec/frontend/read_more_spec.js
+++ b/spec/frontend/read_more_spec.js
@@ -3,8 +3,6 @@ import initReadMore from '~/read_more';
describe('Read more click-to-expand functionality', () => {
const fixtureName = 'projects/overview.html';
- preloadFixtures(fixtureName);
-
beforeEach(() => {
loadFixtures(fixtureName);
});
diff --git a/spec/frontend/right_sidebar_spec.js b/spec/frontend/right_sidebar_spec.js
index f3719b28baa..8699e1cf420 100644
--- a/spec/frontend/right_sidebar_spec.js
+++ b/spec/frontend/right_sidebar_spec.js
@@ -27,7 +27,6 @@ const assertSidebarState = (state) => {
describe('RightSidebar', () => {
describe('fixture tests', () => {
const fixtureName = 'issues/open-issue.html';
- preloadFixtures(fixtureName);
let mock;
beforeEach(() => {
diff --git a/spec/frontend/search/highlight_blob_search_result_spec.js b/spec/frontend/search/highlight_blob_search_result_spec.js
index c1b0c7d794b..6908bcbd283 100644
--- a/spec/frontend/search/highlight_blob_search_result_spec.js
+++ b/spec/frontend/search/highlight_blob_search_result_spec.js
@@ -4,8 +4,6 @@ const fixture = 'search/blob_search_result.html';
const searchKeyword = 'Send'; // spec/frontend/fixtures/search.rb#79
describe('search/highlight_blob_search_result', () => {
- preloadFixtures(fixture);
-
beforeEach(() => loadFixtures(fixture));
it('highlights lines with search term occurrence', () => {
diff --git a/spec/frontend/search_autocomplete_spec.js b/spec/frontend/search_autocomplete_spec.js
index a9fbe0fe552..5aca07d59e4 100644
--- a/spec/frontend/search_autocomplete_spec.js
+++ b/spec/frontend/search_autocomplete_spec.js
@@ -105,7 +105,6 @@ describe('Search autocomplete dropdown', () => {
expect(list.find(mrsIHaveCreatedLink).text()).toBe("Merge requests I've created");
};
- preloadFixtures('static/search_autocomplete.html');
beforeEach(() => {
loadFixtures('static/search_autocomplete.html');
diff --git a/spec/frontend/settings_panels_spec.js b/spec/frontend/settings_panels_spec.js
index 8666106d3c6..6b739617b97 100644
--- a/spec/frontend/settings_panels_spec.js
+++ b/spec/frontend/settings_panels_spec.js
@@ -2,8 +2,6 @@ import $ from 'jquery';
import initSettingsPanels, { isExpanded } from '~/settings_panels';
describe('Settings Panels', () => {
- preloadFixtures('groups/edit.html');
-
beforeEach(() => {
loadFixtures('groups/edit.html');
});
diff --git a/spec/frontend/shortcuts_spec.js b/spec/frontend/shortcuts_spec.js
index 1650dd2c1ca..fc5eeee9687 100644
--- a/spec/frontend/shortcuts_spec.js
+++ b/spec/frontend/shortcuts_spec.js
@@ -20,8 +20,6 @@ describe('Shortcuts', () => {
target,
});
- preloadFixtures(fixtureName);
-
beforeEach(() => {
loadFixtures(fixtureName);
diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js
index b6b29faef79..9b95ed6b816 100644
--- a/spec/frontend/test_setup.js
+++ b/spec/frontend/test_setup.js
@@ -44,11 +44,6 @@ Object.assign(global, {
getJSONFixture,
loadFixtures: loadHTMLFixture,
setFixtures: setHTMLFixture,
-
- // The following functions fill the fixtures cache in Karma.
- // This is not necessary in Jest because we make no Ajax request.
- loadJSONFixtures() {},
- preloadFixtures() {},
});
// custom-jquery-matchers was written for an old Jest version, we need to make it compatible
diff --git a/spec/frontend/user_popovers_spec.js b/spec/frontend/user_popovers_spec.js
index 5c6053f413f..745b66fd700 100644
--- a/spec/frontend/user_popovers_spec.js
+++ b/spec/frontend/user_popovers_spec.js
@@ -3,7 +3,6 @@ import initUserPopovers from '~/user_popovers';
describe('User Popovers', () => {
const fixtureTemplate = 'merge_requests/merge_request_with_mentions.html';
- preloadFixtures(fixtureTemplate);
const selector = '.js-user-link, .gfm-project_member';
const findFixtureLinks = () => {
diff --git a/spec/frontend/vue_shared/alert_details/alert_details_spec.js b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
index ce410a8b3e7..20301e1bc88 100644
--- a/spec/frontend/vue_shared/alert_details/alert_details_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
@@ -89,7 +89,7 @@ describe('AlertDetails', () => {
const findIncidentCreationAlert = () => wrapper.findByTestId('incidentCreationError');
const findEnvironmentName = () => wrapper.findByTestId('environmentName');
const findEnvironmentPath = () => wrapper.findByTestId('environmentPath');
- const findDetailsTable = () => wrapper.find(AlertDetailsTable);
+ const findDetailsTable = () => wrapper.findComponent(AlertDetailsTable);
const findMetricsTab = () => wrapper.findByTestId('metrics');
describe('Alert details', () => {
@@ -192,23 +192,21 @@ describe('AlertDetails', () => {
describe('Create incident from alert', () => {
it('should display "View incident" button that links the incident page when incident exists', () => {
- const issueIid = '3';
+ const iid = '3';
mountComponent({
- data: { alert: { ...mockAlert, issueIid }, sidebarStatus: false },
+ data: { alert: { ...mockAlert, issue: { iid } }, sidebarStatus: false },
});
expect(findViewIncidentBtn().exists()).toBe(true);
- expect(findViewIncidentBtn().attributes('href')).toBe(
- joinPaths(projectIssuesPath, issueIid),
- );
+ expect(findViewIncidentBtn().attributes('href')).toBe(joinPaths(projectIssuesPath, iid));
expect(findCreateIncidentBtn().exists()).toBe(false);
});
it('should display "Create incident" button when incident doesn\'t exist yet', () => {
- const issueIid = null;
+ const issue = null;
mountComponent({
mountMethod: mount,
- data: { alert: { ...mockAlert, issueIid } },
+ data: { alert: { ...mockAlert, issue } },
});
return wrapper.vm.$nextTick().then(() => {
diff --git a/spec/frontend/vue_shared/alert_details/mocks/alerts.json b/spec/frontend/vue_shared/alert_details/mocks/alerts.json
index 5267a4fe50d..007557e234a 100644
--- a/spec/frontend/vue_shared/alert_details/mocks/alerts.json
+++ b/spec/frontend/vue_shared/alert_details/mocks/alerts.json
@@ -21,7 +21,7 @@
"endedAt": "2020-04-17T23:18:14.996Z",
"status": "ACKNOWLEDGED",
"assignees": { "nodes": [{ "username": "root", "avatarUrl": "/url", "name": "root" }] },
- "issueIid": "1",
+ "issue": { "state" : "closed", "iid": "1", "title": "My test issue" },
"notes": {
"nodes": [
{
diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
index a6c5e23ae14..650d204201e 100644
--- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
+++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
@@ -18,7 +18,6 @@ const DEFAULT_PROPS = {
describe('User Popover Component', () => {
const fixtureTemplate = 'merge_requests/diff_comment.html';
- preloadFixtures(fixtureTemplate);
let wrapper;
diff --git a/spec/frontend/zen_mode_spec.js b/spec/frontend/zen_mode_spec.js
index 5cc1d2200d3..bf4b57d8afb 100644
--- a/spec/frontend/zen_mode_spec.js
+++ b/spec/frontend/zen_mode_spec.js
@@ -13,8 +13,6 @@ describe('ZenMode', () => {
let dropzoneForElementSpy;
const fixtureName = 'snippets/show.html';
- preloadFixtures(fixtureName);
-
function enterZen() {
$('.notes-form .js-zen-enter').click();
}
diff --git a/spec/graphql/types/alert_management/alert_type_spec.rb b/spec/graphql/types/alert_management/alert_type_spec.rb
index 82b48a20708..9ff01418c9a 100644
--- a/spec/graphql/types/alert_management/alert_type_spec.rb
+++ b/spec/graphql/types/alert_management/alert_type_spec.rb
@@ -10,7 +10,8 @@ RSpec.describe GitlabSchema.types['AlertManagementAlert'] do
it 'exposes the expected fields' do
expected_fields = %i[
iid
- issue_iid
+ issueIid
+ issue
title
description
severity
diff --git a/spec/lib/sentry/api_urls_spec.rb b/spec/lib/error_tracking/sentry_client/api_urls_spec.rb
index d56b4397e1c..bd701748dc2 100644
--- a/spec/lib/sentry/api_urls_spec.rb
+++ b/spec/lib/error_tracking/sentry_client/api_urls_spec.rb
@@ -2,13 +2,13 @@
require 'spec_helper'
-RSpec.describe Sentry::ApiUrls do
+RSpec.describe ErrorTracking::SentryClient::ApiUrls do
let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project/' }
let(:token) { 'test-token' }
let(:issue_id) { '123456' }
let(:issue_id_with_reserved_chars) { '123$%' }
let(:escaped_issue_id) { '123%24%25' }
- let(:api_urls) { Sentry::ApiUrls.new(sentry_url) }
+ let(:api_urls) { described_class.new(sentry_url) }
# Sentry API returns 404 if there are extra slashes in the URL!
shared_examples 'correct url with extra slashes' do
diff --git a/spec/lib/sentry/client/event_spec.rb b/spec/lib/error_tracking/sentry_client/event_spec.rb
index 07ed331c44c..64e674f1e9b 100644
--- a/spec/lib/sentry/client/event_spec.rb
+++ b/spec/lib/error_tracking/sentry_client/event_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Sentry::Client do
+RSpec.describe ErrorTracking::SentryClient do
include SentryClientHelpers
let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' }
diff --git a/spec/lib/sentry/client/issue_link_spec.rb b/spec/lib/error_tracking/sentry_client/issue_link_spec.rb
index fe3abe7cb23..f86d328ef89 100644
--- a/spec/lib/sentry/client/issue_link_spec.rb
+++ b/spec/lib/error_tracking/sentry_client/issue_link_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Sentry::Client::IssueLink do
+RSpec.describe ErrorTracking::SentryClient::IssueLink do
include SentryClientHelpers
let_it_be(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' }
diff --git a/spec/lib/sentry/client/issue_spec.rb b/spec/lib/error_tracking/sentry_client/issue_spec.rb
index dedef905c95..e54296c58e0 100644
--- a/spec/lib/sentry/client/issue_spec.rb
+++ b/spec/lib/error_tracking/sentry_client/issue_spec.rb
@@ -2,12 +2,12 @@
require 'spec_helper'
-RSpec.describe Sentry::Client::Issue do
+RSpec.describe ErrorTracking::SentryClient::Issue do
include SentryClientHelpers
let(:token) { 'test-token' }
let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0' }
- let(:client) { Sentry::Client.new(sentry_url, token) }
+ let(:client) { ErrorTracking::SentryClient.new(sentry_url, token) }
let(:issue_id) { 11 }
describe '#list_issues' do
@@ -136,7 +136,7 @@ RSpec.describe Sentry::Client::Issue do
subject { client.list_issues(issue_status: issue_status, limit: limit, sort: 'fish') }
it 'throws an error' do
- expect { subject }.to raise_error(Sentry::Client::BadRequestError, 'Invalid value for sort param')
+ expect { subject }.to raise_error(ErrorTracking::SentryClient::BadRequestError, 'Invalid value for sort param')
end
end
@@ -164,7 +164,7 @@ RSpec.describe Sentry::Client::Issue do
end
it 'raises exception' do
- expect { subject }.to raise_error(Sentry::Client::MissingKeysError, 'Sentry API response is missing keys. key not found: "id"')
+ expect { subject }.to raise_error(ErrorTracking::SentryClient::MissingKeysError, 'Sentry API response is missing keys. key not found: "id"')
end
end
@@ -173,7 +173,7 @@ RSpec.describe Sentry::Client::Issue do
deep_size = double('Gitlab::Utils::DeepSize', valid?: false)
allow(Gitlab::Utils::DeepSize).to receive(:new).with(sentry_api_response).and_return(deep_size)
- expect { subject }.to raise_error(Sentry::Client::ResponseInvalidSizeError, 'Sentry API response is too big. Limit is 1 MB.')
+ expect { subject }.to raise_error(ErrorTracking::SentryClient::ResponseInvalidSizeError, 'Sentry API response is too big. Limit is 1 MB.')
end
end
diff --git a/spec/lib/sentry/pagination_parser_spec.rb b/spec/lib/error_tracking/sentry_client/pagination_parser_spec.rb
index c4ed24827bb..c4b771d5b93 100644
--- a/spec/lib/sentry/pagination_parser_spec.rb
+++ b/spec/lib/error_tracking/sentry_client/pagination_parser_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Sentry::PaginationParser do
+RSpec.describe ErrorTracking::SentryClient::PaginationParser do
describe '.parse' do
subject { described_class.parse(headers) }
diff --git a/spec/lib/sentry/client/projects_spec.rb b/spec/lib/error_tracking/sentry_client/projects_spec.rb
index ea2c5ccb81e..247f9c1c085 100644
--- a/spec/lib/sentry/client/projects_spec.rb
+++ b/spec/lib/error_tracking/sentry_client/projects_spec.rb
@@ -2,12 +2,12 @@
require 'spec_helper'
-RSpec.describe Sentry::Client::Projects do
+RSpec.describe ErrorTracking::SentryClient::Projects do
include SentryClientHelpers
let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' }
let(:token) { 'test-token' }
- let(:client) { Sentry::Client.new(sentry_url, token) }
+ let(:client) { ErrorTracking::SentryClient.new(sentry_url, token) }
let(:projects_sample_response) do
Gitlab::Utils.deep_indifferent_access(
Gitlab::Json.parse(fixture_file('sentry/list_projects_sample_response.json'))
@@ -44,7 +44,7 @@ RSpec.describe Sentry::Client::Projects do
end
it 'raises exception' do
- expect { subject }.to raise_error(Sentry::Client::MissingKeysError, 'Sentry API response is missing keys. key not found: "slug"')
+ expect { subject }.to raise_error(ErrorTracking::SentryClient::MissingKeysError, 'Sentry API response is missing keys. key not found: "slug"')
end
end
diff --git a/spec/lib/sentry/client/repo_spec.rb b/spec/lib/error_tracking/sentry_client/repo_spec.rb
index 956c0b6eee1..9a1c7a69c3d 100644
--- a/spec/lib/sentry/client/repo_spec.rb
+++ b/spec/lib/error_tracking/sentry_client/repo_spec.rb
@@ -2,12 +2,12 @@
require 'spec_helper'
-RSpec.describe Sentry::Client::Repo do
+RSpec.describe ErrorTracking::SentryClient::Repo do
include SentryClientHelpers
let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' }
let(:token) { 'test-token' }
- let(:client) { Sentry::Client.new(sentry_url, token) }
+ let(:client) { ErrorTracking::SentryClient.new(sentry_url, token) }
let(:repos_sample_response) { Gitlab::Json.parse(fixture_file('sentry/repos_sample_response.json')) }
describe '#repos' do
diff --git a/spec/lib/sentry/client_spec.rb b/spec/lib/error_tracking/sentry_client_spec.rb
index cddcb6e98fa..9ffd756f057 100644
--- a/spec/lib/sentry/client_spec.rb
+++ b/spec/lib/error_tracking/sentry_client_spec.rb
@@ -2,11 +2,11 @@
require 'spec_helper'
-RSpec.describe Sentry::Client do
+RSpec.describe ErrorTracking::SentryClient do
let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' }
let(:token) { 'test-token' }
- subject { Sentry::Client.new(sentry_url, token) }
+ subject { described_class.new(sentry_url, token) }
it { is_expected.to respond_to :projects }
it { is_expected.to respond_to :list_issues }
diff --git a/spec/mailers/emails/merge_requests_spec.rb b/spec/mailers/emails/merge_requests_spec.rb
index 5ab0265203e..1a61d3c4362 100644
--- a/spec/mailers/emails/merge_requests_spec.rb
+++ b/spec/mailers/emails/merge_requests_spec.rb
@@ -6,7 +6,8 @@ require 'email_spec'
RSpec.describe Emails::MergeRequests do
include EmailSpec::Matchers
- let_it_be(:recipient) { create(:user) }
+ include_context 'gitlab email notification'
+
let_it_be(:current_user) { create(:user) }
let_it_be(:assignee, reload: true) { create(:user, email: 'assignee@example.com', name: 'John Doe') }
let_it_be(:reviewer, reload: true) { create(:user, email: 'reviewer@example.com', name: 'Jane Doe') }
@@ -20,6 +21,42 @@ RSpec.describe Emails::MergeRequests do
description: 'Awesome description')
end
+ let(:recipient) { assignee }
+
+ describe '#merged_merge_request_email' do
+ let(:merge_author) { assignee }
+
+ subject { Notify.merged_merge_request_email(recipient.id, merge_request.id, merge_author.id) }
+
+ it_behaves_like 'a multiple recipients email'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { merge_request }
+ end
+
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like 'an unsubscribeable thread'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
+
+ it 'is sent as the merge author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(merge_author.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
+
+ it 'has the correct subject and body' do
+ aggregate_failures do
+ is_expected.to have_referable_subject(merge_request, reply: true)
+ is_expected.to have_body_text('merged')
+ is_expected.to have_body_text(project_merge_request_path(project, merge_request))
+ is_expected.to have_link(merge_request.to_reference, href: project_merge_request_url(merge_request.target_project, merge_request))
+
+ expect(subject.text_part).to have_content(assignee.name)
+ expect(subject.text_part).to have_content(reviewer.name)
+ end
+ end
+ end
+
describe "#merge_when_pipeline_succeeds_email" do
let(:title) { "Merge request #{merge_request.to_reference} was scheduled to merge after pipeline succeeds by #{current_user.name}" }
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index a965980271b..762c57615b8 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -495,37 +495,6 @@ RSpec.describe Notify do
end
end
- describe 'that are merged' do
- let(:merge_author) { create(:user) }
-
- subject { described_class.merged_merge_request_email(recipient.id, merge_request.id, merge_author.id) }
-
- it_behaves_like 'a multiple recipients email'
- it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
- let(:model) { merge_request }
- end
-
- it_behaves_like 'it should show Gmail Actions View Merge request link'
- it_behaves_like 'an unsubscribeable thread'
- it_behaves_like 'appearance header and footer enabled'
- it_behaves_like 'appearance header and footer not enabled'
-
- it 'is sent as the merge author' do
- sender = subject.header[:from].addrs[0]
- expect(sender.display_name).to eq(merge_author.name)
- expect(sender.address).to eq(gitlab_sender)
- end
-
- it 'has the correct subject and body' do
- aggregate_failures do
- is_expected.to have_referable_subject(merge_request, reply: true)
- is_expected.to have_body_text('merged')
- is_expected.to have_body_text(project_merge_request_path(project, merge_request))
- is_expected.to have_link(merge_request.to_reference, href: project_merge_request_url(merge_request.target_project, merge_request))
- end
- end
- end
-
describe 'that are unmergeable' do
let_it_be(:merge_request) do
create(:merge_request, :conflict,
diff --git a/spec/models/bulk_imports/entity_spec.rb b/spec/models/bulk_imports/entity_spec.rb
index 6c9dc061430..17ab4d5954c 100644
--- a/spec/models/bulk_imports/entity_spec.rb
+++ b/spec/models/bulk_imports/entity_spec.rb
@@ -189,20 +189,4 @@ RSpec.describe BulkImports::Entity, type: :model do
expect(entity.next_page_for(:relation)).to eq('nextPage')
end
end
-
- describe 'caching', :clean_gitlab_redis_cache do
- let(:entity) { create(:bulk_import_entity, :started) }
-
- it 'removes entity cache keys' do
- cache_key = "bulk_import:#{entity.bulk_import.id}:entity:#{entity.id}:relation:1"
-
- Gitlab::Redis::Cache.with do |redis|
- redis.set(cache_key, 1)
-
- expect(redis).to receive(:del).with(cache_key)
- end
-
- entity.finish!
- end
- end
end
diff --git a/spec/models/error_tracking/project_error_tracking_setting_spec.rb b/spec/models/error_tracking/project_error_tracking_setting_spec.rb
index 72ed11f6c74..3ae0666f7d0 100644
--- a/spec/models/error_tracking/project_error_tracking_setting_spec.rb
+++ b/spec/models/error_tracking/project_error_tracking_setting_spec.rb
@@ -111,7 +111,7 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
describe '#sentry_client' do
it 'returns sentry client' do
- expect(subject.sentry_client).to be_a(Sentry::Client)
+ expect(subject.sentry_client).to be_a(ErrorTracking::SentryClient)
end
end
@@ -152,7 +152,7 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
end
end
- context 'when sentry client raises Sentry::Client::Error' do
+ context 'when sentry client raises ErrorTracking::SentryClient::Error' do
let(:sentry_client) { spy(:sentry_client) }
before do
@@ -160,7 +160,7 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
allow(subject).to receive(:sentry_client).and_return(sentry_client)
allow(sentry_client).to receive(:list_issues).with(opts)
- .and_raise(Sentry::Client::Error, 'error message')
+ .and_raise(ErrorTracking::SentryClient::Error, 'error message')
end
it 'returns error' do
@@ -171,7 +171,7 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
end
end
- context 'when sentry client raises Sentry::Client::MissingKeysError' do
+ context 'when sentry client raises ErrorTracking::SentryClient::MissingKeysError' do
let(:sentry_client) { spy(:sentry_client) }
before do
@@ -179,7 +179,7 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
allow(subject).to receive(:sentry_client).and_return(sentry_client)
allow(sentry_client).to receive(:list_issues).with(opts)
- .and_raise(Sentry::Client::MissingKeysError, 'Sentry API response is missing keys. key not found: "id"')
+ .and_raise(ErrorTracking::SentryClient::MissingKeysError, 'Sentry API response is missing keys. key not found: "id"')
end
it 'returns error' do
@@ -190,7 +190,7 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
end
end
- context 'when sentry client raises Sentry::Client::ResponseInvalidSizeError' do
+ context 'when sentry client raises ErrorTracking::SentryClient::ResponseInvalidSizeError' do
let(:sentry_client) { spy(:sentry_client) }
let(:error_msg) {"Sentry API response is too big. Limit is #{Gitlab::Utils::DeepSize.human_default_max_size}."}
@@ -199,7 +199,7 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
allow(subject).to receive(:sentry_client).and_return(sentry_client)
allow(sentry_client).to receive(:list_issues).with(opts)
- .and_raise(Sentry::Client::ResponseInvalidSizeError, error_msg)
+ .and_raise(ErrorTracking::SentryClient::ResponseInvalidSizeError, error_msg)
end
it 'returns error' do
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index f22ee7b1686..551da6d0471 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -2885,11 +2885,6 @@ RSpec.describe MergeRequest, factory_default: :keep do
describe '#mergeable?' do
subject { build_stubbed(:merge_request) }
- it 'returns false if still preparing' do
- expect(subject).to receive(:preparing?) { true }
- expect(subject.mergeable?).to be_falsey
- end
-
it 'returns false if #mergeable_state? is false' do
expect(subject).to receive(:mergeable_state?) { false }
@@ -3080,7 +3075,6 @@ RSpec.describe MergeRequest, factory_default: :keep do
subject { build(:merge_request, merge_status: status) }
where(:status, :public_status) do
- 'preparing' | 'checking'
'cannot_be_merged_rechecking' | 'checking'
'checking' | 'checking'
'cannot_be_merged' | 'cannot_be_merged'
@@ -4136,13 +4130,6 @@ RSpec.describe MergeRequest, factory_default: :keep do
include_examples 'for an valid state transition'
end
- context 'when the status is preparing' do
- let(:merge_status) { :preparing }
- let(:expected_merge_status) { 'unchecked' }
-
- include_examples 'for an valid state transition'
- end
-
context 'when the status is can_be_merged' do
let(:merge_status) { :can_be_merged }
let(:expected_merge_status) { 'unchecked' }
diff --git a/spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb b/spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb
index 6141a172253..f637ca98353 100644
--- a/spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb
+++ b/spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb
@@ -20,7 +20,9 @@ RSpec.describe 'Create an alert issue from an alert' do
errors
alert {
iid
- issueIid
+ issue {
+ iid
+ }
}
issue {
iid
@@ -46,7 +48,7 @@ RSpec.describe 'Create an alert issue from an alert' do
expect(mutation_response.slice('alert', 'issue')).to eq(
'alert' => {
'iid' => alert.iid.to_s,
- 'issueIid' => new_issue.iid.to_s
+ 'issue' => { 'iid' => new_issue.iid.to_s }
},
'issue' => {
'iid' => new_issue.iid.to_s,
diff --git a/spec/requests/api/graphql/project/alert_management/alert/issue_spec.rb b/spec/requests/api/graphql/project/alert_management/alert/issue_spec.rb
new file mode 100644
index 00000000000..9724de4fedb
--- /dev/null
+++ b/spec/requests/api/graphql/project/alert_management/alert/issue_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting Alert Management Alert Issue' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:current_user) { create(:user) }
+ let(:payload) { {} }
+ let(:query) { 'avg(metric) > 1.0' }
+
+ let(:fields) do
+ <<~QUERY
+ nodes {
+ iid
+ issue {
+ iid
+ state
+ }
+ }
+ QUERY
+ end
+
+ let(:graphql_query) do
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ query_graphql_field('alertManagementAlerts', {}, fields)
+ )
+ end
+
+ let(:alerts) { graphql_data.dig('project', 'alertManagementAlerts', 'nodes') }
+ let(:first_alert) { alerts.first }
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ context 'with gitlab alert' do
+ before do
+ create(:alert_management_alert, :with_issue, project: project, payload: payload)
+ end
+
+ it 'includes the correct alert issue payload data' do
+ post_graphql(graphql_query, current_user: current_user)
+
+ expect(first_alert).to include('issue' => { "iid" => "1", "state" => "opened" })
+ end
+ end
+
+ describe 'performance' do
+ let(:first_n) { var('Int') }
+ let(:params) { { first: first_n } }
+ let(:limited_query) { with_signature([first_n], query) }
+
+ context 'with gitlab alert' do
+ before do
+ create(:alert_management_alert, :with_issue, project: project, payload: payload)
+ end
+
+ it 'avoids N+1 queries' do
+ base_count = ActiveRecord::QueryRecorder.new do
+ post_graphql(limited_query, current_user: current_user, variables: first_n.with(1))
+ end
+
+ expect { post_graphql(limited_query, current_user: current_user) }.not_to exceed_query_limit(base_count)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb
index 8deed75a466..fe77d9dc86d 100644
--- a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb
+++ b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe 'getting Alert Management Alerts' do
let_it_be(:payload) { { 'custom' => { 'alert' => 'payload' }, 'runbook' => 'runbook' } }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:current_user) { create(:user) }
- let_it_be(:resolved_alert) { create(:alert_management_alert, :all_fields, :resolved, project: project, issue: nil, severity: :low).present }
+ let_it_be(:resolved_alert) { create(:alert_management_alert, :all_fields, :resolved, project: project, severity: :low).present }
let_it_be(:triggered_alert) { create(:alert_management_alert, :all_fields, project: project, severity: :critical, payload: payload).present }
let_it_be(:other_project_alert) { create(:alert_management_alert, :all_fields).present }
@@ -60,7 +60,6 @@ RSpec.describe 'getting Alert Management Alerts' do
it 'returns the correct properties of the alerts' do
expect(first_alert).to include(
'iid' => triggered_alert.iid.to_s,
- 'issueIid' => triggered_alert.issue_iid.to_s,
'title' => triggered_alert.title,
'description' => triggered_alert.description,
'severity' => triggered_alert.severity.upcase,
@@ -82,7 +81,6 @@ RSpec.describe 'getting Alert Management Alerts' do
expect(second_alert).to include(
'iid' => resolved_alert.iid.to_s,
- 'issueIid' => resolved_alert.issue_iid.to_s,
'status' => 'RESOLVED',
'endedAt' => resolved_alert.ended_at.strftime('%Y-%m-%dT%H:%M:%SZ')
)
diff --git a/spec/requests/api/invitations_spec.rb b/spec/requests/api/invitations_spec.rb
index 2ea237469b1..a40eae8fdaf 100644
--- a/spec/requests/api/invitations_spec.rb
+++ b/spec/requests/api/invitations_spec.rb
@@ -3,14 +3,14 @@
require 'spec_helper'
RSpec.describe API::Invitations do
- let(:maintainer) { create(:user, username: 'maintainer_user') }
- let(:developer) { create(:user) }
- let(:access_requester) { create(:user) }
- let(:stranger) { create(:user) }
+ let_it_be(:maintainer) { create(:user, username: 'maintainer_user') }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:access_requester) { create(:user) }
+ let_it_be(:stranger) { create(:user) }
let(:email) { 'email1@example.com' }
let(:email2) { 'email2@example.com' }
- let(:project) do
+ let_it_be(:project) do
create(:project, :public, creator_id: maintainer.id, namespace: maintainer.namespace) do |project|
project.add_developer(developer)
project.add_maintainer(maintainer)
@@ -18,7 +18,7 @@ RSpec.describe API::Invitations do
end
end
- let!(:group) do
+ let_it_be(:group, reload: true) do
create(:group, :public) do |group|
group.add_developer(developer)
group.add_owner(maintainer)
diff --git a/spec/services/members/invite_service_spec.rb b/spec/services/members/invite_service_spec.rb
index 08cdf0d3ae1..cced93896a5 100644
--- a/spec/services/members/invite_service_spec.rb
+++ b/spec/services/members/invite_service_spec.rb
@@ -2,76 +2,155 @@
require 'spec_helper'
-RSpec.describe Members::InviteService do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
- let(:project_user) { create(:user) }
-
- before do
- project.add_maintainer(user)
+RSpec.describe Members::InviteService, :aggregate_failures do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { project.owner }
+ let_it_be(:project_user) { create(:user) }
+ let(:params) { {} }
+ let(:base_params) { { access_level: Gitlab::Access::GUEST } }
+
+ subject(:result) { described_class.new(user, base_params.merge(params)).execute(project) }
+
+ context 'when email is previously unused by current members' do
+ let(:params) { { email: 'email@example.org' } }
+
+ it 'successfully creates a member' do
+ expect { result }.to change(ProjectMember, :count).by(1)
+ expect(result[:status]).to eq(:success)
+ end
end
- it 'adds an existing user to members' do
- params = { email: project_user.email.to_s, access_level: Gitlab::Access::GUEST }
- result = described_class.new(user, params).execute(project)
+ context 'when emails are passed as an array' do
+ let(:params) { { email: %w[email@example.org email2@example.org] } }
- expect(result[:status]).to eq(:success)
- expect(project.users).to include project_user
+ it 'successfully creates members' do
+ expect { result }.to change(ProjectMember, :count).by(2)
+ expect(result[:status]).to eq(:success)
+ end
end
- it 'creates a new user for an unknown email address' do
- params = { email: 'email@example.org', access_level: Gitlab::Access::GUEST }
- result = described_class.new(user, params).execute(project)
+ context 'when emails are passed as an empty string' do
+ let(:params) { { email: '' } }
- expect(result[:status]).to eq(:success)
+ it 'returns an error' do
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('Email cannot be blank')
+ end
end
- it 'limits the number of emails to 100' do
- emails = Array.new(101).map { |n| "email#{n}@example.com" }
- params = { email: emails, access_level: Gitlab::Access::GUEST }
+ context 'when email param is not included' do
+ it 'returns an error' do
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('Email cannot be blank')
+ end
+ end
- result = described_class.new(user, params).execute(project)
+ context 'when email is not a valid email' do
+ let(:params) { { email: '_bogus_' } }
- expect(result[:status]).to eq(:error)
- expect(result[:message]).to eq('Too many users specified (limit is 100)')
+ it 'returns an error' do
+ expect { result }.not_to change(ProjectMember, :count)
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]['_bogus_']).to eq("Invite email is invalid")
+ end
end
- it 'does not invite an invalid email' do
- params = { email: project_user.id.to_s, access_level: Gitlab::Access::GUEST }
- result = described_class.new(user, params).execute(project)
+ context 'when duplicate email addresses are passed' do
+ let(:params) { { email: 'email@example.org,email@example.org' } }
+
+ it 'only creates one member per unique address' do
+ expect { result }.to change(ProjectMember, :count).by(1)
+ expect(result[:status]).to eq(:success)
+ end
+ end
- expect(result[:status]).to eq(:error)
- expect(result[:message][project_user.id.to_s]).to eq("Invite email is invalid")
- expect(project.users).not_to include project_user
+ context 'when observing email limits' do
+ let_it_be(:emails) { Array(1..101).map { |n| "email#{n}@example.com" } }
+
+ context 'when over the allowed default limit of emails' do
+ let(:params) { { email: emails } }
+
+ it 'limits the number of emails to 100' do
+ expect { result }.not_to change(ProjectMember, :count)
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('Too many users specified (limit is 100)')
+ end
+ end
+
+ context 'when over the allowed custom limit of emails' do
+ let(:params) { { email: 'email@example.org,email2@example.org', limit: 1 } }
+
+ it 'limits the number of emails to the limit supplied' do
+ expect { result }.not_to change(ProjectMember, :count)
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('Too many users specified (limit is 1)')
+ end
+ end
+
+ context 'when limit allowed is disabled via limit param' do
+ let(:params) { { email: emails, limit: -1 } }
+
+ it 'does not limit number of emails' do
+ expect { result }.to change(ProjectMember, :count).by(101)
+ expect(result[:status]).to eq(:success)
+ end
+ end
end
- it 'does not invite to an invalid access level' do
- params = { email: project_user.email, access_level: -1 }
- result = described_class.new(user, params).execute(project)
+ context 'when email belongs to an existing user' do
+ let(:params) { { email: project_user.email } }
- expect(result[:status]).to eq(:error)
- expect(result[:message][project_user.email]).to eq("Access level is not included in the list")
+ it 'adds an existing user to members' do
+ expect { result }.to change(ProjectMember, :count).by(1)
+ expect(result[:status]).to eq(:success)
+ expect(project.users).to include project_user
+ end
end
- it 'does not add a member with an existing invite' do
- invited_member = create(:project_member, :invited, project: project)
+ context 'when access level is not valid' do
+ let(:params) { { email: project_user.email, access_level: -1 } }
- params = { email: invited_member.invite_email,
- access_level: Gitlab::Access::GUEST }
- result = described_class.new(user, params).execute(project)
+ it 'returns an error' do
+ expect { result }.not_to change(ProjectMember, :count)
+ expect(result[:status]).to eq(:error)
+ expect(result[:message][project_user.email]).to eq("Access level is not included in the list")
+ end
+ end
+
+ context 'when invite already exists for an included email' do
+ let!(:invited_member) { create(:project_member, :invited, project: project) }
+ let(:params) { { email: "#{invited_member.invite_email},#{project_user.email}" } }
- expect(result[:status]).to eq(:error)
- expect(result[:message][invited_member.invite_email]).to eq("Member already invited to #{project.name}")
+ it 'adds new email and returns an error for the already invited email' do
+ expect { result }.to change(ProjectMember, :count).by(1)
+ expect(result[:status]).to eq(:error)
+ expect(result[:message][invited_member.invite_email]).to eq("Member already invited to #{project.name}")
+ expect(project.users).to include project_user
+ end
end
- it 'does not add a member with an access_request' do
- requested_member = create(:project_member, :access_request, project: project)
+ context 'when access request already exists for an included email' do
+ let!(:requested_member) { create(:project_member, :access_request, project: project) }
+ let(:params) { { email: "#{requested_member.user.email},#{project_user.email}" } }
+
+ it 'adds new email and returns an error for the already invited email' do
+ expect { result }.to change(ProjectMember, :count).by(1)
+ expect(result[:status]).to eq(:error)
+ expect(result[:message][requested_member.user.email])
+ .to eq("Member cannot be invited because they already requested to join #{project.name}")
+ expect(project.users).to include project_user
+ end
+ end
- params = { email: requested_member.user.email,
- access_level: Gitlab::Access::GUEST }
- result = described_class.new(user, params).execute(project)
+ context 'when email is already a member on the project' do
+ let!(:existing_member) { create(:project_member, :guest, project: project) }
+ let(:params) { { email: "#{existing_member.user.email},#{project_user.email}" } }
- expect(result[:status]).to eq(:error)
- expect(result[:message][requested_member.user.email]).to eq("Member cannot be invited because they already requested to join #{project.name}")
+ it 'adds new email and returns an error for the already invited email' do
+ expect { result }.to change(ProjectMember, :count).by(1)
+ expect(result[:status]).to eq(:error)
+ expect(result[:message][existing_member.user.email]).to eq("Already a member of #{project.name}")
+ expect(project.users).to include project_user
+ end
end
end
diff --git a/spec/services/merge_requests/after_create_service_spec.rb b/spec/services/merge_requests/after_create_service_spec.rb
index 22af346634f..f21feb70bc5 100644
--- a/spec/services/merge_requests/after_create_service_spec.rb
+++ b/spec/services/merge_requests/after_create_service_spec.rb
@@ -67,27 +67,5 @@ RSpec.describe MergeRequests::AfterCreateService do
it_behaves_like 'records an onboarding progress action', :merge_request_created do
let(:namespace) { merge_request.target_project.namespace }
end
-
- context 'when merge request is in unchecked state' do
- before do
- merge_request.mark_as_unchecked!
- execute_service
- end
-
- it 'does not change its state' do
- expect(merge_request.reload).to be_unchecked
- end
- end
-
- context 'when merge request is in preparing state' do
- before do
- merge_request.mark_as_preparing!
- execute_service
- end
-
- it 'marks the merge request as unchecked' do
- expect(merge_request.reload).to be_unchecked
- end
- end
end
end
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index e63e678bdd8..4f47a22b07c 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -67,10 +67,6 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do
expect(Event.where(attributes).count).to eq(1)
end
- it 'sets the merge_status to preparing' do
- expect(merge_request.reload).to be_preparing
- end
-
describe 'when marked with /wip' do
context 'in title and in description' do
let(:opts) do
diff --git a/spec/services/repositories/changelog_service_spec.rb b/spec/services/repositories/changelog_service_spec.rb
index 4ec692d678a..91bc248b20a 100644
--- a/spec/services/repositories/changelog_service_spec.rb
+++ b/spec/services/repositories/changelog_service_spec.rb
@@ -76,7 +76,7 @@ RSpec.describe Repositories::ChangelogService do
recorder = ActiveRecord::QueryRecorder.new { service.execute }
changelog = project.repository.blob_at('master', 'CHANGELOG.md')&.data
- expect(recorder.count).to eq(10)
+ expect(recorder.count).to eq(11)
expect(changelog).to include('Title 1', 'Title 2')
end
diff --git a/spec/support/helpers/javascript_fixtures_helpers.rb b/spec/support/helpers/javascript_fixtures_helpers.rb
index 2224af88ab9..09425c3742a 100644
--- a/spec/support/helpers/javascript_fixtures_helpers.rb
+++ b/spec/support/helpers/javascript_fixtures_helpers.rb
@@ -12,6 +12,7 @@ module JavaScriptFixturesHelpers
included do |base|
base.around do |example|
# pick an arbitrary date from the past, so tests are not time dependent
+ # Also see spec/frontend/__helpers__/fake_date/jest.js
Timecop.freeze(Time.utc(2015, 7, 3, 10)) { example.run }
raise NoMethodError.new('You need to set `response` for the fixture generator! This will automatically happen with `type: :controller` or `type: :request`.', 'response') unless respond_to?(:response)
diff --git a/spec/support/shared_contexts/features/error_tracking_shared_context.rb b/spec/support/shared_contexts/features/error_tracking_shared_context.rb
index 1f4eb3a6df9..f04111e0ce0 100644
--- a/spec/support/shared_contexts/features/error_tracking_shared_context.rb
+++ b/spec/support/shared_contexts/features/error_tracking_shared_context.rb
@@ -9,7 +9,7 @@ RSpec.shared_context 'sentry error tracking context feature' do
let_it_be(:issue_response) { Gitlab::Json.parse(issue_response_body) }
let_it_be(:event_response_body) { fixture_file('sentry/issue_latest_event_sample_response.json') }
let_it_be(:event_response) { Gitlab::Json.parse(event_response_body) }
- let(:sentry_api_urls) { Sentry::ApiUrls.new(project_error_tracking_settings.api_url) }
+ let(:sentry_api_urls) { ErrorTracking::SentryClient::ApiUrls.new(project_error_tracking_settings.api_url) }
let(:issue_id) { issue_response['id'] }
let(:issue_seen) { 1.year.ago.utc }
let(:formatted_issue_seen) { issue_seen.strftime("%Y-%m-%d %-l:%M:%S%p %Z") }
diff --git a/spec/support/shared_examples/lib/sentry/client_shared_examples.rb b/spec/support/shared_examples/lib/sentry/client_shared_examples.rb
index 4221708b55c..d73c7b6848d 100644
--- a/spec/support/shared_examples/lib/sentry/client_shared_examples.rb
+++ b/spec/support/shared_examples/lib/sentry/client_shared_examples.rb
@@ -26,7 +26,7 @@ RSpec.shared_examples 'no Sentry redirects' do |http_method|
end
it 'does not follow redirects' do
- expect { subject }.to raise_exception(Sentry::Client::Error, 'Sentry response status code: 302')
+ expect { subject }.to raise_exception(ErrorTracking::SentryClient::Error, 'Sentry response status code: 302')
expect(redirect_req_stub).to have_been_requested
expect(redirected_req_stub).not_to have_been_requested
end
@@ -53,7 +53,7 @@ RSpec.shared_examples 'maps Sentry exceptions' do |http_method|
it do
expect { subject }
- .to raise_exception(Sentry::Client::Error, message)
+ .to raise_exception(ErrorTracking::SentryClient::Error, message)
end
end
end
diff --git a/spec/workers/error_tracking_issue_link_worker_spec.rb b/spec/workers/error_tracking_issue_link_worker_spec.rb
index 5be568c2dad..90e747c8788 100644
--- a/spec/workers/error_tracking_issue_link_worker_spec.rb
+++ b/spec/workers/error_tracking_issue_link_worker_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe ErrorTrackingIssueLinkWorker do
describe '#perform' do
it 'creates a link between an issue and a Sentry issue in Sentry' do
- expect_next_instance_of(Sentry::Client) do |client|
+ expect_next_instance_of(ErrorTracking::SentryClient) do |client|
expect(client).to receive(:repos).with('sentry-org').and_return([repo])
expect(client)
.to receive(:create_issue_link)
@@ -33,8 +33,8 @@ RSpec.describe ErrorTrackingIssueLinkWorker do
shared_examples_for 'makes no external API requests' do
it 'takes no action' do
- expect_any_instance_of(Sentry::Client).not_to receive(:repos)
- expect_any_instance_of(Sentry::Client).not_to receive(:create_issue_link)
+ expect_any_instance_of(ErrorTracking::SentryClient).not_to receive(:repos)
+ expect_any_instance_of(ErrorTracking::SentryClient).not_to receive(:create_issue_link)
expect(subject).to be nil
end
@@ -42,7 +42,7 @@ RSpec.describe ErrorTrackingIssueLinkWorker do
shared_examples_for 'attempts to create a link via plugin' do
it 'takes no action' do
- expect_next_instance_of(Sentry::Client) do |client|
+ expect_next_instance_of(ErrorTracking::SentryClient) do |client|
expect(client).to receive(:repos).with('sentry-org').and_return([repo])
expect(client)
.to receive(:create_issue_link)
@@ -98,8 +98,8 @@ RSpec.describe ErrorTrackingIssueLinkWorker do
context 'when Sentry repos request errors' do
it 'falls back to creating a link via plugin' do
- expect_next_instance_of(Sentry::Client) do |client|
- expect(client).to receive(:repos).with('sentry-org').and_raise(Sentry::Client::Error)
+ expect_next_instance_of(ErrorTracking::SentryClient) do |client|
+ expect(client).to receive(:repos).with('sentry-org').and_raise(ErrorTracking::SentryClient::Error)
expect(client)
.to receive(:create_issue_link)
.with(nil, sentry_issue.sentry_issue_identifier, issue)
diff --git a/vendor/gitignore/C++.gitignore b/vendor/gitignore/C++.gitignore
index 259148fa18f..259148fa18f 100755..100644
--- a/vendor/gitignore/C++.gitignore
+++ b/vendor/gitignore/C++.gitignore
diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore
index a1c2a238a96..a1c2a238a96 100755..100644
--- a/vendor/gitignore/Java.gitignore
+++ b/vendor/gitignore/Java.gitignore