diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-19 18:09:36 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-19 18:09:36 +0300 |
commit | 8bb837c4d180720d4d923ef2e7bd2c9a46ca97a0 (patch) | |
tree | 7dcb166661ba29fb6cd5935f0db34eee6c935388 | |
parent | eef2437c0a359ec3437d31d1b1ea959e54c71458 (diff) |
Add latest changes from gitlab-org/gitlab@master
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 @@ -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 Binary files differdeleted file mode 100644 index 63d272ae42a..00000000000 --- a/doc/ci/img/environments_monitoring.png +++ /dev/null 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 Binary files differdeleted file mode 100644 index 2900e5d1877..00000000000 --- a/doc/topics/autodevops/img/auto_monitoring.png +++ /dev/null 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' |