diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-01-12 09:15:13 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-01-12 09:15:13 +0000 |
commit | 612dd7d31ab927dd79968a6be7cb36599291bace (patch) | |
tree | 6af6f616794ec0bd40bec2174d5bc58a4231fb21 | |
parent | 563d0d3bc956f6d6a9805720dade3b72bd488043 (diff) | |
download | gitlab-ce-612dd7d31ab927dd79968a6be7cb36599291bace.tar.gz |
Add latest changes from gitlab-org/gitlab@master
72 files changed, 707 insertions, 166 deletions
@@ -170,7 +170,7 @@ gem 'asciidoctor-kroki', '~> 0.5.0', require: false gem 'rouge', '~> 3.27.0' gem 'truncato', '~> 0.7.11' gem 'bootstrap_form', '~> 4.2.0' -gem 'nokogiri', '~> 1.11.4' +gem 'nokogiri', '~> 1.12' gem 'escape_utils', '~> 1.1' # Calendar rendering @@ -264,7 +264,7 @@ gem 'ruby-fogbugz', '~> 0.2.1' gem 'kubeclient', '~> 4.9.2' # Sanitize user input -gem 'sanitize', '~> 5.2.1' +gem 'sanitize', '~> 6.0' gem 'babosa', '~> 1.0.4' # Sanitizes SVG input @@ -277,7 +277,7 @@ gem 'licensee', '~> 9.14.1' gem 'charlock_holmes', '~> 0.7.7' # Detect mime content type from content -gem 'ruby-magic', '~> 0.4' +gem 'ruby-magic', '~> 0.5' # Faster blank gem 'fast_blank' diff --git a/Gemfile.lock b/Gemfile.lock index 645a38e155b..69e37a8f430 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -754,7 +754,7 @@ GEM mini_histogram (0.3.1) mini_magick (4.10.1) mini_mime (1.1.1) - mini_portile2 (2.5.3) + mini_portile2 (2.6.1) minitest (5.11.3) mixlib-cli (2.1.8) mixlib-config (3.0.9) @@ -792,11 +792,9 @@ GEM netrc (0.11.0) nio4r (2.5.8) no_proxy_fix (0.1.2) - nokogiri (1.11.7) - mini_portile2 (~> 2.5.0) + nokogiri (1.12.5) + mini_portile2 (~> 2.6.1) racc (~> 1.4) - nokogumbo (2.0.2) - nokogiri (~> 1.8, >= 1.8.4) notiffany (0.1.3) nenv (~> 0.1) shellany (~> 0.0) @@ -954,7 +952,7 @@ GEM puma (>= 2.7) pyu-ruby-sasl (0.0.3.3) raabro (1.1.6) - racc (1.5.2) + racc (1.6.0) rack (2.2.3) rack-accept (0.4.5) rack (>= 0.4) @@ -1126,8 +1124,8 @@ GEM rubocop-ast (>= 0.7.1) ruby-fogbugz (0.2.1) crack (~> 0.4) - ruby-magic (0.4.0) - mini_portile2 (~> 2.5.0) + ruby-magic (0.5.3) + mini_portile2 (~> 2.6) ruby-prof (1.3.1) ruby-progressbar (1.11.0) ruby-saml (1.13.0) @@ -1144,10 +1142,9 @@ GEM safe_yaml (1.0.4) safety_net_attestation (0.4.0) jwt (~> 2.0) - sanitize (5.2.1) + sanitize (6.0.0) crass (~> 1.0.2) - nokogiri (>= 1.8.0) - nokogumbo (~> 2.0) + nokogiri (>= 1.12.0) sass (3.5.5) sass-listen (~> 4.0.0) sass-listen (4.0.0) @@ -1549,7 +1546,7 @@ DEPENDENCIES net-ldap (~> 0.16.3) net-ntp net-ssh (~> 6.0) - nokogiri (~> 1.11.4) + nokogiri (~> 1.12) oauth2 (~> 1.4) octokit (~> 4.15) ohai (~> 16.10) @@ -1617,14 +1614,14 @@ DEPENDENCIES rspec_junit_formatter rspec_profiling (~> 0.0.6) ruby-fogbugz (~> 0.2.1) - ruby-magic (~> 0.4) + ruby-magic (~> 0.5) ruby-prof (~> 1.3.0) ruby-progressbar (~> 1.10) ruby-saml (~> 1.13.0) ruby_parser (~> 3.15) rubyzip (~> 2.0.0) rugged (~> 1.2) - sanitize (~> 5.2.1) + sanitize (~> 6.0) sassc-rails (~> 2.1.0) sd_notify (~> 0.1.0) seed-fu (~> 2.3.7) diff --git a/app/assets/javascripts/admin/users/components/user_actions.vue b/app/assets/javascripts/admin/users/components/user_actions.vue index 567d7151847..f5d21ece138 100644 --- a/app/assets/javascripts/admin/users/components/user_actions.vue +++ b/app/assets/javascripts/admin/users/components/user_actions.vue @@ -109,12 +109,13 @@ export default { <div v-if="hasDropdownActions" class="gl-p-2"> <gl-dropdown data-testid="dropdown-toggle" - right :text="$options.i18n.userAdministration" :text-sr-only="!showButtonLabels" icon="ellipsis_h" data-qa-selector="user_actions_dropdown_toggle" :data-qa-username="user.username" + no-caret + right > <gl-dropdown-section-header>{{ $options.i18n.userAdministration diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index 563bed6a6b8..dc821cb9f58 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -72,7 +72,7 @@ export default { data-qa-selector="board_card" :class="{ 'multi-select': multiSelectVisible, - 'user-can-drag': isDraggable, + 'gl-cursor-grab': isDraggable, 'is-disabled': isDisabled, 'is-active': isActive, 'gl-cursor-not-allowed gl-bg-gray-10': item.isLoading, diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue index 19004518edf..6835d83a66c 100644 --- a/app/assets/javascripts/boards/components/board_list_header.vue +++ b/app/assets/javascripts/boards/components/board_list_header.vue @@ -263,7 +263,7 @@ export default { > <h3 :class="{ - 'user-can-drag': userCanDrag, + 'gl-cursor-grab': userCanDrag, 'gl-py-3 gl-h-full': list.collapsed && !isSwimlanesHeader, 'gl-border-b-0': list.collapsed || isSwimlanesHeader, 'gl-py-2': list.collapsed && isSwimlanesHeader, diff --git a/app/assets/javascripts/cycle_analytics/components/stage_table.vue b/app/assets/javascripts/cycle_analytics/components/stage_table.vue index fc4dfafb809..1139403bcf7 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_table.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_table.vue @@ -89,6 +89,11 @@ export default { required: false, default: true, }, + includeProjectName: { + type: Boolean, + required: false, + default: false, + }, }, data() { if (this.pagination) { @@ -144,8 +149,13 @@ export default { isMrLink(url = '') { return url.includes('/merge_request'); }, - itemId({ url, iid }) { - return this.isMrLink(url) ? `!${iid}` : `#${iid}`; + itemId({ iid, projectPath }, separator = '#') { + const prefix = this.includeProjectName ? projectPath : ''; + return `${prefix}${separator}${iid}`; + }, + itemDisplayName(item) { + const separator = this.isMrLink(item.url) ? '!' : '#'; + return this.itemId(item, separator); }, itemTitle(item) { return item.title || item.name; @@ -201,8 +211,11 @@ export default { <div data-testid="vsa-stage-event"> <div v-if="item.id" data-testid="vsa-stage-content"> <p class="gl-m-0"> - <gl-link class="gl-text-black-normal pipeline-id" :href="item.url" - >#{{ item.id }}</gl-link + <gl-link + data-testid="vsa-stage-event-link" + class="gl-text-black-normal pipeline-id" + :href="item.url" + >{{ itemId(item.id, '#') }}</gl-link > <gl-icon :size="16" name="fork" /> <gl-link @@ -240,7 +253,12 @@ export default { <gl-link class="gl-text-black-normal" :href="item.url">{{ itemTitle(item) }}</gl-link> </h5> <p class="gl-m-0"> - <gl-link class="gl-text-black-normal" :href="item.url">{{ itemId(item) }}</gl-link> + <gl-link + data-testid="vsa-stage-event-link" + class="gl-text-black-normal" + :href="item.url" + >{{ itemDisplayName(item) }}</gl-link + > <span class="gl-font-lg">·</span> <span data-testid="vsa-stage-event-date"> {{ s__('OpenedNDaysAgo|Opened') }} diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue index 167130cb6f2..ca38c83547b 100644 --- a/app/assets/javascripts/integrations/edit/components/integration_form.vue +++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue @@ -216,62 +216,69 @@ export default { v-bind="propsSource.jiraIssuesProps" @request-jira-issue-types="onRequestJiraIssueTypes" /> - <div v-if="isEditable" class="footer-block row-content-block"> - <template v-if="isInstanceOrGroupLevel"> + + <div + v-if="isEditable" + class="footer-block row-content-block gl-display-flex gl-justify-content-space-between" + > + <div> + <template v-if="isInstanceOrGroupLevel"> + <gl-button + v-gl-modal.confirmSaveIntegration + category="primary" + variant="confirm" + :loading="isSaving" + :disabled="disableButtons" + data-testid="save-button-instance-group" + data-qa-selector="save_changes_button" + > + {{ __('Save changes') }} + </gl-button> + <confirmation-modal @submit="onSaveClick" /> + </template> <gl-button - v-gl-modal.confirmSaveIntegration + v-else category="primary" variant="confirm" + type="submit" :loading="isSaving" :disabled="disableButtons" - data-testid="save-button-instance-group" + data-testid="save-button" data-qa-selector="save_changes_button" + @click.prevent="onSaveClick" > {{ __('Save changes') }} </gl-button> - <confirmation-modal @submit="onSaveClick" /> - </template> - <gl-button - v-else - category="primary" - variant="confirm" - type="submit" - :loading="isSaving" - :disabled="disableButtons" - data-testid="save-button" - data-qa-selector="save_changes_button" - @click.prevent="onSaveClick" - > - {{ __('Save changes') }} - </gl-button> - <gl-button - v-if="showTestButton" - category="secondary" - variant="confirm" - :loading="isTesting" - :disabled="disableButtons" - data-testid="test-button" - @click.prevent="onTestClick" - > - {{ __('Test settings') }} - </gl-button> + <gl-button + v-if="showTestButton" + category="secondary" + variant="confirm" + :loading="isTesting" + :disabled="disableButtons" + data-testid="test-button" + @click.prevent="onTestClick" + > + {{ __('Test settings') }} + </gl-button> + + <gl-button :href="propsSource.cancelPath">{{ __('Cancel') }}</gl-button> + </div> <template v-if="showResetButton"> <gl-button v-gl-modal.confirmResetIntegration - category="secondary" - variant="confirm" + category="tertiary" + variant="danger" :loading="isResetting" :disabled="disableButtons" data-testid="reset-button" > {{ __('Reset') }} </gl-button> + <reset-confirmation-modal @reset="onResetClick" /> </template> - - <gl-button :href="propsSource.cancelPath">{{ __('Cancel') }}</gl-button> </div> </div> </div> diff --git a/app/assets/javascripts/related_issues/components/related_issues_list.vue b/app/assets/javascripts/related_issues/components/related_issues_list.vue index 58138655241..8b39851405e 100644 --- a/app/assets/javascripts/related_issues/components/related_issues_list.vue +++ b/app/assets/javascripts/related_issues/components/related_issues_list.vue @@ -110,7 +110,7 @@ export default { v-for="issue in relatedIssues" :key="issue.id" :class="{ - 'user-can-drag': canReorder, + 'gl-cursor-grab': canReorder, 'sortable-row': canReorder, 'card card-slim': canReorder, }" diff --git a/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue b/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue index 1e9e866d382..82ee41d6def 100644 --- a/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue +++ b/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue @@ -1,6 +1,6 @@ <script> import { GlButton, GlButtonGroup, GlModalDirective, GlTooltipDirective } from '@gitlab/ui'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { __, s__, sprintf } from '~/locale'; import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphql'; import runnerActionsUpdateMutation from '~/runner/graphql/runner_actions_update.mutation.graphql'; @@ -139,7 +139,7 @@ export default { onError(error) { const { message } = error; - createFlash({ message }); + createAlert({ message }); this.reportToSentry(error); }, diff --git a/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue b/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue index 3bb15bff8d8..edcbcb2bf69 100644 --- a/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue +++ b/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue @@ -1,6 +1,6 @@ <script> import { GlDropdownItem, GlLoadingIcon } from '@gitlab/ui'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { TYPE_GROUP, TYPE_PROJECT } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; import { __, s__ } from '~/locale'; @@ -91,7 +91,7 @@ export default { }, onError(error) { const { message } = error; - createFlash({ message }); + createAlert({ message }); this.reportToSentry(error); }, diff --git a/app/assets/javascripts/runner/components/runner_update_form.vue b/app/assets/javascripts/runner/components/runner_update_form.vue index 9a6fc07f6dd..aeb5869bcce 100644 --- a/app/assets/javascripts/runner/components/runner_update_form.vue +++ b/app/assets/javascripts/runner/components/runner_update_form.vue @@ -11,7 +11,7 @@ import { modelToUpdateMutationVariables, runnerToModel, } from 'ee_else_ce/runner/runner_details/runner_update_form_utils'; -import createFlash, { FLASH_TYPES } from '~/flash'; +import { createAlert, VARIANT_SUCCESS } from '~/flash'; import { __ } from '~/locale'; import { captureException } from '~/runner/sentry_utils'; import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED, PROJECT_TYPE } from '../constants'; @@ -75,14 +75,14 @@ export default { if (errors?.length) { // Validation errors need not be thrown - createFlash({ message: errors[0] }); + createAlert({ message: errors[0] }); return; } this.onSuccess(); } catch (error) { const { message } = error; - createFlash({ message }); + createAlert({ message }); this.reportToSentry(error); } finally { @@ -90,7 +90,7 @@ export default { } }, onSuccess() { - createFlash({ message: __('Changes saved.'), type: FLASH_TYPES.SUCCESS }); + createAlert({ message: __('Changes saved.'), variant: VARIANT_SUCCESS }); this.model = runnerToModel(this.runner); }, reportToSentry(error) { diff --git a/app/assets/javascripts/runner/components/search_tokens/tag_token.vue b/app/assets/javascripts/runner/components/search_tokens/tag_token.vue index 7461308ab91..59230bb809e 100644 --- a/app/assets/javascripts/runner/components/search_tokens/tag_token.vue +++ b/app/assets/javascripts/runner/components/search_tokens/tag_token.vue @@ -1,6 +1,6 @@ <script> import { GlFilteredSearchSuggestion, GlToken } from '@gitlab/ui'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { s__ } from '~/locale'; @@ -50,7 +50,7 @@ export default { try { this.tags = await this.getTagsOptions(searchTerm); } catch { - createFlash({ + createAlert({ message: s__('Runners|Something went wrong while fetching the tags suggestions'), }); } finally { diff --git a/app/assets/javascripts/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/runner/group_runners/group_runners_app.vue index a58a53a6a0d..f33f28c11e3 100644 --- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue +++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue @@ -1,6 +1,6 @@ <script> import { GlLink } from '@gitlab/ui'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { fetchPolicies } from '~/lib/graphql'; import { updateHistory } from '~/lib/utils/url_utility'; import { formatNumber, sprintf, s__ } from '~/locale'; @@ -84,7 +84,7 @@ export default { }; }, error(error) { - createFlash({ message: I18N_FETCH_ERROR }); + createAlert({ message: I18N_FETCH_ERROR }); this.reportToSentry(error); }, diff --git a/app/assets/javascripts/runner/runner_details/runner_details_app.vue b/app/assets/javascripts/runner/runner_details/runner_details_app.vue index 7b5f4a90351..f0a1d781a3a 100644 --- a/app/assets/javascripts/runner/runner_details/runner_details_app.vue +++ b/app/assets/javascripts/runner/runner_details/runner_details_app.vue @@ -1,5 +1,5 @@ <script> -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { TYPE_CI_RUNNER } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; import RunnerHeader from '../components/runner_header.vue'; @@ -34,7 +34,7 @@ export default { }; }, error(error) { - createFlash({ message: I18N_FETCH_ERROR }); + createAlert({ message: I18N_FETCH_ERROR }); this.reportToSentry(error); }, diff --git a/app/assets/stylesheets/page_bundles/boards.scss b/app/assets/stylesheets/page_bundles/boards.scss index d4c59a6ab0c..f91ca489bdf 100644 --- a/app/assets/stylesheets/page_bundles/boards.scss +++ b/app/assets/stylesheets/page_bundles/boards.scss @@ -1,9 +1,5 @@ @import 'mixins_and_variables_and_functions'; -.user-can-drag { - cursor: grab; -} - .is-ghost { opacity: 0.3; pointer-events: none; diff --git a/app/assets/stylesheets/page_bundles/issues_list.scss b/app/assets/stylesheets/page_bundles/issues_list.scss index 8a958bdf0c5..41515a98e0a 100644 --- a/app/assets/stylesheets/page_bundles/issues_list.scss +++ b/app/assets/stylesheets/page_bundles/issues_list.scss @@ -35,10 +35,6 @@ } } -.user-can-drag { - cursor: grab; -} - .is-ghost { opacity: 0.3; pointer-events: none; diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index b1a5e4068cc..5aa2aca37f3 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -7,7 +7,7 @@ module IssuesHelper classes = ["issue"] classes << "closed" if issue.closed? classes << "today" if issue.new? - classes << "user-can-drag" if @sort == 'relative_position' + classes << "gl-cursor-grab" if @sort == 'relative_position' classes.join(' ') end diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 943abdc7c1c..453c6bce362 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -8,6 +8,7 @@ class Deployment < ApplicationRecord include Importable include Gitlab::Utils::StrongMemoize include FastDestroyAll + include FromUnion StatusUpdateError = Class.new(StandardError) StatusSyncError = Class.new(StandardError) diff --git a/app/models/preloaders/environments/deployment_preloader.rb b/app/models/preloaders/environments/deployment_preloader.rb new file mode 100644 index 00000000000..fcf892698bb --- /dev/null +++ b/app/models/preloaders/environments/deployment_preloader.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Preloaders + module Environments + # This class is to batch-load deployments of multiple environments. + # The deployments to batch-load are fetched using UNION of N selects in a single query instead of default scoping with `IN (environment_id1, environment_id2 ...)`. + # See https://gitlab.com/gitlab-org/gitlab/-/issues/345672#note_761852224 for more details. + class DeploymentPreloader + attr_reader :environments + + def initialize(environments) + @environments = environments + end + + def execute_with_union(association_name, association_attributes) + load_deployment_association(association_name, association_attributes) + end + + private + + def load_deployment_association(association_name, association_attributes) + return unless environments.present? + + union_arg = environments.inject([]) do |result, environment| + result << environment.association(association_name).scope + end + + union_sql = Deployment.from_union(union_arg).to_sql + + deployments = Deployment + .from("(#{union_sql}) #{::Deployment.table_name}") + .preload(association_attributes) + + deployments_by_environment_id = deployments.index_by(&:environment_id) + + environments.each do |environment| + environment.association(association_name).target = deployments_by_environment_id[environment.id] + environment.association(association_name).loaded! + end + end + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 0e0388b52b7..3dacff4a989 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -48,7 +48,7 @@ class User < ApplicationRecord add_authentication_token_field :incoming_email_token, token_generator: -> { SecureRandom.hex.to_i(16).to_s(36) } add_authentication_token_field :feed_token - add_authentication_token_field :static_object_token + add_authentication_token_field :static_object_token, encrypted: :optional default_value_for :admin, false default_value_for(:external) { Gitlab::CurrentSettings.user_default_external } diff --git a/app/serializers/analytics_build_entity.rb b/app/serializers/analytics_build_entity.rb index 99663c8d5eb..5beb945257f 100644 --- a/app/serializers/analytics_build_entity.rb +++ b/app/serializers/analytics_build_entity.rb @@ -9,6 +9,9 @@ class AnalyticsBuildEntity < Grape::Entity expose :ref, as: :branch expose :short_sha expose :author, using: UserEntity + expose :project_path do |build| + build.project.path + end expose :started_at, as: :date do |build| interval_in_words(build[:started_at]) diff --git a/app/serializers/analytics_issue_entity.rb b/app/serializers/analytics_issue_entity.rb index 307ce14a921..b3244740ae1 100644 --- a/app/serializers/analytics_issue_entity.rb +++ b/app/serializers/analytics_issue_entity.rb @@ -6,6 +6,9 @@ class AnalyticsIssueEntity < Grape::Entity expose :title expose :author, using: UserEntity + expose :project_path do |object| + object[:project_path] + end expose :iid do |object| object[:iid].to_s diff --git a/app/serializers/environment_serializer.rb b/app/serializers/environment_serializer.rb index 2fb1ad52135..11445f93609 100644 --- a/app/serializers/environment_serializer.rb +++ b/app/serializers/environment_serializer.rb @@ -52,7 +52,17 @@ class EnvironmentSerializer < BaseSerializer end def batch_load(resource) - resource = resource.preload(environment_associations) + if ::Feature.enabled?(:custom_preloader_for_deployments, default_enabled: :yaml) + resource = resource.preload(environment_associations.except(:last_deployment, :upcoming_deployment)) + + Preloaders::Environments::DeploymentPreloader.new(resource) + .execute_with_union(:last_deployment, deployment_associations) + + Preloaders::Environments::DeploymentPreloader.new(resource) + .execute_with_union(:upcoming_deployment, deployment_associations) + else + resource = resource.preload(environment_associations) + end resource.all.to_a.tap do |environments| environments.each do |environment| diff --git a/app/services/ci/process_build_service.rb b/app/services/ci/process_build_service.rb index 5271c0fe93d..e6ec65fcc91 100644 --- a/app/services/ci/process_build_service.rb +++ b/app/services/ci/process_build_service.rb @@ -4,14 +4,7 @@ module Ci class ProcessBuildService < BaseService def execute(build, current_status) if valid_statuses_for_build(build).include?(current_status) - if build.schedulable? - build.schedule - elsif build.action? - build.actionize - else - enqueue(build) - end - + process(build) true else build.skip @@ -21,6 +14,16 @@ module Ci private + def process(build) + if build.schedulable? + build.schedule + elsif build.action? + build.actionize + else + enqueue(build) + end + end + def enqueue(build) build.enqueue end diff --git a/app/services/packages/terraform_module/create_package_service.rb b/app/services/packages/terraform_module/create_package_service.rb index 03f749edfa8..d1bc79089a3 100644 --- a/app/services/packages/terraform_module/create_package_service.rb +++ b/app/services/packages/terraform_module/create_package_service.rb @@ -7,7 +7,7 @@ module Packages def execute return error('Version is empty.', 400) if params[:module_version].blank? - return error('Package already exists.', 403) if current_package_exists_elsewhere? + return error('Access Denied', 403) if current_package_exists_elsewhere? return error('Package version already exists.', 403) if current_package_version_exists? return error('File is too large.', 400) if file_size_exceeded? diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml index bafb2085589..ca14d898d79 100644 --- a/app/views/admin/users/_head.html.haml +++ b/app/views/admin/users/_head.html.haml @@ -3,23 +3,26 @@ %h3.page-title.gl-m-0 = @user.name - if @user.blocked_pending_approval? - %span.cred + %span.gl-text-red-500 = s_('AdminUsers|(Pending approval)') - elsif @user.banned? - %span.cred + %span.gl-text-red-500 = s_('AdminUsers|(Banned)') - elsif @user.blocked? - %span.cred + %span.gl-text-red-500 = s_('AdminUsers|(Blocked)') - if @user.internal? - %span.cred + %span.gl-text-red-500 = s_('AdminUsers|(Internal)') - if @user.admin - %span.cred + %span.gl-text-red-500 = s_('AdminUsers|(Admin)') - if @user.deactivated? - %span.cred + %span.gl-text-red-500 = s_('AdminUsers|(Deactivated)') + - if @user.access_locked? + %span.gl-text-red-500 + = s_('AdminUsers|(Locked)') = render_if_exists 'admin/users/auditor_user_badge' = render_if_exists 'admin/users/gma_user_badge' diff --git a/config/feature_flags/development/custom_preloader_for_deployments.yml b/config/feature_flags/development/custom_preloader_for_deployments.yml new file mode 100644 index 00000000000..f8abcb4ba4a --- /dev/null +++ b/config/feature_flags/development/custom_preloader_for_deployments.yml @@ -0,0 +1,8 @@ +--- +name: custom_preloader_for_deployments +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75767 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/348289 +milestone: '14.7' +type: development +group: group::release +default_enabled: false diff --git a/config/initializers/active_record_transaction_observer.rb b/config/initializers/active_record_transaction_observer.rb index fc9b73d656e..b90b3a39ac1 100644 --- a/config/initializers/active_record_transaction_observer.rb +++ b/config/initializers/active_record_transaction_observer.rb @@ -2,17 +2,8 @@ return unless Gitlab.com? || Gitlab.dev_or_test_env? -def feature_flags_available? - # When the DBMS is not available, an exception (e.g. PG::ConnectionBad) is raised - active_db_connection = ActiveRecord::Base.connection.active? rescue false - - active_db_connection && Feature::FlipperFeature.table_exists? -rescue ActiveRecord::NoDatabaseError - false -end - Gitlab::Application.configure do - if feature_flags_available? && ::Feature.enabled?(:active_record_transactions_tracking, type: :ops, default_enabled: :yaml) + if Feature.feature_flags_available? && ::Feature.enabled?(:active_record_transactions_tracking, type: :ops, default_enabled: :yaml) Gitlab::Database::Transaction::Observer.register! end end diff --git a/config/metrics/counts_28d/20210216175550_ci_pipeline_config_repository.yml b/config/metrics/counts_28d/20210216175550_ci_pipeline_config_repository.yml index 976ffa0444a..d27548980b0 100644 --- a/config/metrics/counts_28d/20210216175550_ci_pipeline_config_repository.yml +++ b/config/metrics/counts_28d/20210216175550_ci_pipeline_config_repository.yml @@ -1,7 +1,7 @@ --- data_category: operational key_path: usage_activity_by_stage_monthly.verify.ci_pipeline_config_repository -description: Total Monthly Pipelines from templates in repository +description: Monthly count of unique users creating pipelines from CI files in the repository product_section: ops product_stage: verify product_group: group::pipeline execution diff --git a/config/metrics/counts_7d/20210216184805_i_package_composer_deploy_token_weekly.yml b/config/metrics/counts_7d/20210216184805_i_package_composer_deploy_token_weekly.yml index 627138b5082..4238901ee7f 100644 --- a/config/metrics/counts_7d/20210216184805_i_package_composer_deploy_token_weekly.yml +++ b/config/metrics/counts_7d/20210216184805_i_package_composer_deploy_token_weekly.yml @@ -8,7 +8,7 @@ product_stage: package product_group: group::package product_category: package registry value_type: number -status: deprecated +status: removed time_frame: 7d data_source: redis_hll instrumentation_class: RedisHLLMetric diff --git a/config/metrics/counts_all/20210216175518_ci_pipeline_config_repository.yml b/config/metrics/counts_all/20210216175518_ci_pipeline_config_repository.yml index cf850f26d42..2d904295d18 100644 --- a/config/metrics/counts_all/20210216175518_ci_pipeline_config_repository.yml +++ b/config/metrics/counts_all/20210216175518_ci_pipeline_config_repository.yml @@ -1,7 +1,7 @@ --- data_category: optional key_path: counts.ci_pipeline_config_repository -description: Total Pipelines from templates in repository +description: Total Pipelines from CI files in repository product_section: ops product_stage: verify product_group: group::pipeline execution diff --git a/config/metrics/counts_all/20210216175533_ci_pipeline_config_repository.yml b/config/metrics/counts_all/20210216175533_ci_pipeline_config_repository.yml index 269acb1105e..c28daf950dd 100644 --- a/config/metrics/counts_all/20210216175533_ci_pipeline_config_repository.yml +++ b/config/metrics/counts_all/20210216175533_ci_pipeline_config_repository.yml @@ -1,7 +1,7 @@ --- data_category: optional key_path: usage_activity_by_stage.verify.ci_pipeline_config_repository -description: Total Pipelines from templates in repository +description: Total count of unique users creating pipelines from CI files in the repository product_section: ops product_stage: verify product_group: group::pipeline execution diff --git a/db/post_migrate/20211210140000_add_temporary_static_object_token_index.rb b/db/post_migrate/20211210140000_add_temporary_static_object_token_index.rb new file mode 100644 index 00000000000..54997dc4cc4 --- /dev/null +++ b/db/post_migrate/20211210140000_add_temporary_static_object_token_index.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddTemporaryStaticObjectTokenIndex < Gitlab::Database::Migration[1.0] + disable_ddl_transaction! + + INDEX_NAME = 'index_users_with_static_object_token' + + def up + add_concurrent_index :users, :id, where: "static_object_token IS NOT NULL AND static_object_token_encrypted IS NULL", name: INDEX_NAME + end + + def down + remove_concurrent_index :users, :id, name: INDEX_NAME + end +end diff --git a/db/post_migrate/20211210140629_encrypt_static_object_token.rb b/db/post_migrate/20211210140629_encrypt_static_object_token.rb new file mode 100644 index 00000000000..fe4db9fc14c --- /dev/null +++ b/db/post_migrate/20211210140629_encrypt_static_object_token.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class EncryptStaticObjectToken < Gitlab::Database::Migration[1.0] + BATCH_SIZE = 10_000 + MIGRATION = 'EncryptStaticObjectToken' + + disable_ddl_transaction! + + def up + queue_background_migration_jobs_by_range_at_intervals( + define_batchable_model('users').where.not(static_object_token: nil).where(static_object_token_encrypted: nil), + MIGRATION, + 2.minutes, + batch_size: BATCH_SIZE, + track_jobs: true + ) + end + + def down + # no ops + end +end diff --git a/db/schema_migrations/20211210140000 b/db/schema_migrations/20211210140000 new file mode 100644 index 00000000000..b64d8251d69 --- /dev/null +++ b/db/schema_migrations/20211210140000 @@ -0,0 +1 @@ +f02c1b7412d2bb6d8a20639704ad55cdbcc14bfccf0509b659c3ef9614bcfa2b
\ No newline at end of file diff --git a/db/schema_migrations/20211210140629 b/db/schema_migrations/20211210140629 new file mode 100644 index 00000000000..ad631461d87 --- /dev/null +++ b/db/schema_migrations/20211210140629 @@ -0,0 +1 @@ +7940b0f692b62bcabbe98440082e2245fd28caba2c9e052e85e82acea0a98d23
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 9188caad2c0..ae6c128f293 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -27797,6 +27797,8 @@ CREATE INDEX index_users_star_projects_on_project_id ON users_star_projects USIN CREATE UNIQUE INDEX index_users_star_projects_on_user_id_and_project_id ON users_star_projects USING btree (user_id, project_id); +CREATE INDEX index_users_with_static_object_token ON users USING btree (id) WHERE ((static_object_token IS NOT NULL) AND (static_object_token_encrypted IS NULL)); + CREATE UNIQUE INDEX index_verification_codes_on_phone_and_visitor_id_code ON ONLY verification_codes USING btree (visitor_id_code, phone, created_at); COMMENT ON INDEX index_verification_codes_on_phone_and_visitor_id_code IS 'JiHu-specific index'; diff --git a/doc/administration/operations/cleaning_up_redis_sessions.md b/doc/administration/operations/cleaning_up_redis_sessions.md deleted file mode 100644 index ed5014b65e1..00000000000 --- a/doc/administration/operations/cleaning_up_redis_sessions.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -redirect_to: 'index.md' -remove_date: '2021-12-10' ---- - -This document was moved to [another location](index.md). - -<!-- This redirect file can be deleted after 2021-12-10. --> -<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page --> diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index cdc354c0fb8..5fac7a555d5 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -17690,10 +17690,10 @@ The state of the vulnerability. | Value | Description | | ----- | ----------- | -| <a id="vulnerabilitystateconfirmed"></a>`CONFIRMED` | Confirmed vulnerability. | -| <a id="vulnerabilitystatedetected"></a>`DETECTED` | Detected vulnerability. | -| <a id="vulnerabilitystatedismissed"></a>`DISMISSED` | Dismissed vulnerability. | -| <a id="vulnerabilitystateresolved"></a>`RESOLVED` | Resolved vulnerability. | +| <a id="vulnerabilitystateconfirmed"></a>`CONFIRMED` | For details, see [vulnerability status values](https://docs.gitlab.com/ee/user/application_security/vulnerabilities/index.html#vulnerability-status-values). | +| <a id="vulnerabilitystatedetected"></a>`DETECTED` | For details, see [vulnerability status values](https://docs.gitlab.com/ee/user/application_security/vulnerabilities/index.html#vulnerability-status-values). | +| <a id="vulnerabilitystatedismissed"></a>`DISMISSED` | For details, see [vulnerability status values](https://docs.gitlab.com/ee/user/application_security/vulnerabilities/index.html#vulnerability-status-values). | +| <a id="vulnerabilitystateresolved"></a>`RESOLVED` | For details, see [vulnerability status values](https://docs.gitlab.com/ee/user/application_security/vulnerabilities/index.html#vulnerability-status-values). | ### `WeightWildcardId` diff --git a/doc/development/snowplow/dictionary.md b/doc/development/snowplow/dictionary.md deleted file mode 100644 index 02e9ba5ce20..00000000000 --- a/doc/development/snowplow/dictionary.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -redirect_to: 'https://metrics.gitlab.com/snowplow.html' -remove_date: '2021-12-28' ---- diff --git a/doc/security/img/unlock_user_v14_7.png b/doc/security/img/unlock_user_v14_7.png Binary files differnew file mode 100644 index 00000000000..51015d932cb --- /dev/null +++ b/doc/security/img/unlock_user_v14_7.png diff --git a/doc/security/unlock_user.md b/doc/security/unlock_user.md index fa2fcbe782a..057d4e87efa 100644 --- a/doc/security/unlock_user.md +++ b/doc/security/unlock_user.md @@ -5,9 +5,23 @@ info: To determine the technical writer assigned to the Stage/Group associated w type: howto --- -# How to unlock a locked user from the command line **(FREE SELF)** +# Locked users **(FREE SELF)** -After ten failed login attempts a user gets in a locked state. +Users are locked after ten failed sign-in attempts. These users remain locked: + +- For 10 minutes, after which time they are automatically unlocked. +- Until an admin unlocks them from the [Admin Area](../user/admin_area/index.md) or the command line in under 10 minutes. + +## Unlock a user from the Admin Area + +1. On the top bar, select **Menu > Admin**. +1. On the left sidebar, select **Overview > Users**. +1. Use the search bar to find the locked user. +1. From the **User administration** dropdown select **Unlock**. + + + +## Unlock a user from the command line To unlock a locked user: diff --git a/doc/user/application_security/vulnerabilities/index.md b/doc/user/application_security/vulnerabilities/index.md index 7bdc8cc8479..7fd3c076fe9 100644 --- a/doc/user/application_security/vulnerabilities/index.md +++ b/doc/user/application_security/vulnerabilities/index.md @@ -37,7 +37,7 @@ A vulnerability's status can be one of the following: | Status | Description | |:----------|:------------| -| Detected | The default state for a newly discovered vulnerability. | +| Detected | The default state for a newly discovered vulnerability. Appears as "Needs triage" in the UI. | | Confirmed | A user has seen this vulnerability and confirmed it to be accurate. | | Dismissed | A user has seen this vulnerability and dismissed it because it is not accurate or otherwise not to be resolved. | | Resolved | The vulnerability has been fixed or is no longer present. | diff --git a/lib/banzai/filter/base_sanitization_filter.rb b/lib/banzai/filter/base_sanitization_filter.rb index 7ea32c4b1e7..4e350a59fa0 100644 --- a/lib/banzai/filter/base_sanitization_filter.rb +++ b/lib/banzai/filter/base_sanitization_filter.rb @@ -42,7 +42,7 @@ module Banzai # Allow any protocol in `a` elements # and then remove links with unsafe protocols allowlist[:protocols].delete('a') - allowlist[:transformers].push(self.class.method(:remove_unsafe_links)) + allowlist[:transformers].push(self.class.method(:sanitize_unsafe_links)) # Remove `rel` attribute from `a` elements allowlist[:transformers].push(self.class.remove_rel) diff --git a/lib/feature.rb b/lib/feature.rb index f301f206b46..12b4ef07dd6 100644 --- a/lib/feature.rb +++ b/lib/feature.rb @@ -29,6 +29,15 @@ class Feature class << self delegate :group, to: :flipper + def feature_flags_available? + # When the DBMS is not available, an exception (e.g. PG::ConnectionBad) is raised + active_db_connection = ActiveRecord::Base.connection.active? rescue false # rubocop:disable Database/MultipleDatabases + + active_db_connection && Feature::FlipperFeature.table_exists? + rescue ActiveRecord::NoDatabaseError + false + end + def all flipper.features.to_a end diff --git a/lib/gitlab/background_migration/encrypt_static_object_token.rb b/lib/gitlab/background_migration/encrypt_static_object_token.rb new file mode 100644 index 00000000000..80931353e2f --- /dev/null +++ b/lib/gitlab/background_migration/encrypt_static_object_token.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # Populates "static_object_token_encrypted" field with encrypted versions + # of values from "static_object_token" field + class EncryptStaticObjectToken + # rubocop:disable Style/Documentation + class User < ActiveRecord::Base + include ::EachBatch + self.table_name = 'users' + scope :with_static_object_token, -> { where.not(static_object_token: nil) } + scope :without_static_object_token_encrypted, -> { where(static_object_token_encrypted: nil) } + end + # rubocop:enable Style/Documentation + + BATCH_SIZE = 100 + + def perform(start_id, end_id) + ranged_query = User + .where(id: start_id..end_id) + .with_static_object_token + .without_static_object_token_encrypted + + ranged_query.each_batch(of: BATCH_SIZE) do |sub_batch| + first, last = sub_batch.pluck(Arel.sql('min(id), max(id)')).first + + batch_query = User.unscoped + .where(id: first..last) + .with_static_object_token + .without_static_object_token_encrypted + + user_tokens = batch_query.pluck(:id, :static_object_token) + + user_encrypted_tokens = user_tokens.map do |(id, plaintext_token)| + next if plaintext_token.blank? + + [id, Gitlab::CryptoHelper.aes256_gcm_encrypt(plaintext_token)] + end + + encrypted_tokens_sql = user_encrypted_tokens.compact.map { |(id, token)| "(#{id}, '#{token}')" }.join(',') + + if user_encrypted_tokens.present? + User.connection.execute(<<~SQL) + WITH cte(cte_id, cte_token) AS #{::Gitlab::Database::AsWithMaterialized.materialized_if_supported} ( + SELECT * + FROM (VALUES #{encrypted_tokens_sql}) AS t (id, token) + ) + UPDATE #{User.table_name} + SET static_object_token_encrypted = cte_token + FROM cte + WHERE cte_id = id + SQL + end + + mark_job_as_succeeded(start_id, end_id) + end + end + + private + + def mark_job_as_succeeded(*arguments) + Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded( + self.class.name.demodulize, + arguments + ) + end + end + end +end diff --git a/lib/gitlab/usage_data_counters/hll_redis_counter.rb b/lib/gitlab/usage_data_counters/hll_redis_counter.rb index 8fc8bb5d344..c6e9db6a314 100644 --- a/lib/gitlab/usage_data_counters/hll_redis_counter.rb +++ b/lib/gitlab/usage_data_counters/hll_redis_counter.rb @@ -104,7 +104,7 @@ module Gitlab events_names = events_for_category(category) event_results = events_names.each_with_object({}) do |event, hash| - hash["#{event}_weekly"] = unique_events(**weekly_time_range.merge(event_names: [event])) + hash["#{event}_weekly"] = unique_events(**weekly_time_range.merge(event_names: [event])) unless event == "i_package_composer_deploy_token" hash["#{event}_monthly"] = unique_events(**monthly_time_range.merge(event_names: [event])) end diff --git a/lib/gitlab/utils/sanitize_node_link.rb b/lib/gitlab/utils/sanitize_node_link.rb index ab5d18e9c8a..b0dfa087fcf 100644 --- a/lib/gitlab/utils/sanitize_node_link.rb +++ b/lib/gitlab/utils/sanitize_node_link.rb @@ -8,6 +8,12 @@ module Gitlab UNSAFE_PROTOCOLS = %w(data javascript vbscript).freeze ATTRS_TO_SANITIZE = %w(href src data-src data-canonical-src).freeze + # sanitize 6.0 requires only a context argument. Do not add any default + # arguments to this method. + def sanitize_unsafe_links(env) + remove_unsafe_links(env) + end + def remove_unsafe_links(env, remove_invalid_links: true) node = env[:node] diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a3ce0a4881e..6714eb75fd0 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2648,6 +2648,9 @@ msgstr "" msgid "AdminUsers|(Internal)" msgstr "" +msgid "AdminUsers|(Locked)" +msgstr "" + msgid "AdminUsers|(Pending approval)" msgstr "" diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb index f7cf55d8a95..1370ec9cc0b 100644 --- a/spec/controllers/projects/repositories_controller_spec.rb +++ b/spec/controllers/projects/repositories_controller_spec.rb @@ -210,6 +210,25 @@ RSpec.describe Projects::RepositoriesController do expect(response).to have_gitlab_http_status(:found) end end + + context 'when token is migrated' do + let(:user) { create(:user, static_object_token: '') } + let(:token) { 'Test' } + + it 'calls the action normally' do + user.update_column(:static_object_token, token) + + get :archive, params: { namespace_id: project.namespace, project_id: project, id: 'master', token: token }, format: 'zip' + expect(user.static_object_token).to eq(token) + expect(response).to have_gitlab_http_status(:ok) + + user.update_column(:static_object_token_encrypted, Gitlab::CryptoHelper.aes256_gcm_encrypt(token)) + + get :archive, params: { namespace_id: project.namespace, project_id: project, id: 'master', token: token }, format: 'zip' + expect(user.static_object_token).to eq(token) + expect(response).to have_gitlab_http_status(:ok) + end + end end context 'when a token header is present' do diff --git a/spec/features/admin/users/user_spec.rb b/spec/features/admin/users/user_spec.rb index ae940fecabe..0d053329627 100644 --- a/spec/features/admin/users/user_spec.rb +++ b/spec/features/admin/users/user_spec.rb @@ -125,6 +125,26 @@ RSpec.describe 'Admin::Users::User' do end end + context 'when a user is locked', time_travel_to: '2020-02-02 10:30:45 -0700' do + let_it_be(:locked_user) { create(:user, locked_at: DateTime.parse('2020-02-02 10:30:00 -0700')) } + + before do + visit admin_user_path(locked_user) + end + + it "displays `(Locked)` next to user's name" do + expect(page).to have_content("#{locked_user.name} (Locked)") + end + + it 'allows a user to be unlocked from the `User administration dropdown', :js do + accept_gl_confirm("Unlock user #{locked_user.name}?", button_text: 'Unlock') do + click_action_in_user_dropdown(locked_user.id, 'Unlock') + end + + expect(page).not_to have_content("#{locked_user.name} (Locked)") + end + end + describe 'Impersonation' do let_it_be(:another_user) { create(:user) } diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 2f21961d1fc..d25cddea902 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -528,7 +528,7 @@ RSpec.describe 'Project issue boards', :js do end it 'does not allow dragging' do - expect(page).not_to have_selector('.user-can-drag') + expect(page).not_to have_selector('.gl-cursor-grab') end end diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb index 1bac1bcdf5a..3fc1484826c 100644 --- a/spec/features/groups/issues_spec.rb +++ b/spec/features/groups/issues_spec.rb @@ -156,10 +156,10 @@ RSpec.describe 'Group issues page' do expect(page).to have_selector('.manual-ordering') end - it 'each issue item has a user-can-drag css applied' do + it 'each issue item has a gl-cursor-grab css applied' do visit issues_group_path(group, sort: 'relative_position') - expect(page).to have_selector('.issue.user-can-drag', count: 3) + expect(page).to have_selector('.issue.gl-cursor-grab', count: 3) end it 'issues should be draggable and persist order' do diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js index 5742dfdc5d2..3af173aa18c 100644 --- a/spec/frontend/boards/components/board_card_spec.js +++ b/spec/frontend/boards/components/board_card_spec.js @@ -167,7 +167,7 @@ describe('Board card', () => { mountComponent({ item: { ...mockIssue, isLoading: true } }); expect(wrapper.classes()).toContain('is-disabled'); - expect(wrapper.classes()).not.toContain('user-can-drag'); + expect(wrapper.classes()).not.toContain('gl-cursor-grab'); }); }); @@ -177,7 +177,7 @@ describe('Board card', () => { mountComponent(); expect(wrapper.classes()).not.toContain('is-disabled'); - expect(wrapper.classes()).toContain('user-can-drag'); + expect(wrapper.classes()).toContain('gl-cursor-grab'); }); }); }); diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js index 148d0c5684d..8cc0ad5f30c 100644 --- a/spec/frontend/boards/components/board_list_header_spec.js +++ b/spec/frontend/boards/components/board_list_header_spec.js @@ -180,18 +180,18 @@ describe('Board List Header Component', () => { const canDragList = [ListType.label, ListType.milestone, ListType.iteration, ListType.assignee]; it.each(cannotDragList)( - 'does not have user-can-drag-class so user cannot drag list', + 'does not have gl-cursor-grab class so user cannot drag list', (listType) => { createComponent({ listType }); - expect(findTitle().classes()).not.toContain('user-can-drag'); + expect(findTitle().classes()).not.toContain('gl-cursor-grab'); }, ); - it.each(canDragList)('has user-can-drag-class so user can drag list', (listType) => { + it.each(canDragList)('has gl-cursor-grab class so user can drag list', (listType) => { createComponent({ listType }); - expect(findTitle().classes()).toContain('user-can-drag'); + expect(findTitle().classes()).toContain('gl-cursor-grab'); }); }); }); diff --git a/spec/frontend/cycle_analytics/stage_table_spec.js b/spec/frontend/cycle_analytics/stage_table_spec.js index 3158446c37d..69de2044a8b 100644 --- a/spec/frontend/cycle_analytics/stage_table_spec.js +++ b/spec/frontend/cycle_analytics/stage_table_spec.js @@ -24,6 +24,7 @@ const findTable = () => wrapper.findComponent(GlTable); const findTableHead = () => wrapper.find('thead'); const findTableHeadColumns = () => findTableHead().findAll('th'); const findStageEventTitle = (ev) => extendedWrapper(ev).findByTestId('vsa-stage-event-title'); +const findStageEventLink = (ev) => extendedWrapper(ev).findByTestId('vsa-stage-event-link'); const findStageTime = () => wrapper.findByTestId('vsa-stage-event-time'); const findIcon = (name) => wrapper.findByTestId(`${name}-icon`); @@ -86,6 +87,15 @@ describe('StageTable', () => { expect(titles[index]).toBe(ev.title); }); }); + + it('will not display the project name in the record link', () => { + const evs = findStageEvents(); + + const links = evs.wrappers.map((ev) => findStageEventLink(ev).text()); + issueEventItems.forEach((ev, index) => { + expect(links[index]).toBe(`#${ev.iid}`); + }); + }); }); describe('default event', () => { @@ -187,6 +197,21 @@ describe('StageTable', () => { }); }); + describe('includeProjectName set', () => { + beforeEach(() => { + wrapper = createComponent({ includeProjectName: true }); + }); + + it('will display the project name in the record link', () => { + const evs = findStageEvents(); + + const links = evs.wrappers.map((ev) => findStageEventLink(ev).text()); + issueEventItems.forEach((ev, index) => { + expect(links[index]).toBe(`${ev.projectPath}#${ev.iid}`); + }); + }); + }); + describe('Pagination', () => { beforeEach(() => { wrapper = createComponent(); diff --git a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js index 1ae99abf4aa..ec42782ab15 100644 --- a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js +++ b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js @@ -4,7 +4,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { captureException } from '~/runner/sentry_utils'; @@ -200,7 +200,7 @@ describe('RunnerTypeCell', () => { }); it('error is shown to the user', () => { - expect(createFlash).toHaveBeenCalledTimes(1); + expect(createAlert).toHaveBeenCalledTimes(1); }); }); @@ -229,7 +229,7 @@ describe('RunnerTypeCell', () => { }); it('error is shown to the user', () => { - expect(createFlash).toHaveBeenCalledTimes(1); + expect(createAlert).toHaveBeenCalledTimes(1); }); }); }); @@ -350,7 +350,7 @@ describe('RunnerTypeCell', () => { }); it('error is shown to the user', () => { - expect(createFlash).toHaveBeenCalledTimes(1); + expect(createAlert).toHaveBeenCalledTimes(1); }); it('toast notification is not shown', () => { @@ -382,7 +382,7 @@ describe('RunnerTypeCell', () => { }); it('error is shown to the user', () => { - expect(createFlash).toHaveBeenCalledTimes(1); + expect(createAlert).toHaveBeenCalledTimes(1); }); }); }); diff --git a/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js b/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js index 0d002c272b4..4e93d6bcaa3 100644 --- a/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js +++ b/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js @@ -4,7 +4,7 @@ import { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import RegistrationTokenResetDropdownItem from '~/runner/components/registration/registration_token_reset_dropdown_item.vue'; import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants'; import runnersRegistrationTokenResetMutation from '~/runner/graphql/runners_registration_token_reset.mutation.graphql'; @@ -146,7 +146,7 @@ describe('RegistrationTokenResetDropdownItem', () => { findDropdownItem().trigger('click'); await waitForPromises(); - expect(createFlash).toHaveBeenLastCalledWith({ + expect(createAlert).toHaveBeenLastCalledWith({ message: `Network error: ${mockErrorMsg}`, }); expect(captureException).toHaveBeenCalledWith({ @@ -172,7 +172,7 @@ describe('RegistrationTokenResetDropdownItem', () => { findDropdownItem().trigger('click'); await waitForPromises(); - expect(createFlash).toHaveBeenLastCalledWith({ + expect(createAlert).toHaveBeenLastCalledWith({ message: `${mockErrorMsg} ${mockErrorMsg2}`, }); expect(captureException).toHaveBeenCalledWith({ diff --git a/spec/frontend/runner/components/runner_update_form_spec.js b/spec/frontend/runner/components/runner_update_form_spec.js index 112b990fa45..ebb2e67d1e2 100644 --- a/spec/frontend/runner/components/runner_update_form_spec.js +++ b/spec/frontend/runner/components/runner_update_form_spec.js @@ -5,7 +5,7 @@ import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import createFlash, { FLASH_TYPES } from '~/flash'; +import { createAlert, VARIANT_SUCCESS } from '~/flash'; import RunnerUpdateForm from '~/runner/components/runner_update_form.vue'; import { INSTANCE_TYPE, @@ -79,9 +79,9 @@ describe('RunnerUpdateForm', () => { input: expect.objectContaining(submittedRunner), }); - expect(createFlash).toHaveBeenLastCalledWith({ + expect(createAlert).toHaveBeenLastCalledWith({ message: expect.stringContaining('saved'), - type: FLASH_TYPES.SUCCESS, + variant: VARIANT_SUCCESS, }); expect(findSubmitDisabledAttr()).toBeUndefined(); @@ -238,7 +238,7 @@ describe('RunnerUpdateForm', () => { await submitFormAndWait(); - expect(createFlash).toHaveBeenLastCalledWith({ + expect(createAlert).toHaveBeenLastCalledWith({ message: `Network error: ${mockErrorMsg}`, }); expect(captureException).toHaveBeenCalledWith({ @@ -262,7 +262,7 @@ describe('RunnerUpdateForm', () => { await submitFormAndWait(); - expect(createFlash).toHaveBeenLastCalledWith({ + expect(createAlert).toHaveBeenLastCalledWith({ message: mockErrorMsg, }); expect(captureException).not.toHaveBeenCalled(); diff --git a/spec/frontend/runner/components/search_tokens/tag_token_spec.js b/spec/frontend/runner/components/search_tokens/tag_token_spec.js index 89c06ba2df4..52557ff716d 100644 --- a/spec/frontend/runner/components/search_tokens/tag_token_spec.js +++ b/spec/frontend/runner/components/search_tokens/tag_token_spec.js @@ -3,7 +3,7 @@ import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import axios from '~/lib/utils/axios_utils'; import TagToken, { TAG_SUGGESTIONS_PATH } from '~/runner/components/search_tokens/tag_token.vue'; @@ -168,8 +168,8 @@ describe('TagToken', () => { }); it('error is shown', async () => { - expect(createFlash).toHaveBeenCalledTimes(1); - expect(createFlash).toHaveBeenCalledWith({ message: expect.any(String) }); + expect(createAlert).toHaveBeenCalledTimes(1); + expect(createAlert).toHaveBeenCalledWith({ message: expect.any(String) }); }); }); diff --git a/spec/frontend/runner/group_runners/group_runners_app_spec.js b/spec/frontend/runner/group_runners/group_runners_app_spec.js index 4451100de19..0ce6feceb5b 100644 --- a/spec/frontend/runner/group_runners/group_runners_app_spec.js +++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js @@ -6,7 +6,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import setWindowLocation from 'helpers/set_window_location_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { updateHistory } from '~/lib/utils/url_utility'; @@ -236,7 +236,7 @@ describe('GroupRunnersApp', () => { }); it('error is shown to the user', async () => { - expect(createFlash).toHaveBeenCalledTimes(1); + expect(createAlert).toHaveBeenCalledTimes(1); }); it('error is reported to sentry', async () => { diff --git a/spec/frontend/runner/runner_detail/runner_details_app_spec.js b/spec/frontend/runner/runner_detail/runner_details_app_spec.js index 3f3bbddd3bd..64b2d3c30e2 100644 --- a/spec/frontend/runner/runner_detail/runner_details_app_spec.js +++ b/spec/frontend/runner/runner_detail/runner_details_app_spec.js @@ -2,7 +2,7 @@ import { createLocalVue, mount, shallowMount } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import RunnerHeader from '~/runner/components/runner_header.vue'; @@ -82,7 +82,7 @@ describe('RunnerDetailsApp', () => { }); it('error is shown to the user', () => { - expect(createFlash).toHaveBeenCalled(); + expect(createAlert).toHaveBeenCalled(); }); }); }); diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb index 01db06617dc..8c546390201 100644 --- a/spec/lib/feature_spec.rb +++ b/spec/lib/feature_spec.rb @@ -11,6 +11,32 @@ RSpec.describe Feature, stub_feature_flags: false do skip_feature_flags_yaml_validation end + describe '.feature_flags_available?' do + it 'returns false on connection error' do + expect(ActiveRecord::Base.connection).to receive(:active?).and_raise(PG::ConnectionBad) # rubocop:disable Database/MultipleDatabases + + expect(described_class.feature_flags_available?).to eq(false) + end + + it 'returns false when connection is not active' do + expect(ActiveRecord::Base.connection).to receive(:active?).and_return(false) # rubocop:disable Database/MultipleDatabases + + expect(described_class.feature_flags_available?).to eq(false) + end + + it 'returns false when the flipper table does not exist' do + expect(Feature::FlipperFeature).to receive(:table_exists?).and_return(false) + + expect(described_class.feature_flags_available?).to eq(false) + end + + it 'returns false on NoDatabaseError' do + expect(Feature::FlipperFeature).to receive(:table_exists?).and_raise(ActiveRecord::NoDatabaseError) + + expect(described_class.feature_flags_available?).to eq(false) + end + end + describe '.get' do let(:feature) { double(:feature) } let(:key) { 'my_feature' } diff --git a/spec/lib/gitlab/background_migration/encrypt_static_object_token_spec.rb b/spec/lib/gitlab/background_migration/encrypt_static_object_token_spec.rb new file mode 100644 index 00000000000..94d9f4509a7 --- /dev/null +++ b/spec/lib/gitlab/background_migration/encrypt_static_object_token_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::EncryptStaticObjectToken do + let(:users) { table(:users) } + let!(:user_without_tokens) { create_user!(name: 'notoken') } + let!(:user_with_plaintext_token_1) { create_user!(name: 'plaintext_1', token: 'token') } + let!(:user_with_plaintext_token_2) { create_user!(name: 'plaintext_2', token: 'TOKEN') } + let!(:user_with_plaintext_empty_token) { create_user!(name: 'plaintext_3', token: '') } + let!(:user_with_encrypted_token) { create_user!(name: 'encrypted', encrypted_token: 'encrypted') } + let!(:user_with_both_tokens) { create_user!(name: 'both', token: 'token2', encrypted_token: 'encrypted2') } + + before do + allow(Gitlab::CryptoHelper).to receive(:aes256_gcm_encrypt).and_call_original + allow(Gitlab::CryptoHelper).to receive(:aes256_gcm_encrypt).with('token') { 'secure_token' } + allow(Gitlab::CryptoHelper).to receive(:aes256_gcm_encrypt).with('TOKEN') { 'SECURE_TOKEN' } + end + + subject { described_class.new.perform(start_id, end_id) } + + let(:start_id) { users.minimum(:id) } + let(:end_id) { users.maximum(:id) } + + it 'backfills encrypted tokens to users with plaintext token only', :aggregate_failures do + subject + + new_state = users.pluck(:id, :static_object_token, :static_object_token_encrypted).to_h do |row| + [row[0], [row[1], row[2]]] + end + + expect(new_state.count).to eq(6) + + expect(new_state[user_with_plaintext_token_1.id]).to match_array(%w[token secure_token]) + expect(new_state[user_with_plaintext_token_2.id]).to match_array(%w[TOKEN SECURE_TOKEN]) + + expect(new_state[user_with_plaintext_empty_token.id]).to match_array(['', nil]) + expect(new_state[user_without_tokens.id]).to match_array([nil, nil]) + expect(new_state[user_with_both_tokens.id]).to match_array(%w[token2 encrypted2]) + expect(new_state[user_with_encrypted_token.id]).to match_array([nil, 'encrypted']) + end + + private + + def create_user!(name:, token: nil, encrypted_token: nil) + email = "#{name}@example.com" + + table(:users).create!( + name: name, + email: email, + username: name, + projects_limit: 0, + static_object_token: token, + static_object_token_encrypted: encrypted_token + ) + end +end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 9e9cc2cfab6..5fea68ca2dc 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -1289,6 +1289,8 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do let(:categories) { ::Gitlab::UsageDataCounters::HLLRedisCounter.categories } + let(:ignored_metrics) { ["i_package_composer_deploy_token_weekly"] } + it 'has all known_events' do expect(subject).to have_key(:redis_hll_counters) @@ -1298,6 +1300,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do keys = ::Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category(category) metrics = keys.map { |key| "#{key}_weekly" } + keys.map { |key| "#{key}_monthly" } + metrics -= ignored_metrics if ::Gitlab::UsageDataCounters::HLLRedisCounter::CATEGORIES_FOR_TOTALS.include?(category) metrics.append("#{category}_total_unique_counts_weekly", "#{category}_total_unique_counts_monthly") diff --git a/spec/migrations/20211210140629_encrypt_static_object_token_spec.rb b/spec/migrations/20211210140629_encrypt_static_object_token_spec.rb new file mode 100644 index 00000000000..289cf9a93ed --- /dev/null +++ b/spec/migrations/20211210140629_encrypt_static_object_token_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true +require 'spec_helper' + +require_migration! + +RSpec.describe EncryptStaticObjectToken, :migration do + let_it_be(:background_migration_jobs) { table(:background_migration_jobs) } + let_it_be(:users) { table(:users) } + + let!(:user_without_tokens) { create_user!(name: 'notoken') } + let!(:user_with_plaintext_token_1) { create_user!(name: 'plaintext_1', token: 'token') } + let!(:user_with_plaintext_token_2) { create_user!(name: 'plaintext_2', token: 'TOKEN') } + let!(:user_with_encrypted_token) { create_user!(name: 'encrypted', encrypted_token: 'encrypted') } + let!(:user_with_both_tokens) { create_user!(name: 'both', token: 'token2', encrypted_token: 'encrypted2') } + + before do + stub_const("#{described_class}::BATCH_SIZE", 1) + end + + around do |example| + freeze_time { Sidekiq::Testing.fake! { example.run } } + end + + it 'schedules background migrations' do + migrate! + + expect(background_migration_jobs.count).to eq(2) + expect(background_migration_jobs.first.arguments).to match_array([user_with_plaintext_token_1.id, user_with_plaintext_token_1.id]) + expect(background_migration_jobs.second.arguments).to match_array([user_with_plaintext_token_2.id, user_with_plaintext_token_2.id]) + + expect(BackgroundMigrationWorker.jobs.size).to eq(2) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, user_with_plaintext_token_1.id, user_with_plaintext_token_1.id) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, user_with_plaintext_token_2.id, user_with_plaintext_token_2.id) + end + + private + + def create_user!(name:, token: nil, encrypted_token: nil) + email = "#{name}@example.com" + + table(:users).create!( + name: name, + email: email, + username: name, + projects_limit: 0, + static_object_token: token, + static_object_token_encrypted: encrypted_token + ) + end +end diff --git a/spec/models/preloaders/environments/deployment_preloader_spec.rb b/spec/models/preloaders/environments/deployment_preloader_spec.rb new file mode 100644 index 00000000000..c1812d45628 --- /dev/null +++ b/spec/models/preloaders/environments/deployment_preloader_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Preloaders::Environments::DeploymentPreloader do + let_it_be(:user) { create(:user) } + let_it_be(:project, reload: true) { create(:project, :repository) } + + let_it_be(:pipeline) { create(:ci_pipeline, user: user, project: project, sha: project.commit.sha) } + let_it_be(:ci_build_a) { create(:ci_build, user: user, project: project, pipeline: pipeline) } + let_it_be(:ci_build_b) { create(:ci_build, user: user, project: project, pipeline: pipeline) } + let_it_be(:ci_build_c) { create(:ci_build, user: user, project: project, pipeline: pipeline) } + + let_it_be(:environment_a) { create(:environment, project: project, state: :available) } + let_it_be(:environment_b) { create(:environment, project: project, state: :available) } + + before do + create(:deployment, :success, project: project, environment: environment_a, deployable: ci_build_a) + create(:deployment, :success, project: project, environment: environment_a, deployable: ci_build_b) + create(:deployment, :success, project: project, environment: environment_b, deployable: ci_build_c) + end + + def preload_association(association_name) + described_class.new(project.environments) + .execute_with_union(association_name, deployment_associations) + end + + def deployment_associations + { + user: [], + deployable: { + pipeline: { + manual_actions: [] + } + } + } + end + + it 'does not trigger N+1 queries' do + control = ActiveRecord::QueryRecorder.new { preload_association(:last_deployment) } + + ci_build_d = create(:ci_build, user: user, project: project, pipeline: pipeline) + create(:deployment, :success, project: project, environment: environment_b, deployable: ci_build_d) + + expect { preload_association(:last_deployment) }.not_to exceed_query_limit(control) + end + + it 'batch loads the dependent associations' do + preload_association(:last_deployment) + + expect do + project.environments.first.last_deployment.deployable.pipeline.manual_actions + end.not_to exceed_query_limit(0) + end + + # Example query scoped with IN clause for `last_deployment` association preload: + # SELECT DISTINCT ON (environment_id) deployments.* FROM "deployments" WHERE "deployments"."status" IN (1, 2, 3, 4, 6) AND "deployments"."environment_id" IN (35, 34, 33) ORDER BY environment_id, deployments.id DESC + it 'avoids scoping with IN clause during preload' do + control = ActiveRecord::QueryRecorder.new { preload_association(:last_deployment) } + + default_preload_query = control.occurrences_by_line_method.first[1][:occurrences].any? { |i| i.include?('"deployments"."environment_id" IN') } + + expect(default_preload_query).to be(false) + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 510c78eb5a0..0d4ae19a890 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1773,6 +1773,29 @@ RSpec.describe User do expect(static_object_token).not_to be_blank expect(user.reload.static_object_token).to eq static_object_token end + + it 'generates an encrypted version of the token' do + user = create(:user, static_object_token: nil) + + expect(user[:static_object_token]).to be_nil + expect(user[:static_object_token_encrypted]).to be_nil + + user.static_object_token + + expect(user[:static_object_token]).to be_nil + expect(user[:static_object_token_encrypted]).to be_present + end + + it 'prefers an encoded version of the token' do + user = create(:user, static_object_token: nil) + + token = user.static_object_token + + user.update_column(:static_object_token, 'Test') + + expect(user.static_object_token).not_to eq('Test') + expect(user.static_object_token).to eq(token) + end end describe 'enabled_static_object_token' do diff --git a/spec/serializers/analytics_build_entity_spec.rb b/spec/serializers/analytics_build_entity_spec.rb index 09804681f5d..b8ccc698526 100644 --- a/spec/serializers/analytics_build_entity_spec.rb +++ b/spec/serializers/analytics_build_entity_spec.rb @@ -27,6 +27,10 @@ RSpec.describe AnalyticsBuildEntity do expect(subject).to include(:author) end + it 'contains the project path' do + expect(subject).to include(:project_path) + end + it 'does not contain sensitive information' do expect(subject).not_to include(/token/) expect(subject).not_to include(/variables/) diff --git a/spec/serializers/analytics_issue_entity_spec.rb b/spec/serializers/analytics_issue_entity_spec.rb index 447c5e7d02a..57385471f2a 100644 --- a/spec/serializers/analytics_issue_entity_spec.rb +++ b/spec/serializers/analytics_issue_entity_spec.rb @@ -32,6 +32,10 @@ RSpec.describe AnalyticsIssueEntity do expect(subject).to include(:author) end + it 'contains the project path' do + expect(subject).to include(:project_path) + end + it 'does not contain sensitive information' do expect(subject).not_to include(/token/) expect(subject).not_to include(/variables/) diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb index 985e18f27a0..80b6f00d8c9 100644 --- a/spec/serializers/environment_serializer_spec.rb +++ b/spec/serializers/environment_serializer_spec.rb @@ -185,6 +185,42 @@ RSpec.describe EnvironmentSerializer do end end + context 'batching loading' do + let(:resource) { Environment.all } + + before do + create(:environment, name: 'staging/review-1') + create_environment_with_associations(project) + end + + it 'uses the custom preloader service' do + expect_next_instance_of(Preloaders::Environments::DeploymentPreloader) do |preloader| + expect(preloader).to receive(:execute_with_union).with(:last_deployment, hash_including(:deployable)).and_call_original + end + + expect_next_instance_of(Preloaders::Environments::DeploymentPreloader) do |preloader| + expect(preloader).to receive(:execute_with_union).with(:upcoming_deployment, hash_including(:deployable)).and_call_original + end + + json + end + + # Including for test coverage pipeline failure, remove along with feature flag. + context 'when custom preload feature is disabled' do + before do + Feature.disable(:custom_preloader_for_deployments) + end + + it 'avoids N+1 database queries' do + control_count = ActiveRecord::QueryRecorder.new { json }.count + + create_environment_with_associations(project) + + expect { json }.not_to exceed_query_limit(control_count) + end + end + end + def create_environment_with_associations(project) create(:environment, project: project).tap do |environment| create(:deployment, :success, environment: environment, project: project) diff --git a/spec/services/packages/terraform_module/create_package_service_spec.rb b/spec/services/packages/terraform_module/create_package_service_spec.rb index f911bb5b82c..e172aa726fd 100644 --- a/spec/services/packages/terraform_module/create_package_service_spec.rb +++ b/spec/services/packages/terraform_module/create_package_service_spec.rb @@ -37,7 +37,7 @@ RSpec.describe Packages::TerraformModule::CreatePackageService do let!(:existing_package) { create(:terraform_module_package, project: project2, name: 'foo/bar', version: '1.0.0') } it { expect(subject[:http_status]).to eq 403 } - it { expect(subject[:message]).to be 'Package already exists.' } + it { expect(subject[:message]).to be 'Access Denied' } end context 'version already exists' do |