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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/gitlab-gems.gitlab-ci.yml7
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml15
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml9
-rw-r--r--.rubocop.yml1
-rw-r--r--.rubocop_todo/search/namespaced_class.yml1
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile3
-rw-r--r--Gemfile.checksum10
-rw-r--r--Gemfile.lock11
-rw-r--r--app/assets/javascripts/error_tracking/components/error_details.vue7
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue34
-rw-r--r--app/assets/javascripts/error_tracking/components/timeline_chart.vue129
-rw-r--r--app/assets/javascripts/error_tracking/queries/details.query.graphql4
-rw-r--r--app/assets/javascripts/members/components/table/role_dropdown.vue63
-rw-r--r--app/assets/javascripts/profile/components/user_achievements.vue69
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_add_note.vue6
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.fragment.graphql1
-rw-r--r--app/services/merge_requests/refresh_service.rb8
-rw-r--r--config/feature_flags/development/fix_interrupted_mwps.yml8
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--db/fixtures/development/17_cycle_analytics.rb46
-rw-r--r--doc/ci/environments/index.md26
-rw-r--r--doc/ci/img/environments_monitoring.pngbin45153 -> 0 bytes
-rw-r--r--doc/development/gems.md2
-rw-r--r--doc/topics/autodevops/img/auto_monitoring.pngbin26675 -> 0 bytes
-rw-r--r--doc/topics/autodevops/index.md1
-rw-r--r--doc/topics/autodevops/prepare_deployment.md3
-rw-r--r--doc/topics/autodevops/requirements.md15
-rw-r--r--doc/topics/autodevops/stages.md28
-rw-r--r--gems/gitlab-rspec/.gitignore11
-rw-r--r--gems/gitlab-rspec/.gitlab-ci.yml31
-rw-r--r--gems/gitlab-rspec/.rspec3
-rw-r--r--gems/gitlab-rspec/.rubocop.yml14
-rw-r--r--gems/gitlab-rspec/Gemfile18
-rw-r--r--gems/gitlab-rspec/Gemfile.lock180
-rw-r--r--gems/gitlab-rspec/gitlab-rspec.gemspec28
-rw-r--r--gems/gitlab-rspec/lib/gitlab/rspec.rb4
-rw-r--r--gems/gitlab-rspec/lib/gitlab/rspec/all.rb4
-rw-r--r--gems/gitlab-rspec/lib/gitlab/rspec/stub_env.rb (renamed from spec/support/helpers/stub_env.rb)0
-rw-r--r--gems/gitlab-rspec/lib/gitlab/rspec/version.rb9
-rw-r--r--gems/gitlab-rspec/spec/spec_helper.rb15
-rw-r--r--lib/gitlab/email/handler/service_desk_handler.rb24
-rw-r--r--locale/gitlab.pot6
-rw-r--r--qa/Dockerfile1
-rw-r--r--sidekiq_cluster/cli.rb2
-rw-r--r--spec/contracts/provider/spec_helper.rb2
-rw-r--r--spec/fast_spec_helper.rb1
-rw-r--r--spec/features/groups/members/manage_groups_spec.rb4
-rw-r--r--spec/features/groups/members/manage_members_spec.rb4
-rw-r--r--spec/features/projects/members/groups_with_access_list_spec.rb4
-rw-r--r--spec/features/projects/members/manage_members_spec.rb11
-rw-r--r--spec/frontend/admin/users/components/user_actions_spec.js2
-rw-r--r--spec/frontend/error_tracking/components/error_details_spec.js37
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_list_spec.js25
-rw-r--r--spec/frontend/error_tracking/components/list_mock.json38
-rw-r--r--spec/frontend/error_tracking/components/timeline_chart_spec.js94
-rw-r--r--spec/frontend/fixtures/users.rb3
-rw-r--r--spec/frontend/members/components/table/role_dropdown_spec.js96
-rw-r--r--spec/frontend/profile/components/user_achievements_spec.js9
-rw-r--r--spec/frontend/work_items/components/notes/work_item_add_note_spec.js13
-rw-r--r--spec/frontend/work_items/mock_data.js13
-rw-r--r--spec/lib/gitlab/email/handler/service_desk_handler_spec.rb119
-rw-r--r--spec/lib/gitlab/metrics/environment_spec.rb3
-rw-r--r--spec/requests/api/projects_spec.rb8
-rw-r--r--spec/scripts/generate_failed_package_and_test_mr_message_spec.rb2
-rw-r--r--spec/scripts/generate_message_to_run_e2e_pipeline_spec.rb2
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb57
-rw-r--r--spec/support/rspec.rb3
-rw-r--r--spec/support/shared_contexts/email_shared_context.rb5
-rw-r--r--spec/tooling/danger/project_helper_spec.rb2
-rw-r--r--spec/tooling/lib/tooling/find_changes_spec.rb2
-rw-r--r--spec/tooling/lib/tooling/find_tests_spec.rb2
-rw-r--r--spec/tooling/lib/tooling/gettext_extractor_spec.rb2
-rw-r--r--spec/tooling/lib/tooling/predictive_tests_spec.rb2
-rw-r--r--spec/tooling/rspec_flaky/config_spec.rb2
-rw-r--r--spec/tooling/rspec_flaky/flaky_example_spec.rb2
-rw-r--r--spec/tooling/rspec_flaky/listener_spec.rb2
77 files changed, 1183 insertions, 249 deletions
diff --git a/.gitlab/ci/gitlab-gems.gitlab-ci.yml b/.gitlab/ci/gitlab-gems.gitlab-ci.yml
new file mode 100644
index 00000000000..adab179d2a2
--- /dev/null
+++ b/.gitlab/ci/gitlab-gems.gitlab-ci.yml
@@ -0,0 +1,7 @@
+gems gitlab-rspec:
+ extends:
+ - .gems:rules:gitlab-rspec
+ needs: []
+ trigger:
+ include: gems/gitlab-rspec/.gitlab-ci.yml
+ strategy: depend
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index 0df6ef0cacd..d12b62ca0a8 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -326,6 +326,10 @@ rspec:artifact-collector remainder:
optional: true
- job: rspec background_migration pg14 # 4 jobs
optional: true
+ rules:
+ - !reference ['.rails:rules:ee-and-foss-integration', rules]
+ - !reference ['.rails:rules:ee-and-foss-migration', rules]
+ - !reference ['.rails:rules:ee-and-foss-background-migration', rules]
rspec:artifact-collector as-if-foss unit:
extends:
@@ -351,6 +355,10 @@ rspec:artifact-collector as-if-foss remainder:
optional: true
- job: rspec background_migration pg14-as-if-foss # 4 jobs
optional: true
+ rules:
+ - !reference ['.rails:rules:as-if-foss-integration', rules]
+ - !reference ['.rails:rules:as-if-foss-migration', rules]
+ - !reference ['.rails:rules:as-if-foss-background-migration', rules]
rspec:artifact-collector single-redis:
extends:
@@ -370,6 +378,7 @@ rspec:artifact-collector system single-redis:
rspec:artifact-collector ee single-redis:
extends:
- .artifact-collector
+ - .rails:rules:single-redis
needs:
- job: rspec-ee unit pg14 single-redis # 18 jobs
optional: true
@@ -392,6 +401,12 @@ rspec:artifact-collector ee:
optional: true
- job: rspec-ee system pg14 # 10 jobs
optional: true
+ rules:
+ - !reference ['.rails:rules:ee-only-migration', rules]
+ - !reference ['.rails:rules:ee-only-background-migration', rules]
+ - !reference ['.rails:rules:ee-only-unit', rules]
+ - !reference ['.rails:rules:ee-only-integration', rules]
+ - !reference ['.rails:rules:ee-only-system', rules]
rspec:coverage:
extends:
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index 1c1f6cfa96a..6313f5d5681 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -2128,6 +2128,15 @@
- <<: *if-merge-request
changes: *code-backstage-qa-patterns
+########################
+# GitLab monorepo gems #
+########################
+
+.gems:rules:gitlab-rspec:
+ rules:
+ - <<: *if-merge-request
+ changes: ["gems/gitlab-rspec/**/*"]
+
#######################
# Vendored gems rules #
#######################
diff --git a/.rubocop.yml b/.rubocop.yml
index 95240342be3..a7ebc87f4b3 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -675,7 +675,6 @@ RSpec/EnvMocking:
- 'ee/spec/**/fast_spec_helper.rb'
- 'spec/**/spec_helper.rb'
- 'ee/spec/**/spec_helper.rb'
- - 'spec/support/helpers/stub_env.rb'
RSpec/BeSuccessMatcher:
Enabled: true
diff --git a/.rubocop_todo/search/namespaced_class.yml b/.rubocop_todo/search/namespaced_class.yml
index 65890e096e3..ba2dbafb3c7 100644
--- a/.rubocop_todo/search/namespaced_class.yml
+++ b/.rubocop_todo/search/namespaced_class.yml
@@ -21,6 +21,7 @@ Search/NamespacedClass:
- 'ee/app/graphql/types/iteration_searchable_field_enum.rb'
- 'ee/app/helpers/ee/search_helper.rb'
- 'ee/app/models/concerns/elastic/application_versioned_search.rb'
+ - 'ee/app/models/concerns/elastic/maintain_elasticsearch_on_group_update.rb'
- 'ee/app/models/concerns/elastic/namespace_update.rb'
- 'ee/app/models/concerns/elastic/projects_search.rb'
- 'ee/app/models/concerns/elastic/repositories_search.rb'
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 2cced985770..eaf8c0c6be1 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-b967c1ee24740ab58d8111f0f46bd3fcfa3467a5
+40d411cfea1d40ae2525268896ae75fe026c3e9e
diff --git a/Gemfile b/Gemfile
index 75401ffc4dd..f14f85066b4 100644
--- a/Gemfile
+++ b/Gemfile
@@ -363,7 +363,7 @@ gem 'snowplow-tracker', '~> 0.8.0'
# Metrics
gem 'webrick', '~> 1.8.1', require: false
-gem 'prometheus-client-mmap', '~> 0.24', require: 'prometheus/client'
+gem 'prometheus-client-mmap', '~> 0.25', require: 'prometheus/client'
gem 'warning', '~> 1.3.0'
@@ -453,6 +453,7 @@ group :test do
gem 'rspec_profiling', '~> 0.0.6'
gem 'rspec-benchmark', '~> 0.6.0'
gem 'rspec-parameterized', '~> 1.0', require: false
+ gem 'gitlab-rspec', path: 'gems/gitlab-rspec'
gem 'capybara', '~> 3.39', '>= 3.39.1'
gem 'capybara-screenshot', '~> 1.0.26'
diff --git a/Gemfile.checksum b/Gemfile.checksum
index e5ed8ebcdf8..b2c07e5a201 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -452,11 +452,11 @@
{"name":"premailer","version":"1.16.0","platform":"ruby","checksum":"03e4402c448e6bae13fb5f6301a8bde4f3508e1bff90ae7c0972c7be94694786"},
{"name":"premailer-rails","version":"1.10.3","platform":"ruby","checksum":"7cdcb97027866f7a81c490c6d15ada7f39666b5f6375f0821b7e97e0483b112f"},
{"name":"proc_to_ast","version":"0.1.0","platform":"ruby","checksum":"92a73fa66e2250a83f8589f818b0751bcf227c68f85916202df7af85082f8691"},
-{"name":"prometheus-client-mmap","version":"0.24.5","platform":"aarch64-linux","checksum":"20977bd3dff89e2e4b5d7eb3420a1ea8a002a601390a08a1c0e5065a05c4a36c"},
-{"name":"prometheus-client-mmap","version":"0.24.5","platform":"arm64-darwin","checksum":"15d7e86d4595d24f2c5aa41a0b6d8564d78a1219bd01cba09ef721f304f64ebc"},
-{"name":"prometheus-client-mmap","version":"0.24.5","platform":"ruby","checksum":"a288fb5bd7551fb0008d77eb923e64ad4f583d56fec90de68efd75369d152676"},
-{"name":"prometheus-client-mmap","version":"0.24.5","platform":"x86_64-darwin","checksum":"7e3d1a79c3a1083e5f427399c5afc5316c207fd785c4b8a13134f7af50f0fd84"},
-{"name":"prometheus-client-mmap","version":"0.24.5","platform":"x86_64-linux","checksum":"09d9a3e5598886ff6ad34562745b79823a0bbf6ea59c22535d819ec32275f207"},
+{"name":"prometheus-client-mmap","version":"0.25.0","platform":"aarch64-linux","checksum":"0548954d183b0749ecd0983ea5e046a22137ae8accd5c86c64bc6d0ec57ef5c6"},
+{"name":"prometheus-client-mmap","version":"0.25.0","platform":"arm64-darwin","checksum":"3d5e5665d9de0488cf09663c5a2583470ab726cead90011b1b11571057507157"},
+{"name":"prometheus-client-mmap","version":"0.25.0","platform":"ruby","checksum":"d75dc28326633d780dc43f93be610253fab654ab35f876138cd35080b54fa092"},
+{"name":"prometheus-client-mmap","version":"0.25.0","platform":"x86_64-darwin","checksum":"c33859d23a5f925a76a79d63b50d94e5e181b4ed03b0f8836d23208aec655446"},
+{"name":"prometheus-client-mmap","version":"0.25.0","platform":"x86_64-linux","checksum":"2ea418c3ac327d28506d01898d9fa9cae967317244c39ebdb34680ac6cb0c1e9"},
{"name":"pry","version":"0.14.2","platform":"java","checksum":"fd780670977ba04ff7ee32dabd4d02fe4bf02e977afe8809832d5dca1412862e"},
{"name":"pry","version":"0.14.2","platform":"ruby","checksum":"c4fe54efedaca1d351280b45b8849af363184696fcac1c72e0415f9bdac4334d"},
{"name":"pry-byebug","version":"3.10.1","platform":"ruby","checksum":"c8f975c32255bfdb29e151f5532130be64ff3d0042dc858d0907e849125581f8"},
diff --git a/Gemfile.lock b/Gemfile.lock
index c18ac70599e..26594b29704 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,4 +1,10 @@
PATH
+ remote: gems/gitlab-rspec
+ specs:
+ gitlab-rspec (0.1.0)
+ rspec (~> 3.0)
+
+PATH
remote: vendor/gems/attr_encrypted
specs:
attr_encrypted (3.2.4)
@@ -1154,7 +1160,7 @@ GEM
coderay
parser
unparser
- prometheus-client-mmap (0.24.5)
+ prometheus-client-mmap (0.25.0)
rb_sys (~> 0.9)
pry (0.14.2)
coderay (~> 1.1)
@@ -1759,6 +1765,7 @@ DEPENDENCIES
gitlab-mail_room (~> 0.0.23)
gitlab-markup (~> 1.9.0)
gitlab-net-dns (~> 0.9.2)
+ gitlab-rspec!
gitlab-sidekiq-fetcher!
gitlab-styles (~> 10.0.0)
gitlab_chronic_duration (~> 0.10.6.2)
@@ -1874,7 +1881,7 @@ DEPENDENCIES
pg_query (~> 4.2.1)
png_quantizator (~> 0.2.1)
premailer-rails (~> 1.10.3)
- prometheus-client-mmap (~> 0.24)
+ prometheus-client-mmap (~> 0.25)
pry-byebug
pry-rails (~> 0.3.9)
pry-shell (~> 0.6.1)
diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue
index 182090e64f9..0151dbb0bf7 100644
--- a/app/assets/javascripts/error_tracking/components/error_details.vue
+++ b/app/assets/javascripts/error_tracking/components/error_details.vue
@@ -24,6 +24,7 @@ import {
import { severityLevel, severityLevelVariant, errorStatus } from '../constants';
import Stacktrace from './stacktrace.vue';
import ErrorDetailsInfo from './error_details_info.vue';
+import TimelineChart from './timeline_chart.vue';
const SENTRY_TIMEOUT = 10000;
@@ -42,6 +43,7 @@ export default {
GlDropdownDivider,
TimeAgoTooltip,
ErrorDetailsInfo,
+ TimelineChart,
},
props: {
issueUpdatePath: {
@@ -375,6 +377,11 @@ export default {
<error-details-info :error="error" />
+ <div v-if="error.frequency" class="gl-mt-8">
+ <h3>{{ __('Last 24 hours') }}</h3>
+ <timeline-chart :timeline-data="error.frequency" :height="200" />
+ </div>
+
<div v-if="loadingStacktrace" class="gl-py-5">
<gl-loading-icon size="lg" />
</div>
diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
index e3784cc8b92..0c9a98f3b33 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -30,10 +30,12 @@ import {
} from '../events_tracking';
import { I18N_ERROR_TRACKING_LIST } from '../constants';
import ErrorTrackingActions from './error_tracking_actions.vue';
+import TimelineChart from './timeline_chart.vue';
const isValidErrorId = (errorId) => {
return /^[0-9]+$/.test(errorId);
};
+export const tableDataClass = 'gl-display-flex gl-md-display-table-cell gl-align-items-center';
export default {
FIRST_PAGE: 1,
PREV_PAGE: 1,
@@ -43,30 +45,37 @@ export default {
{
key: 'error',
label: __('Error'),
- thClass: 'w-60p',
+ thClass: 'gl-w-40p',
+ tdClass: `${tableDataClass}`,
+ },
+ {
+ key: 'timeline',
+ label: __('Timeline'),
+ thClass: 'gl-text-center gl-w-20p',
+ tdClass: `${tableDataClass} gl-text-center`,
},
{
key: 'events',
label: __('Events'),
- thClass: 'gl-text-right',
- tdClass: 'gl-text-right',
+ thClass: 'gl-text-center gl-w-10p',
+ tdClass: `${tableDataClass} gl-text-center`,
},
{
key: 'users',
label: __('Users'),
- thClass: 'gl-text-right',
- tdClass: 'gl-text-right',
+ thClass: 'gl-text-center gl-w-10p',
+ tdClass: `${tableDataClass} gl-text-center`,
},
{
key: 'lastSeen',
label: __('Last seen'),
- thClass: 'gl-w-15p',
- tdClass: 'gl-text-left',
+ thClass: 'gl-text-center gl-w-10p',
+ tdClass: `${tableDataClass} gl-text-center`,
},
{
key: 'status',
label: '',
- tdClass: 'gl-text-center',
+ tdClass: `${tableDataClass}`,
},
],
statusFilters: {
@@ -95,6 +104,7 @@ export default {
GlPagination,
TimeAgo,
ErrorTrackingActions,
+ TimelineChart,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -438,6 +448,14 @@ export default {
</div>
</template>
+ <template #cell(timeline)="errors">
+ <timeline-chart
+ v-if="errors.item.frequency"
+ :timeline-data="errors.item.frequency"
+ :height="70"
+ />
+ </template>
+
<template #cell(events)="errors">
{{ errors.item.count }}
</template>
diff --git a/app/assets/javascripts/error_tracking/components/timeline_chart.vue b/app/assets/javascripts/error_tracking/components/timeline_chart.vue
new file mode 100644
index 00000000000..51e0c900e4b
--- /dev/null
+++ b/app/assets/javascripts/error_tracking/components/timeline_chart.vue
@@ -0,0 +1,129 @@
+<script>
+import { GlChart } from '@gitlab/ui/dist/charts';
+import { dataVizBlue500 } from '@gitlab/ui/scss_to_js/scss_variables';
+import { hexToRgba } from '@gitlab/ui/dist/utils/utils';
+import { isNumber } from 'lodash';
+import { formatDate } from '~/lib/utils/datetime/date_format_utility';
+import { logError } from '~/lib/logger';
+
+function parseTimelineData(timelineData) {
+ const xData = [];
+ const yData = [];
+ const invalidDataPoints = [];
+ timelineData.forEach((f) => {
+ let rawDate;
+ let count;
+
+ if (Array.isArray(f)) {
+ [rawDate, count] = f;
+ } else if (f.count !== undefined && f.time !== undefined) {
+ rawDate = f.time;
+ count = f.count;
+ }
+ if (rawDate !== undefined && count !== undefined) {
+ // dates/timestamps are in seconds
+ const date = isNumber(rawDate) ? rawDate * 1000 : rawDate;
+ xData.push(formatDate(date));
+ yData.push(count);
+ } else {
+ invalidDataPoints.push(f);
+ }
+ });
+ if (invalidDataPoints.length > 0) {
+ // only log up to 5 invalid data points to reduce log size
+ logError(`Found invalid data points ${invalidDataPoints.slice(0, 5)}`);
+ }
+ return { xData, yData };
+}
+
+export default {
+ components: {
+ GlChart,
+ },
+ props: {
+ timelineData: {
+ /**
+ * Array items can be:
+ * touples: [a_date: string | number, a_count: number]
+ * objects: {time: a_date, count: a_count}: {time: string | number, count: number}
+ *
+ * Dates can either be string or number/timestamp.
+ * When dates are timestamps, they are expected in seconds.
+ *
+ */
+ type: Array,
+ required: true,
+ validator(value) {
+ for (const item of value) {
+ if (Array.isArray(item)) {
+ if (item.length !== 2 || !isNumber(item[1])) {
+ return false;
+ }
+ } else if (typeof item === 'object') {
+ if (!('time' in item) || !('count' in item)) {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+ return true;
+ },
+ },
+ height: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ chartOptions() {
+ if (!this.timelineData) {
+ return {};
+ }
+ const { xData, yData } = parseTimelineData(this.timelineData);
+
+ return {
+ xAxis: {
+ type: 'category',
+ data: xData,
+ show: true,
+ axisTick: {
+ show: false,
+ },
+ axisLabel: {
+ show: false,
+ },
+ axisLine: {
+ show: true,
+ lineStyle: {
+ width: 1,
+ color: '#ececec',
+ },
+ },
+ },
+ yAxis: {
+ type: 'value',
+ show: false,
+ },
+ series: [
+ {
+ data: yData,
+ type: 'bar',
+ itemStyle: { color: hexToRgba(dataVizBlue500, 0.5) },
+ },
+ ],
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow',
+ },
+ },
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-chart v-if="timelineData" :options="chartOptions" :height="height" />
+</template>
diff --git a/app/assets/javascripts/error_tracking/queries/details.query.graphql b/app/assets/javascripts/error_tracking/queries/details.query.graphql
index dd21b0f9c92..5745491c32d 100644
--- a/app/assets/javascripts/error_tracking/queries/details.query.graphql
+++ b/app/assets/javascripts/error_tracking/queries/details.query.graphql
@@ -20,6 +20,10 @@ query errorDetails($fullPath: ID!, $errorId: GitlabErrorTrackingDetailedErrorID!
externalUrl
externalBaseUrl
firstReleaseVersion
+ frequency {
+ count
+ time
+ }
lastReleaseVersion
gitlabCommit
gitlabCommitPath
diff --git a/app/assets/javascripts/members/components/table/role_dropdown.vue b/app/assets/javascripts/members/components/table/role_dropdown.vue
index a85bb09e17b..4571c4172e5 100644
--- a/app/assets/javascripts/members/components/table/role_dropdown.vue
+++ b/app/assets/javascripts/members/components/table/role_dropdown.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { mapActions } from 'vuex';
import * as Sentry from '@sentry/browser';
@@ -9,10 +9,9 @@ import { guestOverageConfirmAction } from 'ee_else_ce/members/guest_overage_conf
export default {
name: 'RoleDropdown',
components: {
- GlDropdown,
- GlDropdownItem,
- LdapDropdownItem: () =>
- import('ee_component/members/components/action_dropdowns/ldap_dropdown_item.vue'),
+ GlCollapsibleListbox,
+ LdapDropdownFooter: () =>
+ import('ee_component/members/components/action_dropdowns/ldap_dropdown_footer.vue'),
},
inject: ['namespace', 'group'],
props: {
@@ -29,23 +28,22 @@ export default {
return {
isDesktop: false,
busy: false,
+ selectedRoleValue: this.member.accessLevel.integerValue,
};
},
computed: {
disabled() {
return this.permissions.canOverride && !this.member.isOverridden;
},
+ dropdownItems() {
+ return Object.entries(this.member.validRoles).map(([name, value]) => ({
+ value,
+ text: name,
+ }));
+ },
},
mounted() {
this.isDesktop = bp.isDesktop();
-
- // Bootstrap Vue and GlDropdown to not support adding attributes to the dropdown toggle
- // This can be changed once https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1060 is implemented
- const dropdownToggle = this.$refs.glDropdown.$el.querySelector('.dropdown-toggle');
-
- if (dropdownToggle) {
- dropdownToggle.dataset.qaSelector = 'access_level_dropdown';
- }
},
methods: {
...mapActions({
@@ -63,7 +61,7 @@ export default {
memberType: this.namespace,
});
},
- async handleSelect(newRoleValue, newRoleName) {
+ async handleSelect(newRoleValue) {
const currentRoleValue = this.member.accessLevel.integerValue;
if (newRoleValue === currentRoleValue) {
return;
@@ -71,6 +69,7 @@ export default {
this.busy = true;
+ const { text: newRoleName } = this.dropdownItems.find((item) => item.value === newRoleValue);
const confirmed = await this.handleOverageConfirm(
currentRoleValue,
newRoleValue,
@@ -99,27 +98,25 @@ export default {
</script>
<template>
- <gl-dropdown
- ref="glDropdown"
- :right="!isDesktop"
- :text="member.accessLevel.stringValue"
+ <gl-collapsible-listbox
+ v-model="selectedRoleValue"
+ :placement="isDesktop ? 'left' : 'right'"
+ :toggle-text="member.accessLevel.stringValue"
:header-text="__('Change role')"
:disabled="disabled"
:loading="busy"
+ data-qa-selector="access_level_dropdown"
+ :items="dropdownItems"
+ @select="handleSelect"
>
- <gl-dropdown-item
- v-for="(value, name) in member.validRoles"
- :key="value"
- is-check-item
- :is-checked="value === member.accessLevel.integerValue"
- data-qa-selector="access_level_link"
- @click="handleSelect(value, name)"
- >
- {{ name }}
- </gl-dropdown-item>
- <ldap-dropdown-item
- v-if="permissions.canOverride && member.isOverridden"
- :member-id="member.id"
- />
- </gl-dropdown>
+ <template #list-item="{ item }">
+ <span data-qa-selector="access_level_link">{{ item.text }}</span>
+ </template>
+ <template #footer>
+ <ldap-dropdown-footer
+ v-if="permissions.canOverride && member.isOverridden"
+ :member-id="member.id"
+ />
+ </template>
+ </gl-collapsible-listbox>
</template>
diff --git a/app/assets/javascripts/profile/components/user_achievements.vue b/app/assets/javascripts/profile/components/user_achievements.vue
index fd42b64f4c5..13a1b797a83 100644
--- a/app/assets/javascripts/profile/components/user_achievements.vue
+++ b/app/assets/javascripts/profile/components/user_achievements.vue
@@ -1,6 +1,7 @@
<script>
-import { GlPopover, GlSprintf } from '@gitlab/ui';
-import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { GlAvatar, GlBadge, GlPopover, GlSprintf } from '@gitlab/ui';
+import { groupBy } from 'lodash';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
import { s__ } from '~/locale';
import { TYPENAME_USER } from '~/graphql_shared/constants';
import timeagoMixin from '~/vue_shared/mixins/timeago';
@@ -8,7 +9,7 @@ import getUserAchievements from './graphql/get_user_achievements.query.graphql';
export default {
name: 'UserAchievements',
- components: { GlPopover, GlSprintf },
+ components: { GlAvatar, GlBadge, GlPopover, GlSprintf },
mixins: [timeagoMixin],
inject: ['rootUrl', 'userId'],
apollo: {
@@ -29,25 +30,39 @@ export default {
},
methods: {
processNodes(nodes) {
- return nodes.slice(0, 3).map(({ achievement, createdAt, achievement: { namespace } }) => {
- return {
- id: `user-achievement-${getIdFromGraphQLId(achievement.id)}`,
- name: achievement.name,
- timeAgo: this.timeFormatted(createdAt),
- avatarUrl: achievement.avatarUrl || gon.gitlab_logo,
- description: achievement.description,
- namespace: namespace && {
- fullPath: namespace.fullPath,
- webUrl: this.rootUrl + namespace.fullPath,
- },
- };
- });
+ return Object.entries(groupBy(nodes, 'achievement.id'))
+ .slice(0, 3)
+ .map(([id, values]) => {
+ const {
+ achievement: { name, avatarUrl, description, namespace },
+ createdAt,
+ } = values[0];
+ const count = values.length;
+ return {
+ id: `user-achievement-${id}`,
+ name,
+ timeAgo: this.timeFormatted(createdAt),
+ avatarUrl: avatarUrl || gon.gitlab_logo,
+ description,
+ namespace: namespace && {
+ fullPath: namespace.fullPath,
+ webUrl: this.rootUrl + namespace.fullPath,
+ },
+ count,
+ };
+ });
},
achievementAwardedMessage(userAchievement) {
return userAchievement.namespace
? this.$options.i18n.awardedBy
: this.$options.i18n.awardedByUnknownNamespace;
},
+ showCountBadge(count) {
+ return count > 1;
+ },
+ getCountBadge(count) {
+ return `${count}x`;
+ },
},
i18n: {
awardedBy: s__('Achievements|Awarded %{timeAgo} by %{namespace}'),
@@ -61,18 +76,28 @@ export default {
<div
v-for="userAchievement in userAchievements"
:key="userAchievement.id"
- class="gl-display-inline-block"
+ class="gl-display-inline-block gl-vertical-align-top"
data-testid="user-achievement"
>
- <img
+ <gl-avatar
:id="userAchievement.id"
:src="userAchievement.avatarUrl"
- :alt="''"
+ :size="32"
tabindex="0"
- class="gl-avatar gl-avatar-s32 gl-mx-2"
+ shape="rect"
+ class="gl-mx-2"
/>
- <gl-popover triggers="hover focus" placement="top" :target="userAchievement.id">
- <div class="gl-font-weight-bold">{{ userAchievement.name }}</div>
+ <br />
+ <gl-badge v-if="showCountBadge(userAchievement.count)" variant="info" size="sm">{{
+ getCountBadge(userAchievement.count)
+ }}</gl-badge>
+ <gl-popover :target="userAchievement.id">
+ <div>
+ <span class="gl-font-weight-bold">{{ userAchievement.name }}</span>
+ <gl-badge v-if="showCountBadge(userAchievement.count)" variant="info" size="sm">{{
+ getCountBadge(userAchievement.count)
+ }}</gl-badge>
+ </div>
<div>
<gl-sprintf :message="achievementAwardedMessage(userAchievement)">
<template #timeAgo>
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
index 330db7ff2ee..c330eccb186 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
@@ -137,8 +137,8 @@ export default {
isProjectArchived() {
return this.workItem?.project?.archived;
},
- canUpdate() {
- return this.workItem?.userPermissions?.updateWorkItem;
+ canCreateNote() {
+ return this.workItem?.userPermissions?.createNote;
},
workItemState() {
return this.workItem?.state;
@@ -243,7 +243,7 @@ export default {
<li :class="timelineEntryClass">
<work-item-note-signed-out v-if="!signedIn" />
<work-item-comment-locked
- v-else-if="!canUpdate"
+ v-else-if="!canCreateNote"
:work-item-type="workItemType"
:is-project-archived="isProjectArchived"
/>
diff --git a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
index 20a8d127e7d..1ae5617f04d 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
@@ -32,6 +32,7 @@ fragment WorkItem on WorkItem {
updateWorkItem
adminParentLink
setWorkItemMetadata
+ createNote
}
widgets {
...WorkItemWidgets
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index d6740cdf1ac..3a5e8d09759 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -169,7 +169,15 @@ module MergeRequests
@outdate_service ||= Suggestions::OutdateService.new
end
+ def abort_auto_merges?(merge_request)
+ return true unless Feature.enabled?(:fix_interrupted_mwps, @project)
+
+ merge_request.merge_params.with_indifferent_access[:sha] != @push.newrev
+ end
+
def abort_auto_merges(merge_request)
+ return unless abort_auto_merges?(merge_request)
+
abort_auto_merge(merge_request, 'source branch was updated')
end
diff --git a/config/feature_flags/development/fix_interrupted_mwps.yml b/config/feature_flags/development/fix_interrupted_mwps.yml
new file mode 100644
index 00000000000..dafabedb22e
--- /dev/null
+++ b/config/feature_flags/development/fix_interrupted_mwps.yml
@@ -0,0 +1,8 @@
+---
+name: fix_interrupted_mwps
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123349
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/415032
+milestone: '16.1'
+type: development
+group: group::source code
+default_enabled: false
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 75ad86df515..dc0b77ef99a 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -539,6 +539,8 @@
- 1
- - search_project_index_integrity
- 1
+- - search_wiki_elastic_delete_group_wiki
+ - 1
- - search_zoekt_namespace_indexer
- 1
- - security_auto_fix
diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb
index 6e4872a29e7..5442c6eb00b 100644
--- a/db/fixtures/development/17_cycle_analytics.rb
+++ b/db/fixtures/development/17_cycle_analytics.rb
@@ -70,30 +70,36 @@ class Gitlab::Seeder::CycleAnalytics # rubocop:disable Style/ClassAndModuleChild
return
end
- create_developers!
- create_issues!
-
- seed_lead_time!
- seed_issue_stage!
- seed_plan_stage!
- seed_code_stage!
- seed_test_stage!
- seed_review_stage!
- seed_staging_stage!
-
- if Gitlab.ee?
- create_vulnerabilities_count_report!
- seed_dora_metrics!
- create_custom_value_stream!
- create_value_stream_aggregation(project.group)
- end
-
- puts "Successfully seeded '#{project.full_path}' for Value Stream Management!"
- puts "URL: #{Rails.application.routes.url_helpers.project_url(project)}"
+ seed_data!
end
private
+ def seed_data!
+ Sidekiq::Worker.skipping_transaction_check do
+ create_developers!
+ create_issues!
+
+ seed_lead_time!
+ seed_issue_stage!
+ seed_plan_stage!
+ seed_code_stage!
+ seed_test_stage!
+ seed_review_stage!
+ seed_staging_stage!
+
+ if Gitlab.ee?
+ create_vulnerabilities_count_report!
+ seed_dora_metrics!
+ create_custom_value_stream!
+ create_value_stream_aggregation(project.group)
+ end
+
+ puts "Successfully seeded '#{project.full_path}' for Value Stream Management!"
+ puts "URL: #{Rails.application.routes.url_helpers.project_url(project)}"
+ end
+ end
+
def create_custom_value_stream!
[project.project_namespace.reload, project.group].each do |parent|
Analytics::CycleAnalytics::ValueStreams::CreateService.new(
diff --git a/doc/ci/environments/index.md b/doc/ci/environments/index.md
index 50eae65ad76..f2fb8eaa27e 100644
--- a/doc/ci/environments/index.md
+++ b/doc/ci/environments/index.md
@@ -733,7 +733,7 @@ to get alerts when there are critical issues that need immediate attention.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214634) in GitLab 13.4.
-If you [set up alerts for Prometheus metrics](../../operations/metrics/alerts.md),
+If you [set up alerts for Prometheus metrics](../../operations/incident_management/integrations.md#configuration),
alerts for environments are shown on the environments page. The alert with the highest
severity is shown, so you can identify which environments need immediate attention.
@@ -773,28 +773,14 @@ GitLab Auto Rollback is turned off by default. To turn it on:
1. Select the checkbox for **Enable automatic rollbacks**.
1. Select **Save changes**.
-### Monitor environments
+<!--- start_remove The following content will be removed on remove_date: '2023-09-22' -->
-To monitor the behavior of your app as it runs in each environment,
-enable [Prometheus for monitoring system and response metrics](../../user/project/integrations/prometheus.md).
-For the monitoring dashboard to appear, configure Prometheus to collect at least one
-[supported metric](../../user/project/integrations/prometheus_library/index.md).
+### Monitor environments (removed)
-All deployments to an environment are shown on the monitoring dashboard.
-You can view changes in performance for each version of your application.
+This feature was [deprecated](https://gitlab.com/groups/gitlab-org/-/epics/10107) in GitLab 14.7
+and [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/399231) in 16.0.
-GitLab attempts to retrieve [supported performance metrics](../../user/project/integrations/prometheus_library/index.md)
-for any environment that has had a successful deployment. If monitoring data was
-successfully retrieved, a **Monitoring** button appears for each environment.
-
-To view the last eight hours of performance data, select the **Monitoring** button.
-It may take a minute or two for data to appear after initial deployment.
-
-![Monitoring dashboard](../img/environments_monitoring.png)
-
-#### Embed metrics in GitLab Flavored Markdown
-
-Metric charts can be embedded in GitLab Flavored Markdown. See [Embedding Metrics in GitLab Flavored Markdown](../../operations/metrics/embed.md) for more details.
+<!--- end_remove -->
### Web terminals (deprecated)
diff --git a/doc/ci/img/environments_monitoring.png b/doc/ci/img/environments_monitoring.png
deleted file mode 100644
index 63d272ae42a..00000000000
--- a/doc/ci/img/environments_monitoring.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/gems.md b/doc/development/gems.md
index 709dfa105bf..4a332a28f62 100644
--- a/doc/development/gems.md
+++ b/doc/development/gems.md
@@ -156,7 +156,7 @@ You can see example adding new Gem: [!121676](https://gitlab.com/gitlab-org/gitl
paths:
- gitlab-<name-of-gem>/vendor/ruby
before_script:
- - cd vendor/gems/bundler-checksum
+ - cd gems/gitlab-<name-of-gem>
- ruby -v # Print out ruby version for debugging
- gem install bundler --no-document # Bundler is not installed with the image
- bundle config set --local path 'vendor' # Install dependencies into ./vendor/ruby
diff --git a/doc/topics/autodevops/img/auto_monitoring.png b/doc/topics/autodevops/img/auto_monitoring.png
deleted file mode 100644
index 2900e5d1877..00000000000
--- a/doc/topics/autodevops/img/auto_monitoring.png
+++ /dev/null
Binary files differ
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index d51899ee7ca..10979f0bb21 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -40,7 +40,6 @@ Auto DevOps supports development during each of the [DevOps stages](stages.md).
| Test | [Auto License Compliance](stages.md#auto-license-compliance) |
| Deploy | [Auto Review Apps](stages.md#auto-review-apps) |
| Deploy | [Auto Deploy](stages.md#auto-deploy) |
-| Monitor | [Auto Monitoring](stages.md#auto-monitoring) |
| Secure | [Auto Dynamic Application Security Testing (DAST)](stages.md#auto-dast) |
| Secure | [Auto Static Application Security Testing (SAST)](stages.md#auto-sast) |
| Secure | [Auto Secret Detection](stages.md#auto-secret-detection) |
diff --git a/doc/topics/autodevops/prepare_deployment.md b/doc/topics/autodevops/prepare_deployment.md
index 6f434e8fb84..b71beee708e 100644
--- a/doc/topics/autodevops/prepare_deployment.md
+++ b/doc/topics/autodevops/prepare_deployment.md
@@ -37,8 +37,7 @@ to minimize downtime and risk.
## Auto DevOps base domain
The Auto DevOps base domain is required to use
-[Auto Review Apps](stages.md#auto-review-apps), [Auto Deploy](stages.md#auto-deploy), and
-[Auto Monitoring](stages.md#auto-monitoring).
+[Auto Review Apps](stages.md#auto-review-apps) and [Auto Deploy](stages.md#auto-deploy).
To define the base domain, either:
diff --git a/doc/topics/autodevops/requirements.md b/doc/topics/autodevops/requirements.md
index ce53fe26868..dd73a2056e5 100644
--- a/doc/topics/autodevops/requirements.md
+++ b/doc/topics/autodevops/requirements.md
@@ -54,8 +54,7 @@ to minimize downtime and risk.
## Auto DevOps base domain
The Auto DevOps base domain is required to use
-[Auto Review Apps](stages.md#auto-review-apps), [Auto Deploy](stages.md#auto-deploy), and
-[Auto Monitoring](stages.md#auto-monitoring).
+[Auto Review Apps](stages.md#auto-review-apps) and [Auto Deploy](stages.md#auto-deploy).
To define the base domain, either:
@@ -91,8 +90,8 @@ to the Kubernetes pods running your application.
To make full use of Auto DevOps with Kubernetes, you need:
-- **Kubernetes** (for [Auto Review Apps](stages.md#auto-review-apps),
- [Auto Deploy](stages.md#auto-deploy), and [Auto Monitoring](stages.md#auto-monitoring))
+- **Kubernetes** (for [Auto Review Apps](stages.md#auto-review-apps) and
+ [Auto Deploy](stages.md#auto-deploy))
To enable deployments, you need:
@@ -117,8 +116,8 @@ To make full use of Auto DevOps with Kubernetes, you need:
If your cluster is installed on bare metal, see
[Auto DevOps Requirements for bare metal](#auto-devops-requirements-for-bare-metal).
-- **Base domain** (for [Auto Review Apps](stages.md#auto-review-apps),
- [Auto Deploy](stages.md#auto-deploy), and [Auto Monitoring](stages.md#auto-monitoring))
+- **Base domain** (for [Auto Review Apps](stages.md#auto-review-apps) and
+ [Auto Deploy](stages.md#auto-deploy))
You must [specify the Auto DevOps base domain](#auto-devops-base-domain),
which all of your Auto DevOps applications use. This domain must be configured
@@ -148,8 +147,8 @@ To make full use of Auto DevOps with Kubernetes, you need:
certificates are valid and up-to-date.
If you don't have Kubernetes or Prometheus configured, then
-[Auto Review Apps](stages.md#auto-review-apps),
-[Auto Deploy](stages.md#auto-deploy), and [Auto Monitoring](stages.md#auto-monitoring)
+[Auto Review Apps](stages.md#auto-review-apps) and
+[Auto Deploy](stages.md#auto-deploy)
are skipped.
After all requirements are met, you can [enable Auto DevOps](index.md#enable-or-disable-auto-devops).
diff --git a/doc/topics/autodevops/stages.md b/doc/topics/autodevops/stages.md
index c95426b03e6..6be8a71cdbc 100644
--- a/doc/topics/autodevops/stages.md
+++ b/doc/topics/autodevops/stages.md
@@ -591,32 +591,14 @@ When using Cloud Native Buildpacks, instead of `/bin/herokuish procfile exec`, u
/cnb/lifecycle/launcher $COMMAND
```
-## Auto Monitoring
+<!--- start_remove The following content will be removed on remove_date: '2023-09-22' -->
-After your application deploys, Auto Monitoring helps you monitor
-your application's server and response metrics right out of the box. Auto
-Monitoring uses [Prometheus](../../user/project/integrations/prometheus.md) to
-retrieve system metrics, such as CPU and memory usage, directly from
-[Kubernetes](../../user/project/integrations/prometheus_library/kubernetes.md),
-and response metrics, such as HTTP error rates, latency, and throughput, from the
-[NGINX server](../../user/project/integrations/prometheus_library/nginx_ingress.md).
+### Auto Monitoring (removed)
-The metrics include:
+This feature was [deprecated](https://gitlab.com/groups/gitlab-org/-/epics/10107) in GitLab 14.7
+and [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/399231) in 16.0.
-- **Response Metrics:** latency, throughput, error rate
-- **System Metrics:** CPU utilization, memory utilization
-
-To use Auto Monitoring:
-
-1. [Install and configure the Auto DevOps requirements](requirements.md).
-1. [Enable Auto DevOps](index.md#enable-or-disable-auto-devops), if you haven't done already.
-1. On the left sidebar, select **CI/CD > Pipelines**.
-1. Select **Run pipeline**.
-1. After the pipeline finishes successfully, open the
- [monitoring dashboard for a deployed environment](../../ci/environments/index.md#monitor-environments)
- to view the metrics of your deployed application.
-
-![Auto Metrics](img/auto_monitoring.png)
+<!--- end_remove -->
## Auto Code Intelligence
diff --git a/gems/gitlab-rspec/.gitignore b/gems/gitlab-rspec/.gitignore
new file mode 100644
index 00000000000..b04a8c840df
--- /dev/null
+++ b/gems/gitlab-rspec/.gitignore
@@ -0,0 +1,11 @@
+/.bundle/
+/.yardoc
+/_yardoc/
+/coverage/
+/doc/
+/pkg/
+/spec/reports/
+/tmp/
+
+# rspec failure tracking
+.rspec_status
diff --git a/gems/gitlab-rspec/.gitlab-ci.yml b/gems/gitlab-rspec/.gitlab-ci.yml
new file mode 100644
index 00000000000..0932753d1e7
--- /dev/null
+++ b/gems/gitlab-rspec/.gitlab-ci.yml
@@ -0,0 +1,31 @@
+# You can override the included template(s) by including variable overrides
+# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
+# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+# Note that environment variables can be set in several places
+# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
+
+workflow:
+ rules:
+ - if: $CI_MERGE_REQUEST_ID
+
+rspec:
+ image: "ruby:${RUBY_VERSION}"
+ cache:
+ key: gitlab-rspec
+ paths:
+ - gitlab-rspec/vendor/ruby
+ before_script:
+ - cd gems/gitlab-rspec
+ - ruby -v # Print out ruby version for debugging
+ - gem install bundler --no-document # Bundler is not installed with the image
+ - bundle config set --local path 'vendor' # Install dependencies into ./vendor/ruby
+ - bundle config set with 'development'
+ - bundle config set --local frozen 'true' # Disallow Gemfile.lock changes on CI
+ - bundle config # Show bundler configuration
+ - bundle install -j $(nproc)
+ script:
+ - bundle exec rspec
+ parallel:
+ matrix:
+ - RUBY_VERSION: ["2.7", "3.0", "3.1", "3.2"]
diff --git a/gems/gitlab-rspec/.rspec b/gems/gitlab-rspec/.rspec
new file mode 100644
index 00000000000..34c5164d9b5
--- /dev/null
+++ b/gems/gitlab-rspec/.rspec
@@ -0,0 +1,3 @@
+--format documentation
+--color
+--require spec_helper
diff --git a/gems/gitlab-rspec/.rubocop.yml b/gems/gitlab-rspec/.rubocop.yml
new file mode 100644
index 00000000000..b605c30678a
--- /dev/null
+++ b/gems/gitlab-rspec/.rubocop.yml
@@ -0,0 +1,14 @@
+inherit_from:
+ - ../../.rubocop.yml
+
+CodeReuse/ActiveRecord:
+ Enabled: false
+
+AllCops:
+ TargetRubyVersion: 3.0
+
+Naming/FileName:
+ Exclude:
+ - spec/**/*.rb
+ - lib/gitlab/rspec.rb
+ - lib/gitlab/rspec/all.rb
diff --git a/gems/gitlab-rspec/Gemfile b/gems/gitlab-rspec/Gemfile
new file mode 100644
index 00000000000..78f8c3d0b36
--- /dev/null
+++ b/gems/gitlab-rspec/Gemfile
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+source "https://rubygems.org"
+
+gemspec
+
+gem "rspec", "~> 3.0"
+gem 'nokogiri', '~> 1.15.2'
+
+group :development, :test do
+ gem 'rubocop'
+ gem 'rubocop-rspec', '~> 2.18.1'
+ gem 'factory_bot_rails', '~> 6.2.0'
+ gem 'gitlab-styles', '~> 10.0.0', require: false
+ gem 'rspec-rails', '~> 6.0.1'
+ gem 'rspec-parameterized', '~> 1.0'
+ gem 'rspec-benchmark', '~> 0.6.0'
+end
diff --git a/gems/gitlab-rspec/Gemfile.lock b/gems/gitlab-rspec/Gemfile.lock
new file mode 100644
index 00000000000..819a6237a90
--- /dev/null
+++ b/gems/gitlab-rspec/Gemfile.lock
@@ -0,0 +1,180 @@
+PATH
+ remote: .
+ specs:
+ gitlab-rspec (0.1.0)
+ rspec (~> 3.0)
+
+GEM
+ remote: https://rubygems.org/
+ specs:
+ actionpack (7.0.4.3)
+ actionview (= 7.0.4.3)
+ activesupport (= 7.0.4.3)
+ rack (~> 2.0, >= 2.2.0)
+ rack-test (>= 0.6.3)
+ rails-dom-testing (~> 2.0)
+ rails-html-sanitizer (~> 1.0, >= 1.2.0)
+ actionview (7.0.4.3)
+ activesupport (= 7.0.4.3)
+ builder (~> 3.1)
+ erubi (~> 1.4)
+ rails-dom-testing (~> 2.0)
+ rails-html-sanitizer (~> 1.1, >= 1.2.0)
+ activesupport (7.0.4.3)
+ concurrent-ruby (~> 1.0, >= 1.0.2)
+ i18n (>= 1.6, < 2)
+ minitest (>= 5.1)
+ tzinfo (~> 2.0)
+ ast (2.4.2)
+ benchmark-malloc (0.2.0)
+ benchmark-perf (0.6.0)
+ benchmark-trend (0.4.0)
+ binding_of_caller (1.0.0)
+ debug_inspector (>= 0.0.1)
+ builder (3.2.4)
+ coderay (1.1.3)
+ concurrent-ruby (1.2.2)
+ crass (1.0.6)
+ debug_inspector (1.1.0)
+ diff-lcs (1.5.0)
+ erubi (1.12.0)
+ factory_bot (6.2.0)
+ activesupport (>= 5.0.0)
+ factory_bot_rails (6.2.0)
+ factory_bot (~> 6.2.0)
+ railties (>= 5.0.0)
+ gitlab-styles (10.0.0)
+ rubocop (~> 1.43.0)
+ rubocop-graphql (~> 0.18)
+ rubocop-performance (~> 1.15)
+ rubocop-rails (~> 2.17)
+ rubocop-rspec (~> 2.18)
+ i18n (1.13.0)
+ concurrent-ruby (~> 1.0)
+ json (2.6.3)
+ loofah (2.21.3)
+ crass (~> 1.0.2)
+ nokogiri (>= 1.12.0)
+ method_source (1.0.0)
+ mini_portile2 (2.8.2)
+ minitest (5.18.0)
+ nokogiri (1.15.2)
+ mini_portile2 (~> 2.8.2)
+ racc (~> 1.4)
+ parallel (1.23.0)
+ parser (3.2.2.1)
+ ast (~> 2.4.1)
+ proc_to_ast (0.1.0)
+ coderay
+ parser
+ unparser
+ racc (1.6.2)
+ rack (2.2.7)
+ rack-test (2.1.0)
+ rack (>= 1.3)
+ rails-dom-testing (2.0.3)
+ activesupport (>= 4.2.0)
+ nokogiri (>= 1.6)
+ rails-html-sanitizer (1.5.0)
+ loofah (~> 2.19, >= 2.19.1)
+ railties (7.0.4.3)
+ actionpack (= 7.0.4.3)
+ activesupport (= 7.0.4.3)
+ method_source
+ rake (>= 12.2)
+ thor (~> 1.0)
+ zeitwerk (~> 2.5)
+ rainbow (3.1.1)
+ rake (13.0.6)
+ regexp_parser (2.8.0)
+ rexml (3.2.5)
+ rspec (3.12.0)
+ rspec-core (~> 3.12.0)
+ rspec-expectations (~> 3.12.0)
+ rspec-mocks (~> 3.12.0)
+ rspec-benchmark (0.6.0)
+ benchmark-malloc (~> 0.2)
+ benchmark-perf (~> 0.6)
+ benchmark-trend (~> 0.4)
+ rspec (>= 3.0)
+ rspec-core (3.12.0)
+ rspec-support (~> 3.12.0)
+ rspec-expectations (3.12.2)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.12.0)
+ rspec-mocks (3.12.3)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.12.0)
+ rspec-parameterized (1.0.0)
+ rspec-parameterized-core (< 2)
+ rspec-parameterized-table_syntax (< 2)
+ rspec-parameterized-core (1.0.0)
+ parser
+ proc_to_ast
+ rspec (>= 2.13, < 4)
+ unparser
+ rspec-parameterized-table_syntax (1.0.0)
+ binding_of_caller
+ rspec-parameterized-core (< 2)
+ rspec-rails (6.0.2)
+ actionpack (>= 6.1)
+ activesupport (>= 6.1)
+ railties (>= 6.1)
+ rspec-core (~> 3.12)
+ rspec-expectations (~> 3.12)
+ rspec-mocks (~> 3.12)
+ rspec-support (~> 3.12)
+ rspec-support (3.12.0)
+ rubocop (1.43.0)
+ json (~> 2.3)
+ parallel (~> 1.10)
+ parser (>= 3.2.0.0)
+ rainbow (>= 2.2.2, < 4.0)
+ regexp_parser (>= 1.8, < 3.0)
+ rexml (>= 3.2.5, < 4.0)
+ rubocop-ast (>= 1.24.1, < 2.0)
+ ruby-progressbar (~> 1.7)
+ unicode-display_width (>= 2.4.0, < 3.0)
+ rubocop-ast (1.28.1)
+ parser (>= 3.2.1.0)
+ rubocop-capybara (2.18.0)
+ rubocop (~> 1.41)
+ rubocop-graphql (0.19.0)
+ rubocop (>= 0.87, < 2)
+ rubocop-performance (1.18.0)
+ rubocop (>= 1.7.0, < 2.0)
+ rubocop-ast (>= 0.4.0)
+ rubocop-rails (2.19.1)
+ activesupport (>= 4.2.0)
+ rack (>= 1.1)
+ rubocop (>= 1.33.0, < 2.0)
+ rubocop-rspec (2.18.1)
+ rubocop (~> 1.33)
+ rubocop-capybara (~> 2.17)
+ ruby-progressbar (1.13.0)
+ thor (1.2.2)
+ tzinfo (2.0.6)
+ concurrent-ruby (~> 1.0)
+ unicode-display_width (2.4.2)
+ unparser (0.6.7)
+ diff-lcs (~> 1.3)
+ parser (>= 3.2.0)
+ zeitwerk (2.6.8)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ factory_bot_rails (~> 6.2.0)
+ gitlab-rspec!
+ gitlab-styles (~> 10.0.0)
+ nokogiri (~> 1.15.2)
+ rspec (~> 3.0)
+ rspec-benchmark (~> 0.6.0)
+ rspec-parameterized (~> 1.0)
+ rspec-rails (~> 6.0.1)
+ rubocop
+ rubocop-rspec (~> 2.18.1)
+
+BUNDLED WITH
+ 2.4.4
diff --git a/gems/gitlab-rspec/gitlab-rspec.gemspec b/gems/gitlab-rspec/gitlab-rspec.gemspec
new file mode 100644
index 00000000000..630cd866a55
--- /dev/null
+++ b/gems/gitlab-rspec/gitlab-rspec.gemspec
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require_relative "lib/gitlab/rspec/version"
+
+Gem::Specification.new do |spec|
+ spec.name = "gitlab-rspec"
+ spec.version = Gitlab::Rspec::Version::VERSION
+ spec.authors = ["group::tenant-scale"]
+ spec.email = ["engineering@gitlab.com"]
+
+ spec.summary = "GitLab's RSpec extensions"
+ spec.description = "A set of useful helpers to configure RSpec with various stubs and CI configs."
+ spec.homepage = "https://gitlab.com/gitlab-org/gitlab/-/tree/master/gems/gitlab-rspec"
+ spec.license = "MIT"
+ spec.required_ruby_version = ">= 3.0"
+
+ # Specify which files should be added to the gem when it is released.
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
+ spec.files = Dir.chdir(__dir__) do
+ `git ls-files -z`.split("\x0").reject do |f|
+ (File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor])
+ end
+ end
+ spec.bindir = "exe"
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
+ spec.require_paths = ["lib"]
+ spec.add_dependency "rspec", "~> 3.0"
+end
diff --git a/gems/gitlab-rspec/lib/gitlab/rspec.rb b/gems/gitlab-rspec/lib/gitlab/rspec.rb
new file mode 100644
index 00000000000..3c95a98ba61
--- /dev/null
+++ b/gems/gitlab-rspec/lib/gitlab/rspec.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+require "rspec"
+require_relative "rspec/version"
diff --git a/gems/gitlab-rspec/lib/gitlab/rspec/all.rb b/gems/gitlab-rspec/lib/gitlab/rspec/all.rb
new file mode 100644
index 00000000000..47beb70d23e
--- /dev/null
+++ b/gems/gitlab-rspec/lib/gitlab/rspec/all.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+require_relative "../rspec"
+require_relative "stub_env"
diff --git a/spec/support/helpers/stub_env.rb b/gems/gitlab-rspec/lib/gitlab/rspec/stub_env.rb
index afa501d6279..afa501d6279 100644
--- a/spec/support/helpers/stub_env.rb
+++ b/gems/gitlab-rspec/lib/gitlab/rspec/stub_env.rb
diff --git a/gems/gitlab-rspec/lib/gitlab/rspec/version.rb b/gems/gitlab-rspec/lib/gitlab/rspec/version.rb
new file mode 100644
index 00000000000..34c51245012
--- /dev/null
+++ b/gems/gitlab-rspec/lib/gitlab/rspec/version.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Rspec
+ module Version
+ VERSION = "0.1.0"
+ end
+ end
+end
diff --git a/gems/gitlab-rspec/spec/spec_helper.rb b/gems/gitlab-rspec/spec/spec_helper.rb
new file mode 100644
index 00000000000..c694747e094
--- /dev/null
+++ b/gems/gitlab-rspec/spec/spec_helper.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'gitlab/rspec/all'
+
+RSpec.configure do |config|
+ # Enable flags like --only-failures and --next-failure
+ config.example_status_persistence_file_path = ".rspec_status"
+
+ # Disable RSpec exposing methods globally on `Module` and `main`
+ config.disable_monkey_patching!
+
+ config.expect_with :rspec do |c|
+ c.syntax = :expect
+ end
+end
diff --git a/lib/gitlab/email/handler/service_desk_handler.rb b/lib/gitlab/email/handler/service_desk_handler.rb
index 215ba77db13..5d0e6ea61e1 100644
--- a/lib/gitlab/email/handler/service_desk_handler.rb
+++ b/lib/gitlab/email/handler/service_desk_handler.rb
@@ -32,6 +32,9 @@ module Gitlab
def execute
raise ProjectNotFound if project.nil?
+ # Verification emails should never create issues
+ return if handled_custom_email_address_verification?
+
create_issue_or_note
if from_address
@@ -70,6 +73,27 @@ module Gitlab
attr_reader :project_id, :project_path, :service_desk_key
+ def contains_custom_email_address_verification_subaddress?
+ return false unless Feature.enabled?(:service_desk_custom_email, project)
+
+ # Verification email only has one recipient
+ mail.to.first.include?(ServiceDeskSetting::CUSTOM_EMAIL_VERIFICATION_SUBADDRESS)
+ end
+
+ def handled_custom_email_address_verification?
+ return false unless contains_custom_email_address_verification_subaddress?
+
+ ::ServiceDesk::CustomEmailVerifications::UpdateService.new(
+ project: project,
+ current_user: nil,
+ params: {
+ mail: mail
+ }
+ ).execute
+
+ true
+ end
+
def project_from_key
return unless match = service_desk_key.match(PROJECT_KEY_PATTERN)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 69228e114c0..4b7414f4480 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -26606,6 +26606,9 @@ msgstr ""
msgid "Last 2 weeks"
msgstr ""
+msgid "Last 24 hours"
+msgstr ""
+
msgid "Last Accessed On"
msgstr ""
@@ -47589,6 +47592,9 @@ msgstr ""
msgid "Timeago|right now"
msgstr ""
+msgid "Timeline"
+msgstr ""
+
msgid "Timeline event added successfully."
msgstr ""
diff --git a/qa/Dockerfile b/qa/Dockerfile
index bd9cd166701..e5308d78f83 100644
--- a/qa/Dockerfile
+++ b/qa/Dockerfile
@@ -49,6 +49,7 @@ COPY ./config/bundler_setup.rb /home/gitlab/config/
COPY ./lib/gitlab_edition.rb /home/gitlab/lib/
COPY ./lib/gitlab/utils.rb /home/gitlab/lib/gitlab/
COPY ./INSTALLATION_TYPE ./VERSION /home/gitlab/
+COPY ./gems /home/gitlab/
COPY ./qa /home/gitlab/qa
diff --git a/sidekiq_cluster/cli.rb b/sidekiq_cluster/cli.rb
index 22cddead3e4..0d24f70c37d 100644
--- a/sidekiq_cluster/cli.rb
+++ b/sidekiq_cluster/cli.rb
@@ -8,7 +8,7 @@ require 'time'
# In environments where code is preloaded and cached such as `spring`,
# we may run into "already initialized" warnings, hence the check.
-require_relative '../lib/gitlab' unless Object.const_defined?(:Gitlab)
+require_relative '../lib/gitlab'
require_relative '../lib/gitlab/utils'
require_relative '../lib/gitlab/sidekiq_config/cli_methods'
require_relative '../lib/gitlab/sidekiq_config/worker_matcher'
diff --git a/spec/contracts/provider/spec_helper.rb b/spec/contracts/provider/spec_helper.rb
index 44e4d29c18e..f1ceca16b4b 100644
--- a/spec/contracts/provider/spec_helper.rb
+++ b/spec/contracts/provider/spec_helper.rb
@@ -2,10 +2,10 @@
require 'spec_helper'
require 'zeitwerk'
+require 'gitlab/rspec/all'
require_relative 'helpers/users_helper'
require_relative('../../../ee/spec/contracts/provider/spec_helper') if Gitlab.ee?
require Rails.root.join("spec/support/helpers/rails_helpers.rb")
-require Rails.root.join("spec/support/helpers/stub_env.rb")
# Opt out of telemetry collection. We can't allow all engineers, and users who install GitLab from source, to be
# automatically enrolled in sending data on their usage without their knowledge.
diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb
index fcf0c43243f..03c8919912c 100644
--- a/spec/fast_spec_helper.rb
+++ b/spec/fast_spec_helper.rb
@@ -22,6 +22,7 @@ require_relative 'rails_autoload'
require_relative '../config/settings'
require_relative 'support/rspec'
+require_relative '../lib/gitlab'
require_relative '../lib/gitlab/utils'
require_relative '../lib/gitlab/utils/strong_memoize'
diff --git a/spec/features/groups/members/manage_groups_spec.rb b/spec/features/groups/members/manage_groups_spec.rb
index 599b31827da..87de0e2e46b 100644
--- a/spec/features/groups/members/manage_groups_spec.rb
+++ b/spec/features/groups/members/manage_groups_spec.rb
@@ -3,6 +3,7 @@
require 'spec_helper'
RSpec.describe 'Groups > Members > Manage groups', :js, feature_category: :groups_and_projects do
+ include ListboxHelpers
include Features::MembersHelpers
include Features::InviteMembersModalHelpers
include Spec::Support::Helpers::ModalHelpers
@@ -75,8 +76,7 @@ RSpec.describe 'Groups > Members > Manage groups', :js, feature_category: :group
click_groups_tab
page.within(first_row) do
- click_button('Developer')
- click_button('Maintainer')
+ select_from_listbox('Maintainer', from: 'Developer')
wait_for_requests
diff --git a/spec/features/groups/members/manage_members_spec.rb b/spec/features/groups/members/manage_members_spec.rb
index 3c1425fc9ca..138031ffaac 100644
--- a/spec/features/groups/members/manage_members_spec.rb
+++ b/spec/features/groups/members/manage_members_spec.rb
@@ -3,6 +3,7 @@
require 'spec_helper'
RSpec.describe 'Groups > Members > Manage members', feature_category: :groups_and_projects do
+ include ListboxHelpers
include Features::MembersHelpers
include Features::InviteMembersModalHelpers
include Spec::Support::Helpers::ModalHelpers
@@ -35,8 +36,7 @@ RSpec.describe 'Groups > Members > Manage members', feature_category: :groups_an
visit group_group_members_path(group)
page.within(second_row) do
- click_button('Developer')
- click_button('Owner')
+ select_from_listbox('Owner', from: 'Developer')
expect(page).to have_button('Owner')
end
diff --git a/spec/features/projects/members/groups_with_access_list_spec.rb b/spec/features/projects/members/groups_with_access_list_spec.rb
index 0f80c6cb3b8..a2a04ada627 100644
--- a/spec/features/projects/members/groups_with_access_list_spec.rb
+++ b/spec/features/projects/members/groups_with_access_list_spec.rb
@@ -3,6 +3,7 @@
require 'spec_helper'
RSpec.describe 'Projects > Members > Groups with access list', :js, feature_category: :groups_and_projects do
+ include ListboxHelpers
include Features::MembersHelpers
include Spec::Support::Helpers::ModalHelpers
include Features::InviteMembersModalHelpers
@@ -26,8 +27,7 @@ RSpec.describe 'Projects > Members > Groups with access list', :js, feature_cate
end
it 'updates group access level' do
- click_button group_link.human_access
- click_button 'Guest'
+ select_from_listbox('Guest', from: group_link.human_access)
wait_for_requests
diff --git a/spec/features/projects/members/manage_members_spec.rb b/spec/features/projects/members/manage_members_spec.rb
index 5ae6eb83b6b..0e3ac5ff3ac 100644
--- a/spec/features/projects/members/manage_members_spec.rb
+++ b/spec/features/projects/members/manage_members_spec.rb
@@ -3,6 +3,7 @@
require 'spec_helper'
RSpec.describe 'Projects > Members > Manage members', :js, feature_category: :onboarding do
+ include ListboxHelpers
include Features::MembersHelpers
include Features::InviteMembersModalHelpers
include Spec::Support::Helpers::ModalHelpers
@@ -61,11 +62,8 @@ RSpec.describe 'Projects > Members > Manage members', :js, feature_category: :on
page.within find_member_row(project_developer) do
click_button('Developer')
- page.within '.dropdown-menu' do
- expect(page).not_to have_button('Owner')
- end
-
- click_button('Reporter')
+ expect_no_listbox_item('Owner')
+ select_listbox_item('Reporter')
expect(page).to have_button('Reporter')
end
@@ -87,8 +85,7 @@ RSpec.describe 'Projects > Members > Manage members', :js, feature_category: :on
visit_members_page
page.within find_member_row(project_owner) do
- click_button('Owner')
- click_button('Reporter')
+ select_from_listbox('Reporter', from: 'Owner')
expect(page).to have_button('Reporter')
end
diff --git a/spec/frontend/admin/users/components/user_actions_spec.js b/spec/frontend/admin/users/components/user_actions_spec.js
index 73d8c082bb9..69755c6142a 100644
--- a/spec/frontend/admin/users/components/user_actions_spec.js
+++ b/spec/frontend/admin/users/components/user_actions_spec.js
@@ -91,7 +91,7 @@ describe('AdminUserActions component', () => {
initComponent({ actions: [LDAP] });
});
- it('renders the LDAP dropdown item without a link', () => {
+ it('renders the LDAP dropdown footer without a link', () => {
const dropdownAction = wrapper.find(`[data-testid="${LDAP}"]`);
expect(dropdownAction.exists()).toBe(true);
expect(dropdownAction.attributes('href')).toBe(undefined);
diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js
index 92f2e62b3a7..c9238c4b636 100644
--- a/spec/frontend/error_tracking/components/error_details_spec.js
+++ b/spec/frontend/error_tracking/components/error_details_spec.js
@@ -17,6 +17,7 @@ import ErrorDetailsInfo from '~/error_tracking/components/error_details_info.vue
import { createAlert, VARIANT_WARNING } from '~/alert';
import { __ } from '~/locale';
import Tracking from '~/tracking';
+import TimelineChart from '~/error_tracking/components/timeline_chart.vue';
jest.mock('~/alert');
jest.mock('~/tracking');
@@ -159,6 +160,7 @@ describe('ErrorDetails', () => {
mocks.$apollo.queries.error.loading = false;
mountComponent();
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // TODO remove setData usage https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2216
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {
@@ -183,6 +185,7 @@ describe('ErrorDetails', () => {
beforeEach(() => {
store.state.details.loadingStacktrace = false;
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // TODO remove setData usage https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2216
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {
@@ -204,6 +207,7 @@ describe('ErrorDetails', () => {
describe('Badges', () => {
it('should show language and error level badges', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // TODO remove setData usage https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2216
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {
@@ -216,6 +220,7 @@ describe('ErrorDetails', () => {
it('should NOT show the badge if the tag is not present', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // TODO remove setData usage https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2216
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {
@@ -230,6 +235,7 @@ describe('ErrorDetails', () => {
'should set correct severity level variant for %s badge',
async (level) => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // TODO remove setData usage https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2216
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {
@@ -245,6 +251,7 @@ describe('ErrorDetails', () => {
it('should fallback for ERROR severityLevelVariant when severityLevel is unknown', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // TODO remove setData usage https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2216
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {
@@ -268,6 +275,32 @@ describe('ErrorDetails', () => {
});
});
+ describe('timeline chart', () => {
+ it('should not show timeline chart if frequency data does not exist', () => {
+ expect(wrapper.findComponent(TimelineChart).exists()).toBe(false);
+ expect(wrapper.text()).not.toContain('Last 24 hours');
+ });
+
+ it('should show timeline chart', async () => {
+ const mockFrequency = [
+ [0, 1],
+ [2, 3],
+ ];
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // TODO remove setData usage https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2216
+ // eslint-disable-next-line no-restricted-syntax
+ wrapper.setData({
+ error: {
+ frequency: mockFrequency,
+ },
+ });
+ await nextTick();
+ expect(wrapper.findComponent(TimelineChart).exists()).toBe(true);
+ expect(wrapper.findComponent(TimelineChart).props('timelineData')).toEqual(mockFrequency);
+ expect(wrapper.text()).toContain('Last 24 hours');
+ });
+ });
+
describe('Stacktrace', () => {
it('should show stacktrace', async () => {
store.state.details.loadingStacktrace = false;
@@ -402,6 +435,7 @@ describe('ErrorDetails', () => {
it('should show alert with closed issueId', async () => {
const closedIssueId = 123;
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // TODO remove setData usage https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2216
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
isAlertVisible: true,
@@ -424,6 +458,7 @@ describe('ErrorDetails', () => {
describe('is present', () => {
beforeEach(() => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // TODO remove setData usage https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2216
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {
@@ -448,6 +483,7 @@ describe('ErrorDetails', () => {
describe('is not present', () => {
beforeEach(() => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // TODO remove setData usage https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2216
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {
@@ -482,6 +518,7 @@ describe('ErrorDetails', () => {
beforeEach(() => {
mountComponent({ integratedErrorTrackingEnabled: integrated });
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // TODO remove setData usage https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2216
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {},
diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
index 5483500e1dd..49f365e8c60 100644
--- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
@@ -12,6 +12,7 @@ import Vuex from 'vuex';
import stubChildren from 'helpers/stub_children';
import ErrorTrackingActions from '~/error_tracking/components/error_tracking_actions.vue';
import ErrorTrackingList from '~/error_tracking/components/error_tracking_list.vue';
+import TimelineChart from '~/error_tracking/components/timeline_chart.vue';
import Tracking from '~/tracking';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import errorsList from './list_mock.json';
@@ -158,6 +159,30 @@ describe('ErrorTrackingList', () => {
});
});
+ describe('timeline graph', () => {
+ it('should show the timeline chart', () => {
+ findErrorListRows().wrappers.forEach((row, index) => {
+ expect(row.findComponent(TimelineChart).exists()).toBe(true);
+ const mockFrequency = errorsList[index].frequency;
+ expect(row.findComponent(TimelineChart).props('timelineData')).toEqual(mockFrequency);
+ });
+ });
+
+ it('should not show the timeline chart if frequency data does not exist', () => {
+ store.state.list.errors = errorsList.map((e) => ({ ...e, frequency: undefined }));
+ mountComponent({
+ stubs: {
+ GlTable: false,
+ GlLink: false,
+ },
+ });
+
+ findErrorListRows().wrappers.forEach((row) => {
+ expect(row.findComponent(TimelineChart).exists()).toBe(false);
+ });
+ });
+ });
+
describe('filtering', () => {
const findSearchBox = () => wrapper.findComponent(GlFormInput);
diff --git a/spec/frontend/error_tracking/components/list_mock.json b/spec/frontend/error_tracking/components/list_mock.json
index 54ae0a4c7cf..f8addef324e 100644
--- a/spec/frontend/error_tracking/components/list_mock.json
+++ b/spec/frontend/error_tracking/components/list_mock.json
@@ -7,7 +7,17 @@
"count": "52",
"firstSeen": "2019-05-30T07:21:46Z",
"lastSeen": "2019-11-06T03:21:39Z",
- "status": "unresolved"
+ "status": "unresolved",
+ "frequency": [
+ [
+ 0,
+ 1
+ ],
+ [
+ 1,
+ 2
+ ]
+ ]
},
{
"id": "2",
@@ -17,7 +27,17 @@
"count": "12",
"firstSeen": "2019-10-19T03:53:56Z",
"lastSeen": "2019-11-05T03:51:54Z",
- "status": "unresolved"
+ "status": "unresolved",
+ "frequency": [
+ [
+ 0,
+ 1
+ ],
+ [
+ 1,
+ 2
+ ]
+ ]
},
{
"id": "3",
@@ -27,6 +47,16 @@
"count": "275",
"firstSeen": "2019-02-12T07:22:36Z",
"lastSeen": "2019-10-22T03:20:48Z",
- "status": "unresolved"
+ "status": "unresolved",
+ "frequency": [
+ [
+ 0,
+ 1
+ ],
+ [
+ 1,
+ 2
+ ]
+ ]
}
-] \ No newline at end of file
+]
diff --git a/spec/frontend/error_tracking/components/timeline_chart_spec.js b/spec/frontend/error_tracking/components/timeline_chart_spec.js
new file mode 100644
index 00000000000..f864d11804c
--- /dev/null
+++ b/spec/frontend/error_tracking/components/timeline_chart_spec.js
@@ -0,0 +1,94 @@
+import { GlChart } from '@gitlab/ui/dist/charts';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import TimelineChart from '~/error_tracking/components/timeline_chart.vue';
+
+const MOCK_HEIGHT = 123;
+
+describe('TimelineChart', () => {
+ let wrapper;
+
+ function mountComponent(timelineData = []) {
+ wrapper = shallowMountExtended(TimelineChart, {
+ stubs: { GlChart: true },
+ propsData: {
+ timelineData: [...timelineData],
+ height: MOCK_HEIGHT,
+ },
+ });
+ }
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('renders the component', () => {
+ expect(wrapper.exists()).toBe(true);
+ });
+
+ it('does not render a chart if timelineData is missing', () => {
+ wrapper = shallowMountExtended(TimelineChart, {
+ stubs: { GlChart: true },
+ propsData: {
+ timelineData: undefined,
+ height: MOCK_HEIGHT,
+ },
+ });
+ expect(wrapper.findComponent(GlChart).exists()).toBe(false);
+ });
+
+ it('renders a gl-chart', () => {
+ expect(wrapper.findComponent(GlChart).exists()).toBe(true);
+ expect(wrapper.findComponent(GlChart).props('height')).toBe(MOCK_HEIGHT);
+ });
+
+ describe('timeline-data', () => {
+ describe.each([
+ {
+ mockItems: [
+ [1686218400, 1],
+ [1686222000, 2],
+ ],
+ expectedX: ['Jun 8, 2023 10:00am UTC', 'Jun 8, 2023 11:00am UTC'],
+ expectedY: [1, 2],
+ description: 'tuples with dates as timestamps in seconds',
+ },
+ {
+ mockItems: [
+ ['06-05-2023', 1],
+ ['06-06-2023', 2],
+ ],
+ expectedX: ['Jun 5, 2023 12:00am UTC', 'Jun 6, 2023 12:00am UTC'],
+ expectedY: [1, 2],
+ description: 'tuples with non-numeric dates',
+ },
+ {
+ mockItems: [
+ { time: 1686218400, count: 1 },
+ { time: 1686222000, count: 2 },
+ ],
+ expectedX: ['Jun 8, 2023 10:00am UTC', 'Jun 8, 2023 11:00am UTC'],
+ expectedY: [1, 2],
+ description: 'objects with dates as timestamps in seconds',
+ },
+ {
+ mockItems: [
+ { time: '06-05-2023', count: 1 },
+ { time: '06-06-2023', count: 2 },
+ ],
+ expectedX: ['Jun 5, 2023 12:00am UTC', 'Jun 6, 2023 12:00am UTC'],
+ expectedY: [1, 2],
+ description: 'objects with non-numeric dates',
+ },
+ ])('when timeline-data items are $description', ({ mockItems, expectedX, expectedY }) => {
+ it(`renders the chart correctly`, () => {
+ mountComponent(mockItems);
+
+ const chartOptions = wrapper.findComponent(GlChart).props('options');
+ const xData = chartOptions.xAxis.data;
+ const yData = chartOptions.series[0].data;
+ expect(xData).toEqual(expectedX);
+ expect(yData).toEqual(expectedY);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/fixtures/users.rb b/spec/frontend/fixtures/users.rb
index 90a6895af91..89bffea7e4c 100644
--- a/spec/frontend/fixtures/users.rb
+++ b/spec/frontend/fixtures/users.rb
@@ -42,7 +42,7 @@ RSpec.describe 'Users (JavaScript fixtures)', feature_category: :user_profile do
context 'for user achievements' do
let_it_be(:group) { create(:group, :public) }
let_it_be(:private_group) { create(:group, :private) }
- let_it_be(:achievement1) { create(:achievement, namespace: group) }
+ let_it_be(:achievement1) { create(:achievement, namespace: group, name: 'Multiple') }
let_it_be(:achievement2) { create(:achievement, namespace: group) }
let_it_be(:achievement3) { create(:achievement, namespace: group) }
let_it_be(:achievement_from_private_group) { create(:achievement, namespace: private_group) }
@@ -94,6 +94,7 @@ RSpec.describe 'Users (JavaScript fixtures)', feature_category: :user_profile do
[achievement1, achievement2, achievement3, achievement_with_avatar_and_description].each do |achievement|
create(:user_achievement, user: user, achievement: achievement)
end
+ create(:user_achievement, user: user, achievement: achievement1)
post_graphql(query, current_user: user, variables: { id: user.to_global_id })
diff --git a/spec/frontend/members/components/table/role_dropdown_spec.js b/spec/frontend/members/components/table/role_dropdown_spec.js
index 1045e3f9849..1285404fd9f 100644
--- a/spec/frontend/members/components/table/role_dropdown_spec.js
+++ b/spec/frontend/members/components/table/role_dropdown_spec.js
@@ -1,8 +1,7 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import * as Sentry from '@sentry/browser';
-import { within } from '@testing-library/dom';
-import { mount, createWrapper } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
@@ -55,59 +54,50 @@ describe('RoleDropdown', () => {
});
};
- const getDropdownMenu = () => within(wrapper.element).getByRole('menu');
- const getByTextInDropdownMenu = (text, options = {}) =>
- createWrapper(within(getDropdownMenu()).getByText(text, options));
- const getDropdownItemByText = (text) =>
- createWrapper(
- within(getDropdownMenu())
- .getByText(text, { selector: '[role="menuitem"] p' })
- .closest('[role="menuitem"]'),
- );
- const getCheckedDropdownItem = () =>
- wrapper
- .findAllComponents(GlDropdownItem)
- .wrappers.find((dropdownItemWrapper) => dropdownItemWrapper.props('isChecked'));
-
- const findDropdownToggle = () => wrapper.find('button[aria-haspopup="menu"]');
- const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findListboxItems = () => wrapper.findAllComponents(GlListboxItem);
+ const findListboxItemByText = (text) =>
+ findListboxItems().wrappers.find((item) => item.text() === text);
beforeEach(() => {
gon.features = { showOverageOnRolePromotion: true };
});
- describe('when dropdown is open', () => {
+ it('has correct header text props', () => {
+ createComponent();
+ expect(findListbox().props('headerText')).toBe('Change role');
+ });
+
+ it('has items prop with all valid roles', () => {
+ createComponent();
+ const roles = findListbox()
+ .props('items')
+ .map((item) => item.text);
+ expect(roles).toEqual(Object.keys(member.validRoles));
+ });
+
+ describe('when listbox is open', () => {
beforeEach(async () => {
guestOverageConfirmAction.mockReturnValue(true);
createComponent();
- await findDropdownToggle().trigger('click');
- });
-
- it('renders all valid roles', () => {
- Object.keys(member.validRoles).forEach((role) => {
- expect(getDropdownItemByText(role).exists()).toBe(true);
- });
- });
-
- it('renders dropdown header', () => {
- expect(getByTextInDropdownMenu('Change role').exists()).toBe(true);
+ await findListbox().vm.$emit('click');
});
it('sets dropdown toggle and checks selected role', () => {
- expect(findDropdownToggle().text()).toBe('Owner');
- expect(getCheckedDropdownItem().text()).toBe('Owner');
+ expect(findListbox().props('toggleText')).toBe('Owner');
+ expect(findListbox().find('[aria-selected=true]').text()).toBe('Owner');
});
describe('when dropdown item is selected', () => {
it('does nothing if the item selected was already selected', async () => {
- await getDropdownItemByText('Owner').trigger('click');
+ await findListboxItemByText('Owner').trigger('click');
expect(actions.updateMemberRole).not.toHaveBeenCalled();
});
it('calls `updateMemberRole` Vuex action', async () => {
- await getDropdownItemByText('Developer').trigger('click');
+ await findListboxItemByText('Developer').trigger('click');
expect(actions.updateMemberRole).toHaveBeenCalledWith(expect.any(Object), {
memberId: member.id,
@@ -117,7 +107,7 @@ describe('RoleDropdown', () => {
describe('when updateMemberRole is successful', () => {
it('displays toast', async () => {
- await getDropdownItemByText('Developer').trigger('click');
+ await findListboxItemByText('Developer').trigger('click');
await nextTick();
@@ -125,21 +115,21 @@ describe('RoleDropdown', () => {
});
it('puts dropdown in loading state while waiting for `updateMemberRole` to resolve', async () => {
- await getDropdownItemByText('Developer').trigger('click');
+ await findListboxItemByText('Developer').trigger('click');
- expect(findDropdown().props('loading')).toBe(true);
+ expect(findListbox().props('loading')).toBe(true);
});
it('enables dropdown after `updateMemberRole` resolves', async () => {
- await getDropdownItemByText('Developer').trigger('click');
+ await findListboxItemByText('Developer').trigger('click');
await waitForPromises();
- expect(findDropdown().props('disabled')).toBe(false);
+ expect(findListbox().props('disabled')).toBe(false);
});
it('does not log error to Sentry', async () => {
- await getDropdownItemByText('Developer').trigger('click');
+ await findListboxItemByText('Developer').trigger('click');
await waitForPromises();
@@ -155,7 +145,7 @@ describe('RoleDropdown', () => {
});
it('does not display toast', async () => {
- await getDropdownItemByText('Developer').trigger('click');
+ await findListboxItemByText('Developer').trigger('click');
await nextTick();
@@ -163,21 +153,21 @@ describe('RoleDropdown', () => {
});
it('puts dropdown in loading state while waiting for `updateMemberRole` to resolve', async () => {
- await getDropdownItemByText('Developer').trigger('click');
+ await findListboxItemByText('Developer').trigger('click');
- expect(findDropdown().props('loading')).toBe(true);
+ expect(findListbox().props('loading')).toBe(true);
});
it('enables dropdown after `updateMemberRole` resolves', async () => {
- await getDropdownItemByText('Developer').trigger('click');
+ await findListboxItemByText('Developer').trigger('click');
await waitForPromises();
- expect(findDropdown().props('disabled')).toBe(false);
+ expect(findListbox().props('disabled')).toBe(false);
});
it('logs error to Sentry', async () => {
- await getDropdownItemByText('Developer').trigger('click');
+ await findListboxItemByText('Developer').trigger('click');
await waitForPromises();
@@ -190,7 +180,7 @@ describe('RoleDropdown', () => {
it("sets initial dropdown toggle value to member's role", () => {
createComponent();
- expect(findDropdownToggle().text()).toBe('Owner');
+ expect(findListbox().props('toggleText')).toBe('Owner');
});
it('sets the dropdown alignment to right on mobile', async () => {
@@ -199,7 +189,7 @@ describe('RoleDropdown', () => {
await nextTick();
- expect(findDropdown().props('right')).toBe(true);
+ expect(findListbox().props('placement')).toBe('right');
});
it('sets the dropdown alignment to left on desktop', async () => {
@@ -208,7 +198,7 @@ describe('RoleDropdown', () => {
await nextTick();
- expect(findDropdown().props('right')).toBe(false);
+ expect(findListbox().props('placement')).toBe('left');
});
describe('guestOverageConfirmAction', () => {
@@ -219,7 +209,7 @@ describe('RoleDropdown', () => {
beforeEach(() => {
createComponent();
- findDropdownToggle().trigger('click');
+ findListbox().vm.$emit('click');
});
afterEach(() => {
@@ -230,7 +220,7 @@ describe('RoleDropdown', () => {
beforeEach(() => {
mockConfirmAction({ confirmed: true });
- getDropdownItemByText('Reporter').trigger('click');
+ findListboxItemByText('Reporter').trigger('click');
});
it('calls updateMemberRole', () => {
@@ -242,7 +232,7 @@ describe('RoleDropdown', () => {
beforeEach(() => {
mockConfirmAction({ confirmed: false });
- getDropdownItemByText('Reporter').trigger('click');
+ findListboxItemByText('Reporter').trigger('click');
});
it('does not call updateMemberRole', () => {
diff --git a/spec/frontend/profile/components/user_achievements_spec.js b/spec/frontend/profile/components/user_achievements_spec.js
index ff6f323621a..5743c8575d5 100644
--- a/spec/frontend/profile/components/user_achievements_spec.js
+++ b/spec/frontend/profile/components/user_achievements_spec.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import { GlBadge } from '@gitlab/ui';
import getUserAchievementsEmptyResponse from 'test_fixtures/graphql/get_user_achievements_empty_response.json';
import getUserAchievementsLongResponse from 'test_fixtures/graphql/get_user_achievements_long_response.json';
import getUserAchievementsResponse from 'test_fixtures/graphql/get_user_achievements_with_avatar_and_description_response.json';
@@ -63,6 +64,14 @@ describe('UserAchievements', () => {
expect(wrapper.findAllByTestId('user-achievement').length).toBe(3);
});
+ it('renders count for achievements awarded more than once', async () => {
+ createComponent({ queryHandler: jest.fn().mockResolvedValue(getUserAchievementsLongResponse) });
+
+ await waitForPromises();
+
+ expect(achievement().findComponent(GlBadge).text()).toBe('2x');
+ });
+
it('renders correctly if the achievement is from a private namespace', async () => {
createComponent({
queryHandler: jest.fn().mockResolvedValue(getUserAchievementsPrivateGroupResponse),
diff --git a/spec/frontend/work_items/components/notes/work_item_add_note_spec.js b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
index 9bb84947db5..e6d20dcb0d9 100644
--- a/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
@@ -32,12 +32,14 @@ describe('Work item add note', () => {
const findCommentForm = () => wrapper.findComponent(WorkItemCommentForm);
const findTextarea = () => wrapper.findByTestId('note-reply-textarea');
+ const findWorkItemLockedComponent = () => wrapper.findComponent(WorkItemCommentLocked);
const createComponent = async ({
mutationHandler = mutationSuccessHandler,
canUpdate = true,
+ canCreateNote = true,
workItemIid = '1',
- workItemResponse = workItemByIidResponseFactory({ canUpdate }),
+ workItemResponse = workItemByIidResponseFactory({ canUpdate, canCreateNote }),
signedIn = true,
isEditing = true,
workItemType = 'Task',
@@ -265,4 +267,13 @@ describe('Work item add note', () => {
expect(wrapper.attributes('class')).toContain('internal-note');
});
+
+ describe('when work item`createNote` permission false', () => {
+ it('cannot add comment', async () => {
+ await createComponent({ isEditing: false, canCreateNote: false });
+
+ expect(findWorkItemLockedComponent().exists()).toBe(true);
+ expect(findCommentForm().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index 84e5e76cb4c..a873462ea63 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -110,6 +110,7 @@ export const workItemQueryResponse = {
updateWorkItem: false,
setWorkItemMetadata: false,
adminParentLink: false,
+ createNote: false,
__typename: 'WorkItemPermissions',
},
widgets: [
@@ -214,6 +215,7 @@ export const updateWorkItemMutationResponse = {
updateWorkItem: false,
setWorkItemMetadata: false,
adminParentLink: false,
+ createNote: false,
__typename: 'WorkItemPermissions',
},
reference: 'test-project-path#1',
@@ -322,6 +324,7 @@ export const convertWorkItemMutationResponse = {
updateWorkItem: false,
setWorkItemMetadata: false,
adminParentLink: false,
+ createNote: false,
__typename: 'WorkItemPermissions',
},
reference: 'gitlab-org/gitlab-test#1',
@@ -418,6 +421,7 @@ export const objectiveType = {
export const workItemResponseFactory = ({
canUpdate = false,
canDelete = false,
+ canCreateNote = false,
adminParentLink = false,
notificationsWidgetPresent = true,
currentUserTodosWidgetPresent = true,
@@ -473,6 +477,7 @@ export const workItemResponseFactory = ({
updateWorkItem: canUpdate,
setWorkItemMetadata: canUpdate,
adminParentLink,
+ createNote: canCreateNote,
__typename: 'WorkItemPermissions',
},
reference: 'test-project-path#1',
@@ -751,6 +756,7 @@ export const createWorkItemMutationResponse = {
updateWorkItem: false,
setWorkItemMetadata: false,
adminParentLink: false,
+ createNote: false,
__typename: 'WorkItemPermissions',
},
reference: 'test-project-path#1',
@@ -980,6 +986,7 @@ export const workItemHierarchyEmptyResponse = {
updateWorkItem: false,
setWorkItemMetadata: false,
adminParentLink: false,
+ createNote: false,
__typename: 'WorkItemPermissions',
},
confidential: false,
@@ -1029,6 +1036,7 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
updateWorkItem: false,
setWorkItemMetadata: false,
adminParentLink: false,
+ createNote: false,
__typename: 'WorkItemPermissions',
},
project: {
@@ -1178,6 +1186,7 @@ export const workItemHierarchyResponse = {
updateWorkItem: true,
setWorkItemMetadata: true,
adminParentLink: true,
+ createNote: true,
__typename: 'WorkItemPermissions',
},
author: {
@@ -1277,6 +1286,7 @@ export const workItemObjectiveWithChild = {
updateWorkItem: true,
setWorkItemMetadata: true,
adminParentLink: true,
+ createNote: true,
__typename: 'WorkItemPermissions',
},
author: {
@@ -1346,6 +1356,7 @@ export const workItemHierarchyTreeResponse = {
updateWorkItem: true,
setWorkItemMetadata: true,
adminParentLink: true,
+ createNote: true,
__typename: 'WorkItemPermissions',
},
confidential: false,
@@ -1426,6 +1437,7 @@ export const changeIndirectWorkItemParentMutationResponse = {
updateWorkItem: true,
setWorkItemMetadata: true,
adminParentLink: true,
+ createNote: true,
__typename: 'WorkItemPermissions',
},
description: null,
@@ -1493,6 +1505,7 @@ export const changeWorkItemParentMutationResponse = {
updateWorkItem: true,
setWorkItemMetadata: true,
adminParentLink: true,
+ createNote: true,
__typename: 'WorkItemPermissions',
},
description: null,
diff --git a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
index ef2acc9ec92..98522c53a47 100644
--- a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
@@ -381,6 +381,125 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler, feature_category: :se
it_behaves_like 'a new issue request'
end
end
+
+ context 'when receiving a service desk custom email address verification email' do
+ let(:email_raw) { service_desk_fixture('emails/service_desk_custom_email_address_verification.eml') }
+
+ shared_examples 'an early exiting handler' do
+ it 'does not trigger the verification process and does not add an issue' do
+ expect(ServiceDesk::CustomEmailVerifications::UpdateService).to receive(:execute).exactly(0).times
+ expect { receiver.execute }.to not_change { Issue.count }
+ end
+ end
+
+ shared_examples 'a handler that does not verify the custom email' do |error_identifier|
+ it 'does not verify the custom email address' do
+ # project has no owner, so only notify verification triggerer
+ expect(Notify).to receive(:service_desk_verification_result_email).once
+
+ receiver.execute
+
+ expect(settings.reload.custom_email_enabled).to be false
+ expect(verification.reload).to have_attributes(
+ state: 'failed',
+ error: error_identifier
+ )
+ end
+ end
+
+ shared_examples 'a handler that verifies Service Desk custom email verification emails' do
+ it_behaves_like 'an early exiting handler'
+
+ context 'with valid service desk settings' do
+ let_it_be(:user) { create(:user) }
+
+ let!(:settings) { create(:service_desk_setting, project: project, custom_email: 'custom-support-email@example.com') }
+ let!(:verification) { create(:service_desk_custom_email_verification, project: project, token: 'ZROT4ZZXA-Y6', triggerer: user) }
+
+ let(:message_delivery) { instance_double(ActionMailer::MessageDelivery) }
+
+ before do
+ project.add_maintainer(user)
+
+ allow(message_delivery).to receive(:deliver_later)
+ allow(Notify).to receive(:service_desk_verification_result_email).and_return(message_delivery)
+ end
+
+ it 'successfully verifies the custom email address' do
+ # project has no owner, so only notify verification triggerer
+ expect(Notify).to receive(:service_desk_verification_result_email).once
+
+ receiver.execute
+
+ expect(settings.reload.custom_email_enabled).to be false
+ expect(verification.reload).to have_attributes(
+ state: 'finished',
+ error: nil
+ )
+ end
+
+ context 'and custom email address is not the configured subaddress of the project' do
+ before do
+ settings.update!(custom_email: 'custom-support-email@example.com')
+ end
+
+ it_behaves_like 'an early exiting handler'
+ end
+
+ context 'and verification tokens do not match' do
+ before do
+ verification.update!(token: 'XXXXXXXXXXXX')
+ end
+
+ it_behaves_like 'a handler that does not verify the custom email', 'incorrect_token'
+ end
+
+ context 'and verification email ingested too late' do
+ before do
+ verification.update!(triggered_at: ServiceDesk::CustomEmailVerification::TIMEFRAME.ago)
+ end
+
+ it_behaves_like 'a handler that does not verify the custom email', 'mail_not_received_within_timeframe'
+ end
+
+ context 'and from header differs from custom email address' do
+ before do
+ settings.update!(custom_email: 'different-from@example.com')
+ end
+
+ it_behaves_like 'a handler that does not verify the custom email', 'incorrect_from'
+ end
+ end
+
+ context 'when service_desk_custom_email feature flag is disabled' do
+ before do
+ stub_feature_flags(service_desk_custom_email: false)
+ end
+
+ it 'does not trigger the verification process and adds an issue instead' do
+ expect { receiver.execute }.to change { Issue.count }.by(1)
+ end
+ end
+ end
+
+ context 'when using incoming_email address' do
+ before do
+ stub_incoming_email_setting(enabled: true, address: 'support+%{key}@example.com')
+ end
+
+ it_behaves_like 'a handler that verifies Service Desk custom email verification emails'
+ end
+
+ context 'when using service_desk_email address' do
+ let(:receiver) { Gitlab::Email::ServiceDeskReceiver.new(email_raw) }
+
+ before do
+ stub_service_desk_email_setting(enabled: true, address: 'support+%{key}@example.com')
+ end
+
+ it_behaves_like 'a handler that verifies Service Desk custom email verification emails'
+ end
+ end
end
context 'when issue email creation fails' do
diff --git a/spec/lib/gitlab/metrics/environment_spec.rb b/spec/lib/gitlab/metrics/environment_spec.rb
index e94162e625e..4e3b1b5273e 100644
--- a/spec/lib/gitlab/metrics/environment_spec.rb
+++ b/spec/lib/gitlab/metrics/environment_spec.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require 'rspec-parameterized'
-
-require_relative '../../../support/helpers/stub_env'
+require 'gitlab/rspec/all'
RSpec.describe Gitlab::Metrics::Environment, feature_category: :error_budgets do
include StubENV
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index bb96771b3d5..53e216323d1 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -223,14 +223,6 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
include_examples 'includes container_registry_access_level'
- context 'when projects_preloader_fix is disabled' do
- before do
- stub_feature_flags(projects_preloader_fix: false)
- end
-
- include_examples 'includes container_registry_access_level'
- end
-
it 'includes various project feature fields' do
get api(path, user)
project_response = json_response.find { |p| p['id'] == project.id }
diff --git a/spec/scripts/generate_failed_package_and_test_mr_message_spec.rb b/spec/scripts/generate_failed_package_and_test_mr_message_spec.rb
index 79e63c95f65..7aff33855aa 100644
--- a/spec/scripts/generate_failed_package_and_test_mr_message_spec.rb
+++ b/spec/scripts/generate_failed_package_and_test_mr_message_spec.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
require 'fast_spec_helper'
+require 'gitlab/rspec/all'
require_relative '../../scripts/generate-failed-package-and-test-mr-message'
-require_relative '../support/helpers/stub_env'
RSpec.describe GenerateFailedPackageAndTestMrMessage, feature_category: :tooling do
include StubENV
diff --git a/spec/scripts/generate_message_to_run_e2e_pipeline_spec.rb b/spec/scripts/generate_message_to_run_e2e_pipeline_spec.rb
index aee16334003..61307937101 100644
--- a/spec/scripts/generate_message_to_run_e2e_pipeline_spec.rb
+++ b/spec/scripts/generate_message_to_run_e2e_pipeline_spec.rb
@@ -3,8 +3,8 @@
# rubocop:disable RSpec/VerifiedDoubles
require 'fast_spec_helper'
+require 'gitlab/rspec/all'
require_relative '../../scripts/generate-message-to-run-e2e-pipeline'
-require_relative '../support/helpers/stub_env'
RSpec.describe GenerateMessageToRunE2ePipeline, feature_category: :tooling do
include StubENV
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 4d533b67690..4271a71372e 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -1024,4 +1024,61 @@ RSpec.describe MergeRequests::RefreshService, feature_category: :code_review_wor
end
end
end
+
+ describe '#abort_auto_merges' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:author) { user }
+
+ let_it_be(:merge_request, refind: true) do
+ create(
+ :merge_request,
+ source_project: project,
+ target_project: project,
+ merge_user: user,
+ auto_merge_enabled: true,
+ auto_merge_strategy: AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS
+ )
+ end
+
+ let(:service) { described_class.new(project: project, current_user: user) }
+ let(:oldrev) { merge_request.diff_refs.base_sha }
+ let(:newrev) { merge_request.diff_refs.head_sha }
+ let(:merge_sha) { oldrev }
+ let(:fix_interrupted_mwps) { true }
+
+ before do
+ stub_feature_flags(fix_interrupted_mwps: fix_interrupted_mwps)
+
+ merge_request.merge_params[:sha] = merge_sha
+ merge_request.save!
+
+ service.execute(oldrev, newrev, "refs/heads/#{merge_request.source_branch}")
+
+ merge_request.reload
+ end
+
+ it 'aborts MWPS for merge requests' do
+ expect(merge_request.auto_merge_enabled?).to be_falsey
+ expect(merge_request.merge_user).to be_nil
+ end
+
+ context 'when merge params contains up-to-date sha' do
+ let(:merge_sha) { newrev }
+
+ it 'maintains MWPS for merge requests' do
+ expect(merge_request.auto_merge_enabled?).to be_truthy
+ expect(merge_request.merge_user).to eq(user)
+ end
+
+ context 'when fix_interrupted_mwps ff is disabled' do
+ let(:fix_interrupted_mwps) { false }
+
+ it 'aborts MWPS for merge requests' do
+ expect(merge_request.auto_merge_enabled?).to be_falsey
+ expect(merge_request.merge_user).to be_nil
+ end
+ end
+ end
+ end
end
diff --git a/spec/support/rspec.rb b/spec/support/rspec.rb
index 94c43669173..b34f8fe9a22 100644
--- a/spec/support/rspec.rb
+++ b/spec/support/rspec.rb
@@ -5,10 +5,9 @@ require_relative "system_exit_detected"
require_relative "helpers/stub_configuration"
require_relative "helpers/stub_metrics"
require_relative "helpers/stub_object_storage"
-require_relative "helpers/stub_env"
require_relative "helpers/fast_rails_root"
-require_relative "../../lib/gitlab/utils"
+require 'gitlab/rspec/all'
RSpec::Expectations.configuration.on_potential_false_positives = :raise
diff --git a/spec/support/shared_contexts/email_shared_context.rb b/spec/support/shared_contexts/email_shared_context.rb
index 12d4af5170b..8b4c1c1e243 100644
--- a/spec/support/shared_contexts/email_shared_context.rb
+++ b/spec/support/shared_contexts/email_shared_context.rb
@@ -24,7 +24,10 @@ end
def service_desk_fixture(path, slug: nil, key: 'mykey')
slug ||= project.full_path_slug.to_s
- fixture_file(path).gsub('project_slug', slug).gsub('project_key', key)
+ fixture_file(path)
+ .gsub('project_slug', slug)
+ .gsub('project_key', key)
+ .gsub('project_id', project.project_id.to_s)
end
RSpec.shared_examples 'reply processing shared examples' do
diff --git a/spec/tooling/danger/project_helper_spec.rb b/spec/tooling/danger/project_helper_spec.rb
index 3910f569400..fb0603a207a 100644
--- a/spec/tooling/danger/project_helper_spec.rb
+++ b/spec/tooling/danger/project_helper_spec.rb
@@ -5,9 +5,9 @@ require 'gitlab-dangerfiles'
require 'danger'
require 'danger/plugins/internal/helper'
require 'gitlab/dangerfiles/spec_helper'
+require 'gitlab/rspec/all'
require_relative '../../../danger/plugins/project_helper'
-require_relative '../../../spec/support/helpers/stub_env'
RSpec.describe Tooling::Danger::ProjectHelper do
include StubENV
diff --git a/spec/tooling/lib/tooling/find_changes_spec.rb b/spec/tooling/lib/tooling/find_changes_spec.rb
index 43c3da5699d..fef29ad3f2c 100644
--- a/spec/tooling/lib/tooling/find_changes_spec.rb
+++ b/spec/tooling/lib/tooling/find_changes_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require_relative '../../../../tooling/lib/tooling/find_changes'
-require_relative '../../../support/helpers/stub_env'
+require 'gitlab/rspec/all'
require 'json'
require 'tempfile'
diff --git a/spec/tooling/lib/tooling/find_tests_spec.rb b/spec/tooling/lib/tooling/find_tests_spec.rb
index 905f81c4bbd..67b6650b335 100644
--- a/spec/tooling/lib/tooling/find_tests_spec.rb
+++ b/spec/tooling/lib/tooling/find_tests_spec.rb
@@ -2,7 +2,7 @@
require 'tempfile'
require_relative '../../../../tooling/lib/tooling/find_tests'
-require_relative '../../../support/helpers/stub_env'
+require 'gitlab/rspec/all'
RSpec.describe Tooling::FindTests, feature_category: :tooling do
include StubENV
diff --git a/spec/tooling/lib/tooling/gettext_extractor_spec.rb b/spec/tooling/lib/tooling/gettext_extractor_spec.rb
index 3c0f91342c2..14310c804f1 100644
--- a/spec/tooling/lib/tooling/gettext_extractor_spec.rb
+++ b/spec/tooling/lib/tooling/gettext_extractor_spec.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
require 'rspec/parameterized'
+require 'gitlab/rspec/all'
require_relative '../../../../tooling/lib/tooling/gettext_extractor'
-require_relative '../../../support/helpers/stub_env'
require_relative '../../../support/tmpdir'
RSpec.describe Tooling::GettextExtractor, feature_category: :tooling do
diff --git a/spec/tooling/lib/tooling/predictive_tests_spec.rb b/spec/tooling/lib/tooling/predictive_tests_spec.rb
index b82364fe6f6..fdb7d09a3e2 100644
--- a/spec/tooling/lib/tooling/predictive_tests_spec.rb
+++ b/spec/tooling/lib/tooling/predictive_tests_spec.rb
@@ -2,8 +2,8 @@
require 'tempfile'
require 'fileutils'
+require 'gitlab/rspec/all'
require_relative '../../../../tooling/lib/tooling/predictive_tests'
-require_relative '../../../support/helpers/stub_env'
RSpec.describe Tooling::PredictiveTests, feature_category: :tooling do
include StubENV
diff --git a/spec/tooling/rspec_flaky/config_spec.rb b/spec/tooling/rspec_flaky/config_spec.rb
index 63f42d7c6cc..8ba4dd1640e 100644
--- a/spec/tooling/rspec_flaky/config_spec.rb
+++ b/spec/tooling/rspec_flaky/config_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'rspec-parameterized'
-require_relative '../../support/helpers/stub_env'
+require 'gitlab/rspec/all'
require_relative '../../../tooling/rspec_flaky/config'
diff --git a/spec/tooling/rspec_flaky/flaky_example_spec.rb b/spec/tooling/rspec_flaky/flaky_example_spec.rb
index 511f3286f56..68e40bc1050 100644
--- a/spec/tooling/rspec_flaky/flaky_example_spec.rb
+++ b/spec/tooling/rspec_flaky/flaky_example_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative '../../support/helpers/stub_env'
+require 'gitlab/rspec/all'
require_relative '../../support/time_travel'
require_relative '../../../tooling/rspec_flaky/flaky_example'
diff --git a/spec/tooling/rspec_flaky/listener_spec.rb b/spec/tooling/rspec_flaky/listener_spec.rb
index 0bbd6454969..c0b71403347 100644
--- a/spec/tooling/rspec_flaky/listener_spec.rb
+++ b/spec/tooling/rspec_flaky/listener_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative '../../support/helpers/stub_env'
+require 'gitlab/rspec/all'
require_relative '../../support/time_travel'
require_relative '../../../tooling/rspec_flaky/listener'