Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									8132e39e1b
								
							
						
					
					
						commit
						15ea3fec22
					
				|  | @ -77,6 +77,7 @@ eslint-report.html | |||
| /.gitlab_kas_secret | ||||
| /webpack-report/ | ||||
| /crystalball/ | ||||
| /deprecations/ | ||||
| /knapsack/ | ||||
| /rspec_flaky/ | ||||
| /locale/**/LC_MESSAGES | ||||
|  |  | |||
|  | @ -21,6 +21,7 @@ | |||
|     RUBY_GC_MALLOC_LIMIT: 67108864 | ||||
|     RUBY_GC_MALLOC_LIMIT_MAX: 134217728 | ||||
|     CRYSTALBALL: "true" | ||||
|     RECORD_DEPRECATIONS: "true" | ||||
|   needs: ["setup-test-env", "retrieve-tests-metadata", "compile-test-assets"] | ||||
|   script: | ||||
|     - *base-script | ||||
|  | @ -31,6 +32,7 @@ | |||
|     paths: | ||||
|       - coverage/ | ||||
|       - crystalball/ | ||||
|       - deprecations/ | ||||
|       - knapsack/ | ||||
|       - rspec_flaky/ | ||||
|       - rspec_profiling/ | ||||
|  |  | |||
|  | @ -36,8 +36,7 @@ If applicable, any groups/projects that are happy to have this feature turned on | |||
| - [ ] Ensure that documentation has been updated | ||||
| - [ ] Enable on GitLab.com for individual groups/projects listed above and verify behaviour  (`/chatops run feature set --project=gitlab-org/gitlab feature_name true`) | ||||
| - [ ] Coordinate a time to enable the flag with the SRE oncall and release managers | ||||
|   - In `#production` by pinging `@sre-oncall` | ||||
|   - In `#g_delivery` by pinging `@release-managers` | ||||
|   - In `#production` mention `@sre-oncall` and `@release-managers`. Once an SRE on call and Release Manager on call confirm, you can proceed with the rollout | ||||
| - [ ] Announce on the issue an estimated time this will be enabled on GitLab.com | ||||
| - [ ] Enable on GitLab.com by running chatops command in `#production` (`/chatops run feature set feature_name true`) | ||||
| - [ ] Cross post chatops Slack command to `#support_gitlab-com` ([more guidance when this is necessary in the dev docs](https://docs.gitlab.com/ee/development/feature_flags/controls.html#where-to-run-commands)) and in your team channel | ||||
|  |  | |||
|  | @ -47,6 +47,10 @@ Cop/StaticTranslationDefinition: | |||
|     - 'spec/**/*' | ||||
|     - 'ee/spec/**/*' | ||||
| 
 | ||||
| Lint/LastKeywordArgument: | ||||
|   Enabled: true | ||||
|   Safe: false | ||||
| 
 | ||||
| # This cop checks whether some constant value isn't a | ||||
| # mutable literal (e.g. array or hash). | ||||
| Style/MutableConstant: | ||||
|  |  | |||
							
								
								
									
										1
									
								
								Gemfile
								
								
								
								
							
							
						
						
									
										1
									
								
								Gemfile
								
								
								
								
							|  | @ -351,6 +351,7 @@ group :development do | |||
| end | ||||
| 
 | ||||
| group :development, :test do | ||||
|   gem 'deprecation_toolkit', '~> 1.5.1', require: false | ||||
|   gem 'bullet', '~> 6.1.0' | ||||
|   gem 'pry-byebug', '~> 3.9.0', platform: :mri | ||||
|   gem 'pry-rails', '~> 0.3.9' | ||||
|  |  | |||
|  | @ -224,6 +224,8 @@ GEM | |||
|     declarative-option (0.1.0) | ||||
|     default_value_for (3.3.0) | ||||
|       activerecord (>= 3.2.0, < 6.1) | ||||
|     deprecation_toolkit (1.5.1) | ||||
|       activesupport (>= 4.2) | ||||
|     derailed_benchmarks (1.7.0) | ||||
|       benchmark-ips (~> 2) | ||||
|       get_process_mem (~> 0) | ||||
|  | @ -1302,6 +1304,7 @@ DEPENDENCIES | |||
|   database_cleaner (~> 1.7.0) | ||||
|   deckar01-task_list (= 2.3.1) | ||||
|   default_value_for (~> 3.3.0) | ||||
|   deprecation_toolkit (~> 1.5.1) | ||||
|   derailed_benchmarks | ||||
|   device_detector | ||||
|   devise (~> 4.7.2) | ||||
|  |  | |||
|  | @ -216,8 +216,12 @@ export default { | |||
|       return { | ||||
|         name: this.currentIntegration?.name || '', | ||||
|         active: this.currentIntegration?.active || false, | ||||
|         token: this.currentIntegration?.token || this.selectedIntegrationType.token, | ||||
|         url: this.currentIntegration?.url || this.selectedIntegrationType.url, | ||||
|         token: | ||||
|           this.currentIntegration?.token || | ||||
|           (this.selectedIntegrationType !== this.generic ? this.selectedIntegrationType.token : ''), | ||||
|         url: | ||||
|           this.currentIntegration?.url || | ||||
|           (this.selectedIntegrationType !== this.generic ? this.selectedIntegrationType.url : ''), | ||||
|         apiUrl: this.currentIntegration?.apiUrl || '', | ||||
|       }; | ||||
|     }, | ||||
|  | @ -246,8 +250,20 @@ export default { | |||
|     canEditPayload() { | ||||
|       return this.hasSamplePayload && !this.resetSamplePayloadConfirmed; | ||||
|     }, | ||||
|     isResetAuthKeyDisabled() { | ||||
|       return !this.active && !this.integrationForm.token !== ''; | ||||
|     }, | ||||
|     isPayloadEditDisabled() { | ||||
|       return !this.active || this.canEditPayload; | ||||
|       return this.glFeatures.multipleHttpIntegrationsCustomMapping | ||||
|         ? !this.active || this.canEditPayload | ||||
|         : !this.active; | ||||
|     }, | ||||
|     isSubmitTestPayloadDisabled() { | ||||
|       return ( | ||||
|         !this.active || | ||||
|         Boolean(this.integrationTestPayload.error) || | ||||
|         this.integrationTestPayload.json === '' | ||||
|       ); | ||||
|     }, | ||||
|   }, | ||||
|   watch: { | ||||
|  | @ -257,7 +273,7 @@ export default { | |||
|       } | ||||
|       this.selectedIntegration = val.type; | ||||
|       this.active = val.active; | ||||
|       if (val.type === typeSet.http) this.getIntegrationMapping(val.id); | ||||
|       if (val.type === typeSet.http && this.showMappingBuilder) this.getIntegrationMapping(val.id); | ||||
|       return this.integrationTypeSelect(); | ||||
|     }, | ||||
|   }, | ||||
|  | @ -297,14 +313,8 @@ export default { | |||
|         }); | ||||
|     }, | ||||
|     submitWithTestPayload() { | ||||
|       return service | ||||
|         .updateTestAlert(this.testAlertPayload) | ||||
|         .then(() => { | ||||
|       this.$emit('set-test-alert-payload', this.testAlertPayload); | ||||
|       this.submit(); | ||||
|         }) | ||||
|         .catch(() => { | ||||
|           this.$emit('test-payload-failure'); | ||||
|         }); | ||||
|     }, | ||||
|     submit() { | ||||
|       // TODO: Will be removed in 13.7 as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/273657 | ||||
|  | @ -323,6 +333,7 @@ export default { | |||
|         return this.$emit('update-integration', integrationPayload); | ||||
|       } | ||||
| 
 | ||||
|       this.reset(); | ||||
|       return this.$emit('create-new-integration', integrationPayload); | ||||
|     }, | ||||
|     reset() { | ||||
|  | @ -539,7 +550,7 @@ export default { | |||
|               </template> | ||||
|             </gl-form-input-group> | ||||
| 
 | ||||
|             <gl-button v-gl-modal.authKeyModal :disabled="!active"> | ||||
|             <gl-button v-gl-modal.authKeyModal :disabled="isResetAuthKeyDisabled"> | ||||
|               {{ $options.i18n.integrationFormSteps.step3.reset }} | ||||
|             </gl-button> | ||||
|             <gl-modal | ||||
|  | @ -642,7 +653,7 @@ export default { | |||
|         <gl-button | ||||
|           v-if="!isManagingOpsgenie" | ||||
|           data-testid="integration-test-and-submit" | ||||
|           :disabled="Boolean(integrationTestPayload.error)" | ||||
|           :disabled="isSubmitTestPayloadDisabled" | ||||
|           category="secondary" | ||||
|           variant="success" | ||||
|           class="gl-mx-3 js-no-auto-disable" | ||||
|  | @ -14,7 +14,8 @@ import resetHttpTokenMutation from '../graphql/mutations/reset_http_token.mutati | |||
| import resetPrometheusTokenMutation from '../graphql/mutations/reset_prometheus_token.mutation.graphql'; | ||||
| import updateCurrentIntergrationMutation from '../graphql/mutations/update_current_intergration.mutation.graphql'; | ||||
| import IntegrationsList from './alerts_integrations_list.vue'; | ||||
| import SettingsFormNew from './alerts_settings_form_new.vue'; | ||||
| import AlertSettingsForm from './alerts_settings_form.vue'; | ||||
| import service from '../services'; | ||||
| import { typeSet } from '../constants'; | ||||
| import { | ||||
|   updateStoreAfterIntegrationDelete, | ||||
|  | @ -35,6 +36,9 @@ export default { | |||
|       'AlertsIntegrations|The integration has been successfully saved. Alerts from this new integration should now appear on your alerts list.', | ||||
|     ), | ||||
|     integrationRemoved: s__('AlertsIntegrations|The integration has been successfully removed.'), | ||||
|     alertSent: s__( | ||||
|       'AlertsIntegrations|The test alert has been successfully sent, and should now be visible on your alerts list.', | ||||
|     ), | ||||
|   }, | ||||
|   components: { | ||||
|     // TODO: Will be removed in 13.7 as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/273657 | ||||
|  | @ -42,7 +46,7 @@ export default { | |||
|     GlLink, | ||||
|     GlSprintf, | ||||
|     IntegrationsList, | ||||
|     SettingsFormNew, | ||||
|     AlertSettingsForm, | ||||
|   }, | ||||
|   inject: { | ||||
|     generic: { | ||||
|  | @ -89,6 +93,7 @@ export default { | |||
|   data() { | ||||
|     return { | ||||
|       isUpdating: false, | ||||
|       testAlertPayload: null, | ||||
|       integrations: {}, | ||||
|       currentIntegration: null, | ||||
|     }; | ||||
|  | @ -131,6 +136,19 @@ export default { | |||
|           if (error) { | ||||
|             return createFlash({ message: error }); | ||||
|           } | ||||
| 
 | ||||
|           if (this.testAlertPayload) { | ||||
|             const integration = | ||||
|               httpIntegrationCreate?.integration || prometheusIntegrationCreate?.integration; | ||||
| 
 | ||||
|             const payload = { | ||||
|               ...this.testAlertPayload, | ||||
|               endpoint: integration.url, | ||||
|               token: integration.token, | ||||
|             }; | ||||
|             return this.validateAlertPayload(payload); | ||||
|           } | ||||
| 
 | ||||
|           return createFlash({ | ||||
|             message: this.$options.i18n.changesSaved, | ||||
|             type: FLASH_TYPES.SUCCESS, | ||||
|  | @ -161,6 +179,13 @@ export default { | |||
|           if (error) { | ||||
|             return createFlash({ message: error }); | ||||
|           } | ||||
| 
 | ||||
|           if (this.testAlertPayload) { | ||||
|             return this.validateAlertPayload(); | ||||
|           } | ||||
| 
 | ||||
|           this.clearCurrentIntegration(); | ||||
| 
 | ||||
|           return createFlash({ | ||||
|             message: this.$options.i18n.changesSaved, | ||||
|             type: FLASH_TYPES.SUCCESS, | ||||
|  | @ -171,6 +196,7 @@ export default { | |||
|         }) | ||||
|         .finally(() => { | ||||
|           this.isUpdating = false; | ||||
|           this.testAlertPayload = null; | ||||
|         }); | ||||
|     }, | ||||
|     resetToken({ type, variables }) { | ||||
|  | @ -194,7 +220,13 @@ export default { | |||
|             const integration = | ||||
|               httpIntegrationResetToken?.integration || | ||||
|               prometheusIntegrationResetToken?.integration; | ||||
|             this.currentIntegration = integration; | ||||
| 
 | ||||
|             this.$apollo.mutate({ | ||||
|               mutation: updateCurrentIntergrationMutation, | ||||
|               variables: { | ||||
|                 ...integration, | ||||
|               }, | ||||
|             }); | ||||
| 
 | ||||
|             return createFlash({ | ||||
|               message: this.$options.i18n.changesSaved, | ||||
|  | @ -262,8 +294,21 @@ export default { | |||
|         variables: {}, | ||||
|       }); | ||||
|     }, | ||||
|     testPayloadFailure() { | ||||
|     setTestAlertPayload(payload) { | ||||
|       this.testAlertPayload = payload; | ||||
|     }, | ||||
|     validateAlertPayload(payload) { | ||||
|       return service | ||||
|         .updateTestAlert(payload ?? this.testAlertPayload) | ||||
|         .then(() => { | ||||
|           return createFlash({ | ||||
|             message: this.$options.i18n.alertSent, | ||||
|             type: FLASH_TYPES.SUCCESS, | ||||
|           }); | ||||
|         }) | ||||
|         .catch(() => { | ||||
|           createFlash({ message: INTEGRATION_PAYLOAD_TEST_ERROR }); | ||||
|         }); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
|  | @ -297,7 +342,7 @@ export default { | |||
|       @edit-integration="editIntegration" | ||||
|       @delete-integration="deleteIntegration" | ||||
|     /> | ||||
|     <settings-form-new | ||||
|     <alert-settings-form | ||||
|       :loading="isUpdating" | ||||
|       :can-add-integration="canAddIntegration" | ||||
|       :can-manage-opsgenie="canManageOpsgenie" | ||||
|  | @ -305,7 +350,7 @@ export default { | |||
|       @update-integration="updateIntegration" | ||||
|       @reset-token="resetToken" | ||||
|       @clear-current-integration="clearCurrentIntegration" | ||||
|       @test-payload-failure="testPayloadFailure" | ||||
|       @set-test-alert-payload="setTestAlertPayload" | ||||
|     /> | ||||
|   </div> | ||||
| </template> | ||||
|  |  | |||
|  | @ -38,6 +38,8 @@ module Types | |||
|           feature_flag: :user_group_counts | ||||
|     field :status, Types::UserStatusType, null: true, | ||||
|            description: 'User status' | ||||
|     field :location, ::GraphQL::STRING_TYPE, null: true, | ||||
|           description: 'The location of the user.' | ||||
|     field :project_memberships, Types::ProjectMemberType.connection_type, null: true, | ||||
|           description: 'Project memberships of the user', | ||||
|           method: :project_members | ||||
|  |  | |||
|  | @ -3,7 +3,6 @@ | |||
| module Terraform | ||||
|   class State < ApplicationRecord | ||||
|     include UsageStatistics | ||||
|     include FileStoreMounter | ||||
|     include IgnorableColumns | ||||
|     # These columns are being removed since geo replication falls to the versioned state | ||||
|     # Tracking in https://gitlab.com/gitlab-org/gitlab/-/issues/258262 | ||||
|  | @ -36,18 +35,8 @@ module Terraform | |||
| 
 | ||||
|     default_value_for(:uuid, allows_nil: false) { SecureRandom.hex(UUID_LENGTH / 2) } | ||||
| 
 | ||||
|     mount_file_store_uploader StateUploader | ||||
| 
 | ||||
|     def file_store | ||||
|       super || StateUploader.default_store | ||||
|     end | ||||
| 
 | ||||
|     def latest_file | ||||
|       if versioning_enabled? | ||||
|       latest_version&.file | ||||
|       else | ||||
|         latest_version&.file || file | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     def locked? | ||||
|  | @ -55,13 +44,14 @@ module Terraform | |||
|     end | ||||
| 
 | ||||
|     def update_file!(data, version:, build:) | ||||
|       # This check is required to maintain backwards compatibility with | ||||
|       # states that were created prior to versioning being supported. | ||||
|       # This can be removed in 14.0 when support for these states is dropped. | ||||
|       # See https://gitlab.com/gitlab-org/gitlab/-/issues/258960 | ||||
|       if versioning_enabled? | ||||
|         create_new_version!(data: data, version: version, build: build) | ||||
|       elsif latest_version.present? | ||||
|         migrate_legacy_version!(data: data, version: version, build: build) | ||||
|       else | ||||
|         self.file = data | ||||
|         save! | ||||
|         migrate_legacy_version!(data: data, version: version, build: build) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|  |  | |||
|  | @ -10,9 +10,9 @@ module Terraform | |||
| 
 | ||||
|     scope :ordered_by_version_desc, -> { order(version: :desc) } | ||||
| 
 | ||||
|     default_value_for(:file_store) { VersionedStateUploader.default_store } | ||||
|     default_value_for(:file_store) { StateUploader.default_store } | ||||
| 
 | ||||
|     mount_file_store_uploader VersionedStateUploader | ||||
|     mount_file_store_uploader StateUploader | ||||
| 
 | ||||
|     delegate :project_id, :uuid, to: :terraform_state, allow_nil: true | ||||
| 
 | ||||
|  |  | |||
|  | @ -587,11 +587,13 @@ class User < ApplicationRecord | |||
| 
 | ||||
|       sanitized_order_sql = Arel.sql(sanitize_sql_array([order, query: query])) | ||||
| 
 | ||||
|       where( | ||||
|         fuzzy_arel_match(:name, query, lower_exact_match: true) | ||||
|           .or(fuzzy_arel_match(:username, query, lower_exact_match: true)) | ||||
|           .or(arel_table[:email].eq(query)) | ||||
|       ).reorder(sanitized_order_sql, :name) | ||||
|       search_query = if Feature.enabled?(:user_search_secondary_email) | ||||
|                        search_with_secondary_emails(query) | ||||
|                      else | ||||
|                        search_without_secondary_emails(query) | ||||
|                      end | ||||
| 
 | ||||
|       search_query.reorder(sanitized_order_sql, :name) | ||||
|     end | ||||
| 
 | ||||
|     # Limits the result set to users _not_ in the given query/list of IDs. | ||||
|  | @ -606,6 +608,18 @@ class User < ApplicationRecord | |||
|       reorder(:name) | ||||
|     end | ||||
| 
 | ||||
|     def search_without_secondary_emails(query) | ||||
|       return none if query.blank? | ||||
| 
 | ||||
|       query = query.downcase | ||||
| 
 | ||||
|       where( | ||||
|         fuzzy_arel_match(:name, query, lower_exact_match: true) | ||||
|           .or(fuzzy_arel_match(:username, query, lower_exact_match: true)) | ||||
|           .or(arel_table[:email].eq(query)) | ||||
|       ) | ||||
|     end | ||||
| 
 | ||||
|     # searches user by given pattern | ||||
|     # it compares name, email, username fields and user's secondary emails with given pattern | ||||
|     # This method uses ILIKE on PostgreSQL. | ||||
|  | @ -616,15 +630,16 @@ class User < ApplicationRecord | |||
|       query = query.downcase | ||||
| 
 | ||||
|       email_table = Email.arel_table | ||||
|       matched_by_emails_user_ids = email_table | ||||
|       matched_by_email_user_id = email_table | ||||
|         .project(email_table[:user_id]) | ||||
|         .where(email_table[:email].eq(query)) | ||||
|         .take(1) # at most 1 record as there is a unique constraint | ||||
| 
 | ||||
|       where( | ||||
|         fuzzy_arel_match(:name, query) | ||||
|           .or(fuzzy_arel_match(:username, query)) | ||||
|           .or(arel_table[:email].eq(query)) | ||||
|           .or(arel_table[:id].in(matched_by_emails_user_ids)) | ||||
|           .or(arel_table[:id].eq(matched_by_email_user_id)) | ||||
|       ) | ||||
|     end | ||||
| 
 | ||||
|  |  | |||
|  | @ -7,8 +7,9 @@ module Users | |||
|     end | ||||
| 
 | ||||
|     def execute(user) | ||||
|       return error(_('You are not allowed to approve a user')) unless allowed? | ||||
|       return error(_('The user you are trying to approve is not pending an approval')) unless approval_required?(user) | ||||
|       return error(_('You are not allowed to approve a user'), :forbidden) unless allowed? | ||||
|       return error(_('The user you are trying to approve is not pending an approval'), :conflict) if user.active? | ||||
|       return error(_('The user you are trying to approve is not pending an approval'), :conflict) unless approval_required?(user) | ||||
| 
 | ||||
|       if user.activate | ||||
|         # Resends confirmation email if the user isn't confirmed yet. | ||||
|  | @ -18,9 +19,9 @@ module Users | |||
|         DeviseMailer.user_admin_approval(user).deliver_later | ||||
| 
 | ||||
|         after_approve_hook(user) | ||||
|         success | ||||
|         success(message: 'Success', http_status: :created) | ||||
|       else | ||||
|         error(user.errors.full_messages.uniq.join('. ')) | ||||
|         error(user.errors.full_messages.uniq.join('. '), :unprocessable_entity) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,18 +6,34 @@ module Terraform | |||
| 
 | ||||
|     storage_options Gitlab.config.terraform_state | ||||
| 
 | ||||
|     delegate :project_id, to: :model | ||||
|     delegate :terraform_state, :project_id, to: :model | ||||
| 
 | ||||
|     # Use Lockbox to encrypt/decrypt the stored file (registers CarrierWave callbacks) | ||||
|     encrypt(key: :key) | ||||
| 
 | ||||
|     def filename | ||||
|       # This check is required to maintain backwards compatibility with | ||||
|       # states that were created prior to versioning being supported. | ||||
|       # This can be removed in 14.0 when support for these states is dropped. | ||||
|       # See https://gitlab.com/gitlab-org/gitlab/-/issues/258960 | ||||
|       if terraform_state.versioning_enabled? | ||||
|         "#{model.version}.tfstate" | ||||
|       else | ||||
|         "#{model.uuid}.tfstate" | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     def store_dir | ||||
|       # This check is required to maintain backwards compatibility with | ||||
|       # states that were created prior to versioning being supported. | ||||
|       # This can be removed in 14.0 when support for these states is dropped. | ||||
|       # See https://gitlab.com/gitlab-org/gitlab/-/issues/258960 | ||||
|       if terraform_state.versioning_enabled? | ||||
|         Gitlab::HashedPath.new(model.uuid, root_hash: project_id) | ||||
|       else | ||||
|         project_id.to_s | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     def key | ||||
|       OpenSSL::HMAC.digest('SHA256', Gitlab::Application.secrets.db_key_base, project_id.to_s) | ||||
|  |  | |||
|  | @ -1,23 +0,0 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Terraform | ||||
|   class VersionedStateUploader < StateUploader | ||||
|     delegate :terraform_state, to: :model | ||||
| 
 | ||||
|     def filename | ||||
|       if terraform_state.versioning_enabled? | ||||
|         "#{model.version}.tfstate" | ||||
|       else | ||||
|         "#{model.uuid}.tfstate" | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     def store_dir | ||||
|       if terraform_state.versioning_enabled? | ||||
|         Gitlab::HashedPath.new(model.uuid, root_hash: project_id) | ||||
|       else | ||||
|         project_id.to_s | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Add secondary indexes to partitioned audit_events | ||||
| merge_request: 48270 | ||||
| author: | ||||
| type: added | ||||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Add API endoint for Administrators to approve pending users | ||||
| merge_request: 47564 | ||||
| author: | ||||
| type: added | ||||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Add User.location field to GraphQL API | ||||
| merge_request: 48059 | ||||
| author: | ||||
| type: changed | ||||
|  | @ -0,0 +1,6 @@ | |||
| --- | ||||
| title: Update alert setting form to handle JSON payload submit when mapping builder | ||||
|   is not enabled | ||||
| merge_request: 48231 | ||||
| author: | ||||
| type: fixed | ||||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Allow secondary emails in user search | ||||
| merge_request: 47587 | ||||
| author: | ||||
| type: added | ||||
|  | @ -1,7 +1,8 @@ | |||
| --- | ||||
| name: ci_variable_expansion_in_rules_changes | ||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45037 | ||||
| rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/267192 | ||||
| name: user_search_secondary_email | ||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47587 | ||||
| rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/282137 | ||||
| milestone: '13.7' | ||||
| type: development | ||||
| group: group::pipeline authoring | ||||
| default_enabled: true | ||||
| group: group::access | ||||
| default_enabled: false | ||||
|  | @ -0,0 +1,29 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class AddPartitionedAuditEventIndexes < ActiveRecord::Migration[6.0] | ||||
|   include Gitlab::Database::PartitioningMigrationHelpers | ||||
| 
 | ||||
|   DOWNTIME = false | ||||
| 
 | ||||
|   CREATED_AT_AUTHOR_ID_INDEX_NAME = 'analytics_index_audit_events_part_on_created_at_and_author_id' | ||||
|   ENTITY_ID_DESC_INDEX_NAME = 'idx_audit_events_part_on_entity_id_desc_author_id_created_at' | ||||
| 
 | ||||
|   disable_ddl_transaction! | ||||
| 
 | ||||
|   def up | ||||
|     add_concurrent_partitioned_index :audit_events_part_5fc467ac26, | ||||
|       [:created_at, :author_id], | ||||
|       name: CREATED_AT_AUTHOR_ID_INDEX_NAME | ||||
| 
 | ||||
|     add_concurrent_partitioned_index :audit_events_part_5fc467ac26, | ||||
|       [:entity_id, :entity_type, :id, :author_id, :created_at], | ||||
|       order: { id: :desc }, | ||||
|       name: ENTITY_ID_DESC_INDEX_NAME | ||||
|   end | ||||
| 
 | ||||
|   def down | ||||
|     remove_concurrent_partitioned_index_by_name :audit_events_part_5fc467ac26, ENTITY_ID_DESC_INDEX_NAME | ||||
| 
 | ||||
|     remove_concurrent_partitioned_index_by_name :audit_events_part_5fc467ac26, CREATED_AT_AUTHOR_ID_INDEX_NAME | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1 @@ | |||
| d8d774e788eeaaecbda3cb7c5530926e74843d844bfad27b6a6e65bf5f89ac8a | ||||
|  | @ -20038,6 +20038,8 @@ CREATE INDEX active_billable_users ON users USING btree (id) WHERE (((state)::te | |||
| 
 | ||||
| CREATE INDEX analytics_index_audit_events_on_created_at_and_author_id ON audit_events USING btree (created_at, author_id); | ||||
| 
 | ||||
| CREATE INDEX analytics_index_audit_events_part_on_created_at_and_author_id ON ONLY audit_events_part_5fc467ac26 USING btree (created_at, author_id); | ||||
| 
 | ||||
| CREATE INDEX analytics_index_events_on_created_at_and_author_id ON events USING btree (created_at, author_id); | ||||
| 
 | ||||
| CREATE INDEX analytics_repository_languages_on_project_id ON analytics_language_trend_repository_languages USING btree (project_id); | ||||
|  | @ -20082,6 +20084,8 @@ CREATE INDEX finding_links_on_vulnerability_occurrence_id ON vulnerability_findi | |||
| 
 | ||||
| CREATE INDEX idx_audit_events_on_entity_id_desc_author_id_created_at ON audit_events USING btree (entity_id, entity_type, id DESC, author_id, created_at); | ||||
| 
 | ||||
| CREATE INDEX idx_audit_events_part_on_entity_id_desc_author_id_created_at ON ONLY audit_events_part_5fc467ac26 USING btree (entity_id, entity_type, id DESC, author_id, created_at); | ||||
| 
 | ||||
| CREATE INDEX idx_ci_pipelines_artifacts_locked ON ci_pipelines USING btree (ci_ref_id, id) WHERE (locked = 1); | ||||
| 
 | ||||
| CREATE INDEX idx_container_exp_policies_on_project_id_next_run_at_enabled ON container_expiration_policies USING btree (project_id, next_run_at, enabled); | ||||
|  |  | |||
|  | @ -23030,6 +23030,11 @@ type User { | |||
|   """ | ||||
|   id: ID! | ||||
| 
 | ||||
|   """ | ||||
|   The location of the user. | ||||
|   """ | ||||
|   location: String | ||||
| 
 | ||||
|   """ | ||||
|   Human-readable name of the user | ||||
|   """ | ||||
|  |  | |||
|  | @ -66794,6 +66794,20 @@ | |||
|               "isDeprecated": false, | ||||
|               "deprecationReason": null | ||||
|             }, | ||||
|             { | ||||
|               "name": "location", | ||||
|               "description": "The location of the user.", | ||||
|               "args": [ | ||||
| 
 | ||||
|               ], | ||||
|               "type": { | ||||
|                 "kind": "SCALAR", | ||||
|                 "name": "String", | ||||
|                 "ofType": null | ||||
|               }, | ||||
|               "isDeprecated": false, | ||||
|               "deprecationReason": null | ||||
|             }, | ||||
|             { | ||||
|               "name": "name", | ||||
|               "description": "Human-readable name of the user", | ||||
|  |  | |||
|  | @ -3450,6 +3450,7 @@ Autogenerated return type of UpdateSnippet. | |||
| | `groupCount` | Int | Group count for the user. Available only when feature flag `user_group_counts` is enabled | | ||||
| | `groupMemberships` | GroupMemberConnection | Group memberships of the user | | ||||
| | `id` | ID! | ID of the user | | ||||
| | `location` | String | The location of the user. | | ||||
| | `name` | String! | Human-readable name of the user | | ||||
| | `projectMemberships` | ProjectMemberConnection | Project memberships of the user | | ||||
| | `snippets` | SnippetConnection | Snippets authored by the user | | ||||
|  |  | |||
|  | @ -1275,8 +1275,8 @@ Parameters: | |||
| Returns: | ||||
| 
 | ||||
| - `201 OK` on success. | ||||
| - `404 User Not Found` if user cannot be found. | ||||
| - `403 Forbidden` when trying to activate a user blocked by admin or by LDAP synchronization. | ||||
| - `404 User Not Found` if the user cannot be found. | ||||
| - `403 Forbidden` if the user cannot be activated because they are blocked by an administrator or by LDAP synchronization. | ||||
| 
 | ||||
| ### Get user contribution events | ||||
| 
 | ||||
|  | @ -1337,6 +1337,44 @@ Example response: | |||
| ] | ||||
| ``` | ||||
| 
 | ||||
| ## Approve user | ||||
| 
 | ||||
| > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/263107) in GitLab 13.7. | ||||
| 
 | ||||
| Approves the specified user. Available only for administrators. | ||||
| 
 | ||||
| ```plaintext | ||||
| POST /users/:id/approve | ||||
| ``` | ||||
| 
 | ||||
| Parameters: | ||||
| 
 | ||||
| - `id` (required) - ID of specified user | ||||
| 
 | ||||
| ```shell | ||||
| curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/users/42/approve" | ||||
| ``` | ||||
| 
 | ||||
| Returns: | ||||
| 
 | ||||
| - `201 OK` on success. | ||||
| - `404 User Not Found` if user cannot be found. | ||||
| - `403 Forbidden` if the user cannot be approved because they are blocked by an administrator or by LDAP synchronization. | ||||
| 
 | ||||
| Example Responses: | ||||
| 
 | ||||
| ```json | ||||
| { "message": "Success" } | ||||
| ``` | ||||
| 
 | ||||
| ```json | ||||
| { "message": "404 User Not Found" } | ||||
| ``` | ||||
| 
 | ||||
| ```json | ||||
| { "message": "The user you are trying to approve is not pending an approval" } | ||||
| ``` | ||||
| 
 | ||||
| ## Get an impersonation token of a user | ||||
| 
 | ||||
| > Requires admin permissions. | ||||
|  |  | |||
|  | @ -1342,14 +1342,7 @@ if there is no `if:` statement that limits the job to branch or merge request pi | |||
| ##### Variables in `rules:changes` | ||||
| 
 | ||||
| > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34272) in GitLab 13.6. | ||||
| > - It was [deployed behind a feature flag](../../user/feature_flags.md), disabled by default. | ||||
| > - [Became enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/267192) in GitLab 13.6. | ||||
| > - It's enabled on GitLab.com. | ||||
| > - It's recommended for production use. | ||||
| > - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-variables-support-in-ruleschanges). **(CORE ONLY)** | ||||
| 
 | ||||
| CAUTION: **Warning:** | ||||
| This feature might not be available to you. Check the **version history** note above for details. | ||||
| > - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/267192) in GitLab 13.7. | ||||
| 
 | ||||
| Environment variables can be used in `rules:changes` expressions to determine when | ||||
| to add jobs to a pipeline: | ||||
|  | @ -1368,25 +1361,6 @@ The `$` character can be used for both variables and paths. For example, if the | |||
| `$DOCKERFILES_DIR` variable exists, its value is used. If it does not exist, the | ||||
| `$` is interpreted as being part of a path. | ||||
| 
 | ||||
| ###### Enable or disable variables support in `rules:changes` **(CORE ONLY)** | ||||
| 
 | ||||
| Variables support in `rules:changes` is under development, but is ready for production use. | ||||
| It is deployed behind a feature flag that is **enabled by default**. | ||||
| [GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md) | ||||
| can opt to disable it. | ||||
| 
 | ||||
| To enable it: | ||||
| 
 | ||||
| ```ruby | ||||
| Feature.enable(:ci_variable_expansion_in_rules_changes) | ||||
| ``` | ||||
| 
 | ||||
| To disable it: | ||||
| 
 | ||||
| ```ruby | ||||
| Feature.disable(:ci_variable_expansion_in_rules_changes) | ||||
| ``` | ||||
| 
 | ||||
| #### `rules:exists` | ||||
| 
 | ||||
| > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/24021) in GitLab 12.4. | ||||
|  |  | |||
|  | @ -257,19 +257,22 @@ Users of GitLab 12.1 and earlier should use the command `gitlab-rake gitlab:back | |||
| 
 | ||||
| #### Excluding specific directories from the backup | ||||
| 
 | ||||
| You can choose what should be exempt from the backup by adding the environment | ||||
| variable `SKIP`. The available options are: | ||||
| You can exclude specific directories from the backup by adding the environment variable `SKIP`, whose values are a comma-separated list of the following options: | ||||
| 
 | ||||
| - `db` (database) | ||||
| - `uploads` (attachments) | ||||
| - `repositories` (Git repositories data) | ||||
| - `builds` (CI job output logs) | ||||
| - `artifacts` (CI job artifacts) | ||||
| - `lfs` (LFS objects) | ||||
| - `registry` (Container Registry images) | ||||
| - `pages` (Pages content) | ||||
| - `repositories` (Git repositories data)   | ||||
| 
 | ||||
| Use a comma to specify several options at the same time: | ||||
| All wikis will be backed up as part of the `repositories` group. Non-existent wikis will be skipped during a backup. | ||||
|    | ||||
| NOTE: **Note:** | ||||
| When [backing up and restoring Helm Charts](https://docs.gitlab.com/charts/architecture/backup-restore.html), there is an additional option `packages`, which refers to any packages managed by the GitLab [package registry](../user/packages/package_registry/index.md).    | ||||
| For more information see [command line arguments](https://docs.gitlab.com/charts/architecture/backup-restore.html#command-line-arguments). | ||||
| 
 | ||||
| All wikis are backed up as part of the `repositories` group. Non-existent | ||||
| wikis are skipped during a backup. | ||||
|  |  | |||
|  | @ -283,3 +283,25 @@ Support for custom certificate authorities was introduced in the following versi | |||
| ### Getting warning message `gl-secret-detection-report.json: no matching files` | ||||
| 
 | ||||
| For information on this, see the [general Application Security troubleshooting section](../../../ci/pipelines/job_artifacts.md#error-message-no-files-to-upload). | ||||
| 
 | ||||
| ### Error: `Couldn't run the gitleaks command: exit status 2` | ||||
| 
 | ||||
| This error is usually caused by the `GIT_DEPTH` value of 50 that is set for all [projects by default](../../../ci/pipelines/settings.md#git-shallow-clone).  | ||||
| 
 | ||||
| For example, if a pipeline is triggered from a Merge Request containing 60 commits while the `GIT_DEPTH` is set to 50, the Secret Detection job will fail as the clone will not have been deep enough to contain all of the relevant commits.  | ||||
| 
 | ||||
| You can confirm this to be the cause of the error by implementing a [logging level](../../application_security/secret_detection/index.md#logging-level) of `debug`. Once implemented, the logs should look similar to the following example, wherein an "object not found" error can be seen: | ||||
| 
 | ||||
| ```plaintext | ||||
| ERRO[2020-11-18T18:05:52Z] object not found                              | ||||
| [ERRO] [secrets] [2020-11-18T18:05:52Z] ▶ Couldn't run the gitleaks command: exit status 2 | ||||
| [ERRO] [secrets] [2020-11-18T18:05:52Z] ▶ Gitleaks analysis failed: exit status 2 | ||||
| ``` | ||||
| 
 | ||||
| If this is the case, we can resolve the issue by setting the [`GIT_DEPTH` variable](../../../ci/runners/README.md#shallow-cloning) to a higher value. In order to apply this only to the Secret Detection job, the following can be added to your `.gitlab-ci.yml`: | ||||
| 
 | ||||
| ```yaml | ||||
| secret_detection: | ||||
|   variables: | ||||
|     GIT_DEPTH: 100 | ||||
| ``` | ||||
|  |  | |||
|  | @ -534,6 +534,24 @@ module API | |||
| 
 | ||||
|         user.activate | ||||
|       end | ||||
| 
 | ||||
|       desc 'Approve a pending user. Available only for admins.' | ||||
|       params do | ||||
|         requires :id, type: Integer, desc: 'The ID of the user' | ||||
|       end | ||||
|       post ':id/approve', feature_category: :authentication_and_authorization do | ||||
|         user = User.find_by(id: params[:id]) | ||||
|         not_found!('User') unless can?(current_user, :read_user, user) | ||||
| 
 | ||||
|         result = ::Users::ApproveService.new(current_user).execute(user) | ||||
| 
 | ||||
|         if result[:success] | ||||
|           result | ||||
|         else | ||||
|           render_api_error!(result[:message], result[:http_status]) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       # rubocop: enable CodeReuse/ActiveRecord | ||||
|       desc 'Deactivate an active user. Available only for admins.' | ||||
|       params do | ||||
|  |  | |||
|  | @ -11,7 +11,7 @@ module Gitlab | |||
|         def satisfied_by?(pipeline, context) | ||||
|           return true if pipeline.modified_paths.nil? | ||||
| 
 | ||||
|           expanded_globs = expand_globs(pipeline, context) | ||||
|           expanded_globs = expand_globs(context) | ||||
|           pipeline.modified_paths.any? do |path| | ||||
|             expanded_globs.any? do |glob| | ||||
|               File.fnmatch?(glob, path, File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB) | ||||
|  | @ -19,8 +19,7 @@ module Gitlab | |||
|           end | ||||
|         end | ||||
| 
 | ||||
|         def expand_globs(pipeline, context) | ||||
|           return @globs unless ::Feature.enabled?(:ci_variable_expansion_in_rules_changes, pipeline.project, default_enabled: true) | ||||
|         def expand_globs(context) | ||||
|           return @globs unless context | ||||
| 
 | ||||
|           @globs.map do |glob| | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| .auto-deploy: | ||||
|   image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v2.0.0-beta.2" | ||||
|   image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v2.0.0" | ||||
|   dependencies: [] | ||||
| 
 | ||||
| review: | ||||
|  |  | |||
|  | @ -2733,6 +2733,9 @@ msgstr "" | |||
| msgid "AlertsIntegrations|The integration token could not be reset. Please try again." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "AlertsIntegrations|The test alert has been successfully sent, and should now be visible on your alerts list." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "AlertsIntegrations|You have opted to delete the %{integrationName} integration. Do you want to proceed? It means you will no longer receive alerts from this endpoint in your alert list, and this action cannot be undone." | ||||
| msgstr "" | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,69 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module RuboCop | ||||
|   module Cop | ||||
|     module Lint | ||||
|       # This cop only works if there are files from deprecation_toolkit. You can | ||||
|       # generate these files by: | ||||
|       # | ||||
|       # 1. Running specs with RECORD_DEPRECATIONS=1 | ||||
|       # 1. Downloading the complete set of deprecations/ files from a CI | ||||
|       # pipeline (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47720) | ||||
|       class LastKeywordArgument < Cop | ||||
|         MSG = 'Using the last argument as keyword parameters is deprecated'.freeze | ||||
| 
 | ||||
|         DEPRECATIONS_GLOB = File.expand_path('../../../deprecations/**/*.yml', __dir__) | ||||
|         KEYWORD_DEPRECATION_STR = 'maybe ** should be added to the call' | ||||
| 
 | ||||
|         def on_send(node) | ||||
|           arg = node.arguments.last | ||||
|           return unless arg | ||||
| 
 | ||||
|           return unless known_match?(processed_source.file_path, node.first_line, node.method_name.to_s) | ||||
| 
 | ||||
|           return if arg.children.first.respond_to?(:kwsplat_type?) && arg.children.first&.kwsplat_type? | ||||
| 
 | ||||
|           # parser thinks `a: :b, c: :d` is hash type, it's actually kwargs | ||||
|           return if arg.hash_type? && !arg.source.match(/\A{/) | ||||
| 
 | ||||
|           add_offense(arg) | ||||
|         end | ||||
| 
 | ||||
|         def autocorrect(arg) | ||||
|           lambda do |corrector| | ||||
|             if arg.hash_type? | ||||
|               kwarg = arg.source.sub(/\A{\s*/, '').sub(/\s*}\z/, '') | ||||
|               corrector.replace(arg, kwarg) | ||||
|             elsif arg.splat_type? | ||||
|               corrector.insert_before(arg, '*') | ||||
|             else | ||||
|               corrector.insert_before(arg, '**') | ||||
|             end | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         private | ||||
| 
 | ||||
|         def known_match?(file_path, line_number, method_name) | ||||
|           file_path_from_root = file_path.sub(File.expand_path('../../..', __dir__), '') | ||||
| 
 | ||||
|           self.class.keyword_warnings.any? do |warning| | ||||
|             warning.include?("#{file_path_from_root}:#{line_number}") && warning.include?("called method `#{method_name}'") | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         def self.keyword_warnings | ||||
|           @keyword_warnings ||= keywords_list | ||||
|         end | ||||
| 
 | ||||
|         def self.keywords_list | ||||
|           hash = Dir.glob(DEPRECATIONS_GLOB).each_with_object({}) do |file, hash| | ||||
|             hash.merge!(YAML.safe_load(File.read(file))) | ||||
|           end | ||||
| 
 | ||||
|           hash.values.flatten.select { |str| str.include?(KEYWORD_DEPRECATION_STR) }.uniq | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,18 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| if ENV.key?('RECORD_DEPRECATIONS') | ||||
|   require 'deprecation_toolkit' | ||||
|   require 'deprecation_toolkit/rspec' | ||||
|   DeprecationToolkit::Configuration.test_runner = :rspec | ||||
|   DeprecationToolkit::Configuration.deprecation_path = 'deprecations' | ||||
|   DeprecationToolkit::Configuration.behavior = DeprecationToolkit::Behaviors::Record | ||||
| 
 | ||||
|   # Enable ruby deprecations for keywords, it's suppressed by default in Ruby 2.7.2 | ||||
|   Warning[:deprecated] = true | ||||
| 
 | ||||
|   kwargs_warnings = [ | ||||
|     # Taken from https://github.com/jeremyevans/ruby-warning/blob/1.1.0/lib/warning.rb#L18 | ||||
|     %r{warning: (?:Using the last argument (?:for `.+' )?as keyword parameters is deprecated; maybe \*\* should be added to the call|Passing the keyword argument (?:for `.+' )?as the last hash parameter is deprecated|Splitting the last argument (?:for `.+' )?into positional and keyword parameters is deprecated|The called method (?:`.+' )?is defined here)\n\z} | ||||
|   ] | ||||
|   DeprecationToolkit::Configuration.warnings_treated_as_deprecation = kwargs_warnings | ||||
| end | ||||
|  | @ -6,11 +6,6 @@ FactoryBot.define do | |||
| 
 | ||||
|     sequence(:name) { |n| "state-#{n}" } | ||||
| 
 | ||||
|     trait :with_file do | ||||
|       versioning_enabled { false } | ||||
|       file { fixture_file_upload('spec/fixtures/terraform/terraform.tfstate', 'application/json') } | ||||
|     end | ||||
| 
 | ||||
|     trait :locked do | ||||
|       sequence(:lock_xid) { |n| "lock-#{n}" } | ||||
|       locked_at { Time.current } | ||||
|  | @ -22,8 +17,5 @@ FactoryBot.define do | |||
|         create(:terraform_state_version, terraform_state: state) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     # Remove with https://gitlab.com/gitlab-org/gitlab/-/issues/235108 | ||||
|     factory :legacy_terraform_state, parent: :terraform_state, traits: [:with_file] | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -35,6 +35,10 @@ FactoryBot.define do | |||
|       user_type { :alert_bot } | ||||
|     end | ||||
| 
 | ||||
|     trait :deactivated do | ||||
|       after(:build) { |user, _| user.deactivate! } | ||||
|     end | ||||
| 
 | ||||
|     trait :project_bot do | ||||
|       user_type { :project_bot } | ||||
|     end | ||||
|  |  | |||
|  | @ -87,7 +87,7 @@ exports[`AlertsSettingsFormNew with default values renders the initial template | |||
|     <div class=\\"gl-display-flex gl-justify-content-start gl-py-3\\"><button data-testid=\\"integration-form-submit\\" type=\\"submit\\" class=\\"btn js-no-auto-disable btn-success btn-md gl-button\\"> | ||||
|         <!----> | ||||
|         <!----> <span class=\\"gl-button-text\\">Save integration | ||||
|       </span></button> <button data-testid=\\"integration-test-and-submit\\" type=\\"button\\" class=\\"btn gl-mx-3 js-no-auto-disable btn-success btn-md gl-button btn-success-secondary\\"> | ||||
|       </span></button> <button data-testid=\\"integration-test-and-submit\\" type=\\"button\\" disabled=\\"disabled\\" class=\\"btn gl-mx-3 js-no-auto-disable btn-success btn-md disabled gl-button btn-success-secondary\\"> | ||||
|         <!----> | ||||
|         <!----> <span class=\\"gl-button-text\\">Save and test payload</span></button> <button type=\\"reset\\" class=\\"btn js-no-auto-disable btn-default btn-md gl-button\\"> | ||||
|         <!----> | ||||
|  | @ -8,7 +8,7 @@ import { | |||
|   GlFormTextarea, | ||||
| } from '@gitlab/ui'; | ||||
| import waitForPromises from 'helpers/wait_for_promises'; | ||||
| import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form_new.vue'; | ||||
| import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form.vue'; | ||||
| import { defaultAlertSettingsConfig } from './util'; | ||||
| import { typeSet } from '~/alerts_settings/constants'; | ||||
| 
 | ||||
|  | @ -1,11 +1,13 @@ | |||
| import VueApollo from 'vue-apollo'; | ||||
| import { mount, createLocalVue } from '@vue/test-utils'; | ||||
| import AxiosMockAdapter from 'axios-mock-adapter'; | ||||
| import createMockApollo from 'jest/helpers/mock_apollo_helper'; | ||||
| import waitForPromises from 'helpers/wait_for_promises'; | ||||
| import { GlLoadingIcon, GlAlert } from '@gitlab/ui'; | ||||
| import { useMockIntersectionObserver } from 'helpers/mock_dom_observer'; | ||||
| import { GlLoadingIcon, GlAlert } from '@gitlab/ui'; | ||||
| import axios from '~/lib/utils/axios_utils'; | ||||
| import AlertsSettingsWrapper from '~/alerts_settings/components/alerts_settings_wrapper.vue'; | ||||
| import AlertsSettingsFormNew from '~/alerts_settings/components/alerts_settings_form_new.vue'; | ||||
| import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form.vue'; | ||||
| import IntegrationsList from '~/alerts_settings/components/alerts_integrations_list.vue'; | ||||
| import getIntegrationsQuery from '~/alerts_settings/graphql/queries/get_integrations.query.graphql'; | ||||
| import createHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql'; | ||||
|  | @ -113,17 +115,15 @@ describe('AlertsSettingsWrapper', () => { | |||
|   } | ||||
| 
 | ||||
|   afterEach(() => { | ||||
|     if (wrapper) { | ||||
|     wrapper.destroy(); | ||||
|     wrapper = null; | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   describe('rendered via default permissions', () => { | ||||
|     it('renders the GraphQL alerts integrations list and new form', () => { | ||||
|       createComponent(); | ||||
|       expect(wrapper.find(IntegrationsList).exists()).toBe(true); | ||||
|       expect(wrapper.find(AlertsSettingsFormNew).exists()).toBe(true); | ||||
|       expect(wrapper.find(AlertsSettingsForm).exists()).toBe(true); | ||||
|     }); | ||||
| 
 | ||||
|     it('uses a loading state inside the IntegrationsList table', () => { | ||||
|  | @ -153,7 +153,7 @@ describe('AlertsSettingsWrapper', () => { | |||
|       jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ | ||||
|         data: { createHttpIntegrationMutation: { integration: { id: '1' } } }, | ||||
|       }); | ||||
|       wrapper.find(AlertsSettingsFormNew).vm.$emit('create-new-integration', { | ||||
|       wrapper.find(AlertsSettingsForm).vm.$emit('create-new-integration', { | ||||
|         type: typeSet.http, | ||||
|         variables: createHttpVariables, | ||||
|       }); | ||||
|  | @ -175,7 +175,7 @@ describe('AlertsSettingsWrapper', () => { | |||
|       jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ | ||||
|         data: { updateHttpIntegrationMutation: { integration: { id: '1' } } }, | ||||
|       }); | ||||
|       wrapper.find(AlertsSettingsFormNew).vm.$emit('update-integration', { | ||||
|       wrapper.find(AlertsSettingsForm).vm.$emit('update-integration', { | ||||
|         type: typeSet.http, | ||||
|         variables: updateHttpVariables, | ||||
|       }); | ||||
|  | @ -195,7 +195,7 @@ describe('AlertsSettingsWrapper', () => { | |||
|       jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ | ||||
|         data: { resetHttpTokenMutation: { integration: { id: '1' } } }, | ||||
|       }); | ||||
|       wrapper.find(AlertsSettingsFormNew).vm.$emit('reset-token', { | ||||
|       wrapper.find(AlertsSettingsForm).vm.$emit('reset-token', { | ||||
|         type: typeSet.http, | ||||
|         variables: { id: ID }, | ||||
|       }); | ||||
|  | @ -217,7 +217,7 @@ describe('AlertsSettingsWrapper', () => { | |||
|       jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ | ||||
|         data: { createPrometheusIntegrationMutation: { integration: { id: '2' } } }, | ||||
|       }); | ||||
|       wrapper.find(AlertsSettingsFormNew).vm.$emit('create-new-integration', { | ||||
|       wrapper.find(AlertsSettingsForm).vm.$emit('create-new-integration', { | ||||
|         type: typeSet.prometheus, | ||||
|         variables: createPrometheusVariables, | ||||
|       }); | ||||
|  | @ -239,7 +239,7 @@ describe('AlertsSettingsWrapper', () => { | |||
|       jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ | ||||
|         data: { updatePrometheusIntegrationMutation: { integration: { id: '2' } } }, | ||||
|       }); | ||||
|       wrapper.find(AlertsSettingsFormNew).vm.$emit('update-integration', { | ||||
|       wrapper.find(AlertsSettingsForm).vm.$emit('update-integration', { | ||||
|         type: typeSet.prometheus, | ||||
|         variables: updatePrometheusVariables, | ||||
|       }); | ||||
|  | @ -259,7 +259,7 @@ describe('AlertsSettingsWrapper', () => { | |||
|       jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ | ||||
|         data: { resetPrometheusTokenMutation: { integration: { id: '1' } } }, | ||||
|       }); | ||||
|       wrapper.find(AlertsSettingsFormNew).vm.$emit('reset-token', { | ||||
|       wrapper.find(AlertsSettingsForm).vm.$emit('reset-token', { | ||||
|         type: typeSet.prometheus, | ||||
|         variables: { id: ID }, | ||||
|       }); | ||||
|  | @ -279,7 +279,7 @@ describe('AlertsSettingsWrapper', () => { | |||
|       }); | ||||
| 
 | ||||
|       jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(ADD_INTEGRATION_ERROR); | ||||
|       wrapper.find(AlertsSettingsFormNew).vm.$emit('create-new-integration', {}); | ||||
|       wrapper.find(AlertsSettingsForm).vm.$emit('create-new-integration', {}); | ||||
| 
 | ||||
|       await waitForPromises(); | ||||
| 
 | ||||
|  | @ -294,7 +294,7 @@ describe('AlertsSettingsWrapper', () => { | |||
| 
 | ||||
|       jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(RESET_INTEGRATION_TOKEN_ERROR); | ||||
| 
 | ||||
|       wrapper.find(AlertsSettingsFormNew).vm.$emit('reset-token', {}); | ||||
|       wrapper.find(AlertsSettingsForm).vm.$emit('reset-token', {}); | ||||
| 
 | ||||
|       await waitForPromises(); | ||||
|       expect(createFlash).toHaveBeenCalledWith({ message: RESET_INTEGRATION_TOKEN_ERROR }); | ||||
|  | @ -308,23 +308,25 @@ describe('AlertsSettingsWrapper', () => { | |||
| 
 | ||||
|       jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(errorMsg); | ||||
| 
 | ||||
|       wrapper.find(AlertsSettingsFormNew).vm.$emit('update-integration', {}); | ||||
|       wrapper.find(AlertsSettingsForm).vm.$emit('update-integration', {}); | ||||
| 
 | ||||
|       await waitForPromises(); | ||||
|       expect(createFlash).toHaveBeenCalledWith({ message: UPDATE_INTEGRATION_ERROR }); | ||||
|     }); | ||||
| 
 | ||||
|     it('shows an error alert when integration test payload fails ', async () => { | ||||
|       const mock = new AxiosMockAdapter(axios); | ||||
|       mock.onPost(/(.*)/).replyOnce(403); | ||||
|       createComponent({ | ||||
|         data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] }, | ||||
|         loading: false, | ||||
|       }); | ||||
| 
 | ||||
|       wrapper.find(AlertsSettingsFormNew).vm.$emit('test-payload-failure'); | ||||
| 
 | ||||
|       await waitForPromises(); | ||||
|       return wrapper.vm.validateAlertPayload({ endpoint: '', data: '', token: '' }).then(() => { | ||||
|         expect(createFlash).toHaveBeenCalledWith({ message: INTEGRATION_PAYLOAD_TEST_ERROR }); | ||||
|         expect(createFlash).toHaveBeenCalledTimes(1); | ||||
|         mock.restore(); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -23,12 +23,12 @@ RSpec.describe Mutations::Boards::Lists::Create do | |||
| 
 | ||||
|   describe '#ready?' do | ||||
|     it 'raises an error if required arguments are missing' do | ||||
|       expect { mutation.ready?({ board_id: 'some id' }) } | ||||
|       expect { mutation.ready?(board_id: 'some id') } | ||||
|         .to raise_error(Gitlab::Graphql::Errors::ArgumentError, /one and only one of/) | ||||
|     end | ||||
| 
 | ||||
|     it 'raises an error if too many required arguments are specified' do | ||||
|       expect { mutation.ready?({ board_id: 'some id', backlog: true, label_id: 'some label' }) } | ||||
|       expect { mutation.ready?(board_id: 'some id', backlog: true, label_id: 'some label') } | ||||
|         .to raise_error(Gitlab::Graphql::Errors::ArgumentError, /one and only one of/) | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -21,6 +21,7 @@ RSpec.describe GitlabSchema.types['User'] do | |||
|       todos | ||||
|       state | ||||
|       status | ||||
|       location | ||||
|       authoredMergeRequests | ||||
|       assignedMergeRequests | ||||
|       groupMemberships | ||||
|  |  | |||
|  | @ -3,21 +3,13 @@ | |||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe Terraform::State do | ||||
|   subject { create(:terraform_state, :with_file) } | ||||
| 
 | ||||
|   let(:terraform_state_file) { fixture_file('terraform/terraform.tfstate') } | ||||
| 
 | ||||
|   it { is_expected.to be_a FileStoreMounter } | ||||
|   subject { create(:terraform_state, :with_version) } | ||||
| 
 | ||||
|   it { is_expected.to belong_to(:project) } | ||||
|   it { is_expected.to belong_to(:locked_by_user).class_name('User') } | ||||
| 
 | ||||
|   it { is_expected.to validate_presence_of(:project_id) } | ||||
| 
 | ||||
|   before do | ||||
|     stub_terraform_state_object_storage | ||||
|   end | ||||
| 
 | ||||
|   describe 'scopes' do | ||||
|     describe '.ordered_by_name' do | ||||
|       let_it_be(:project) { create(:project) } | ||||
|  | @ -35,45 +27,12 @@ RSpec.describe Terraform::State do | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#file' do | ||||
|     context 'when a file exists' do | ||||
|       it 'does not use the default file' do | ||||
|         expect(subject.file.read).to eq(terraform_state_file) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#file_store' do | ||||
|     context 'when a value is set' do | ||||
|       it 'returns the value' do | ||||
|         [ObjectStorage::Store::LOCAL, ObjectStorage::Store::REMOTE].each do |store| | ||||
|           expect(build(:terraform_state, file_store: store).file_store).to eq(store) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#update_file_store' do | ||||
|     context 'when file is stored in object storage' do | ||||
|       it_behaves_like 'mounted file in object store' | ||||
|     end | ||||
| 
 | ||||
|     context 'when file is stored locally' do | ||||
|       before do | ||||
|         stub_terraform_state_object_storage(enabled: false) | ||||
|       end | ||||
| 
 | ||||
|       it_behaves_like 'mounted file in local store' | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#latest_file' do | ||||
|     subject { terraform_state.latest_file } | ||||
| 
 | ||||
|     context 'versioning is enabled' do | ||||
|     let(:terraform_state) { create(:terraform_state, :with_version) } | ||||
|     let(:latest_version) { terraform_state.latest_version } | ||||
| 
 | ||||
|     subject { terraform_state.latest_file } | ||||
| 
 | ||||
|     it { is_expected.to eq latest_version.file } | ||||
| 
 | ||||
|     context 'but no version exists yet' do | ||||
|  | @ -83,19 +42,6 @@ RSpec.describe Terraform::State do | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|     context 'versioning is disabled' do | ||||
|       let(:terraform_state) { create(:terraform_state, :with_file) } | ||||
| 
 | ||||
|       it { is_expected.to eq terraform_state.file } | ||||
| 
 | ||||
|       context 'and a version exists (migration to versioned in progress)' do | ||||
|         let!(:migrated_version) { create(:terraform_state_version, terraform_state: terraform_state) } | ||||
| 
 | ||||
|         it { is_expected.to eq terraform_state.latest_version.file } | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#update_file!' do | ||||
|     let_it_be(:build) { create(:ci_build) } | ||||
|     let_it_be(:version) { 3 } | ||||
|  | @ -115,23 +61,15 @@ RSpec.describe Terraform::State do | |||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'versioning is disabled' do | ||||
|       let(:terraform_state) { create(:terraform_state, :with_file) } | ||||
| 
 | ||||
|       it 'modifies the existing state record' do | ||||
|         expect { subject }.not_to change { Terraform::StateVersion.count } | ||||
| 
 | ||||
|         expect(terraform_state.latest_file.read).to eq(data) | ||||
|       end | ||||
| 
 | ||||
|       context 'and a version exists (migration to versioned in progress)' do | ||||
|     context 'versioning is disabled (migration to versioned in progress)' do | ||||
|       let(:terraform_state) { create(:terraform_state, versioning_enabled: false) } | ||||
|       let!(:migrated_version) { create(:terraform_state_version, terraform_state: terraform_state, version: 0) } | ||||
| 
 | ||||
|       it 'creates a new version, corrects the migrated version number, and marks the state as versioned' do | ||||
|         expect { subject }.to change { Terraform::StateVersion.count } | ||||
| 
 | ||||
|         expect(migrated_version.reload.version).to eq(1) | ||||
|           expect(migrated_version.file.read).to eq(terraform_state_file) | ||||
|         expect(migrated_version.file.read).to eq(fixture_file('terraform/terraform.tfstate')) | ||||
| 
 | ||||
|         expect(terraform_state.reload.latest_version.version).to eq(version) | ||||
|         expect(terraform_state.latest_version.file.read).to eq(data) | ||||
|  | @ -151,5 +89,4 @@ RSpec.describe Terraform::State do | |||
|       end | ||||
|     end | ||||
|   end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -2024,9 +2024,10 @@ RSpec.describe User do | |||
|   end | ||||
| 
 | ||||
|   describe '.search' do | ||||
|     let!(:user) { create(:user, name: 'user', username: 'usern', email: 'email@gmail.com') } | ||||
|     let!(:user2) { create(:user, name: 'user name', username: 'username', email: 'someemail@gmail.com') } | ||||
|     let!(:user3) { create(:user, name: 'us', username: 'se', email: 'foo@gmail.com') } | ||||
|     let_it_be(:user) { create(:user, name: 'user', username: 'usern', email: 'email@example.com') } | ||||
|     let_it_be(:user2) { create(:user, name: 'user name', username: 'username', email: 'someemail@example.com') } | ||||
|     let_it_be(:user3) { create(:user, name: 'us', username: 'se', email: 'foo@example.com') } | ||||
|     let_it_be(:email) { create(:email, user: user, email: 'alias@example.com') } | ||||
| 
 | ||||
|     describe 'name matching' do | ||||
|       it 'returns users with a matching name with exact match first' do | ||||
|  | @ -2056,7 +2057,7 @@ RSpec.describe User do | |||
|       end | ||||
| 
 | ||||
|       it 'does not return users with a partially matching Email' do | ||||
|         expect(described_class.search(user.email[0..2])).not_to include(user, user2) | ||||
|         expect(described_class.search(user.email[1...-1])).to be_empty | ||||
|       end | ||||
| 
 | ||||
|       it 'returns users with a matching Email regardless of the casing' do | ||||
|  | @ -2064,6 +2065,36 @@ RSpec.describe User do | |||
|       end | ||||
|     end | ||||
| 
 | ||||
|     describe 'secondary email matching' do | ||||
|       context 'feature flag :user_search_secondary_email is enabled' do | ||||
|         it 'returns users with a matching secondary email' do | ||||
|           expect(described_class.search(email.email)).to include(email.user) | ||||
|         end | ||||
| 
 | ||||
|         it 'does not return users with a matching part of secondary email' do | ||||
|           expect(described_class.search(email.email[1...-1])).to be_empty | ||||
|         end | ||||
| 
 | ||||
|         it 'returns users with a matching secondary email regardless of the casing' do | ||||
|           expect(described_class.search(email.email.upcase)).to include(email.user) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'feature flag :user_search_secondary_email is disabled' do | ||||
|         before do | ||||
|           stub_feature_flags(user_search_secondary_email: false) | ||||
|         end | ||||
| 
 | ||||
|         it 'does not return users with a matching secondary email' do | ||||
|           expect(described_class.search(email.email)).not_to include(email.user) | ||||
|         end | ||||
| 
 | ||||
|         it 'does not return users with a matching part of secondary email' do | ||||
|           expect(described_class.search(email.email[1...-1])).to be_empty | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     describe 'username matching' do | ||||
|       it 'returns users with a matching username' do | ||||
|         expect(described_class.search(user.username)).to eq([user, user2]) | ||||
|  | @ -2103,65 +2134,119 @@ RSpec.describe User do | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '.search_with_secondary_emails' do | ||||
|     delegate :search_with_secondary_emails, to: :described_class | ||||
| 
 | ||||
|     let!(:user) { create(:user, name: 'John Doe', username: 'john.doe', email: 'john.doe@example.com' ) } | ||||
|     let!(:another_user) { create(:user, name: 'Albert Smith', username: 'albert.smith', email: 'albert.smith@example.com' ) } | ||||
|     let!(:email) do | ||||
|       create(:email, user: another_user, email: 'alias@example.com') | ||||
|     end | ||||
|   describe '.search_without_secondary_emails' do | ||||
|     let_it_be(:user) { create(:user, name: 'John Doe', username: 'john.doe', email: 'someone.1@example.com' ) } | ||||
|     let_it_be(:another_user) { create(:user, name: 'Albert Smith', username: 'albert.smith', email: 'another.2@example.com' ) } | ||||
|     let_it_be(:email) { create(:email, user: another_user, email: 'alias@example.com') } | ||||
| 
 | ||||
|     it 'returns users with a matching name' do | ||||
|       expect(search_with_secondary_emails(user.name)).to eq([user]) | ||||
|       expect(described_class.search_without_secondary_emails(user.name)).to eq([user]) | ||||
|     end | ||||
| 
 | ||||
|     it 'returns users with a partially matching name' do | ||||
|       expect(search_with_secondary_emails(user.name[0..2])).to eq([user]) | ||||
|       expect(described_class.search_without_secondary_emails(user.name[0..2])).to eq([user]) | ||||
|     end | ||||
| 
 | ||||
|     it 'returns users with a matching name regardless of the casing' do | ||||
|       expect(search_with_secondary_emails(user.name.upcase)).to eq([user]) | ||||
|       expect(described_class.search_without_secondary_emails(user.name.upcase)).to eq([user]) | ||||
|     end | ||||
| 
 | ||||
|     it 'returns users with a matching email' do | ||||
|       expect(search_with_secondary_emails(user.email)).to eq([user]) | ||||
|       expect(described_class.search_without_secondary_emails(user.email)).to eq([user]) | ||||
|     end | ||||
| 
 | ||||
|     it 'does not return users with a partially matching email' do | ||||
|       expect(search_with_secondary_emails(user.email[0..2])).not_to include([user]) | ||||
|       expect(described_class.search_without_secondary_emails(user.email[1...-1])).to be_empty | ||||
|     end | ||||
| 
 | ||||
|     it 'returns users with a matching email regardless of the casing' do | ||||
|       expect(search_with_secondary_emails(user.email.upcase)).to eq([user]) | ||||
|       expect(described_class.search_without_secondary_emails(user.email.upcase)).to eq([user]) | ||||
|     end | ||||
| 
 | ||||
|     it 'returns users with a matching username' do | ||||
|       expect(search_with_secondary_emails(user.username)).to eq([user]) | ||||
|       expect(described_class.search_without_secondary_emails(user.username)).to eq([user]) | ||||
|     end | ||||
| 
 | ||||
|     it 'returns users with a partially matching username' do | ||||
|       expect(search_with_secondary_emails(user.username[0..2])).to eq([user]) | ||||
|       expect(described_class.search_without_secondary_emails(user.username[0..2])).to eq([user]) | ||||
|     end | ||||
| 
 | ||||
|     it 'returns users with a matching username regardless of the casing' do | ||||
|       expect(search_with_secondary_emails(user.username.upcase)).to eq([user]) | ||||
|       expect(described_class.search_without_secondary_emails(user.username.upcase)).to eq([user]) | ||||
|     end | ||||
| 
 | ||||
|     it 'returns users with a matching whole secondary email' do | ||||
|       expect(search_with_secondary_emails(email.email)).to eq([email.user]) | ||||
|     it 'does not return users with a matching whole secondary email' do | ||||
|       expect(described_class.search_without_secondary_emails(email.email)).not_to include(email.user) | ||||
|     end | ||||
| 
 | ||||
|     it 'does not return users with a matching part of secondary email' do | ||||
|       expect(search_with_secondary_emails(email.email[1..4])).not_to include([email.user]) | ||||
|       expect(described_class.search_without_secondary_emails(email.email[1...-1])).to be_empty | ||||
|     end | ||||
| 
 | ||||
|     it 'returns no matches for an empty string' do | ||||
|       expect(search_with_secondary_emails('')).to be_empty | ||||
|       expect(described_class.search_without_secondary_emails('')).to be_empty | ||||
|     end | ||||
| 
 | ||||
|     it 'returns no matches for nil' do | ||||
|       expect(search_with_secondary_emails(nil)).to be_empty | ||||
|       expect(described_class.search_without_secondary_emails(nil)).to be_empty | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '.search_with_secondary_emails' do | ||||
|     let_it_be(:user) { create(:user, name: 'John Doe', username: 'john.doe', email: 'someone.1@example.com' ) } | ||||
|     let_it_be(:another_user) { create(:user, name: 'Albert Smith', username: 'albert.smith', email: 'another.2@example.com' ) } | ||||
|     let_it_be(:email) { create(:email, user: another_user, email: 'alias@example.com') } | ||||
| 
 | ||||
|     it 'returns users with a matching name' do | ||||
|       expect(described_class.search_with_secondary_emails(user.name)).to eq([user]) | ||||
|     end | ||||
| 
 | ||||
|     it 'returns users with a partially matching name' do | ||||
|       expect(described_class.search_with_secondary_emails(user.name[0..2])).to eq([user]) | ||||
|     end | ||||
| 
 | ||||
|     it 'returns users with a matching name regardless of the casing' do | ||||
|       expect(described_class.search_with_secondary_emails(user.name.upcase)).to eq([user]) | ||||
|     end | ||||
| 
 | ||||
|     it 'returns users with a matching email' do | ||||
|       expect(described_class.search_with_secondary_emails(user.email)).to eq([user]) | ||||
|     end | ||||
| 
 | ||||
|     it 'does not return users with a partially matching email' do | ||||
|       expect(described_class.search_with_secondary_emails(user.email[1...-1])).to be_empty | ||||
|     end | ||||
| 
 | ||||
|     it 'returns users with a matching email regardless of the casing' do | ||||
|       expect(described_class.search_with_secondary_emails(user.email.upcase)).to eq([user]) | ||||
|     end | ||||
| 
 | ||||
|     it 'returns users with a matching username' do | ||||
|       expect(described_class.search_with_secondary_emails(user.username)).to eq([user]) | ||||
|     end | ||||
| 
 | ||||
|     it 'returns users with a partially matching username' do | ||||
|       expect(described_class.search_with_secondary_emails(user.username[0..2])).to eq([user]) | ||||
|     end | ||||
| 
 | ||||
|     it 'returns users with a matching username regardless of the casing' do | ||||
|       expect(described_class.search_with_secondary_emails(user.username.upcase)).to eq([user]) | ||||
|     end | ||||
| 
 | ||||
|     it 'returns users with a matching whole secondary email' do | ||||
|       expect(described_class.search_with_secondary_emails(email.email)).to eq([email.user]) | ||||
|     end | ||||
| 
 | ||||
|     it 'does not return users with a matching part of secondary email' do | ||||
|       expect(described_class.search_with_secondary_emails(email.email[1...-1])).to be_empty | ||||
|     end | ||||
| 
 | ||||
|     it 'returns no matches for an empty string' do | ||||
|       expect(described_class.search_with_secondary_emails('')).to be_empty | ||||
|     end | ||||
| 
 | ||||
|     it 'returns no matches for nil' do | ||||
|       expect(described_class.search_with_secondary_emails(nil)).to be_empty | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|  |  | |||
|  | @ -2510,6 +2510,98 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'approve pending user' do | ||||
|     shared_examples '404' do | ||||
|       it 'returns 404' do | ||||
|         expect(response).to have_gitlab_http_status(:not_found) | ||||
|         expect(json_response['message']).to eq('404 User Not Found') | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     describe 'POST /users/:id/approve' do | ||||
|       subject(:approve) { post api("/users/#{user_id}/approve", api_user) } | ||||
| 
 | ||||
|       let_it_be(:pending_user) { create(:user, :blocked_pending_approval) } | ||||
|       let_it_be(:deactivated_user) { create(:user, :deactivated) } | ||||
|       let_it_be(:blocked_user) { create(:user, :blocked) } | ||||
| 
 | ||||
|       context 'performed by a non-admin user' do | ||||
|         let(:api_user) { user } | ||||
|         let(:user_id) { pending_user.id } | ||||
| 
 | ||||
|         it 'is not authorized to perform the action' do | ||||
|           expect { approve }.not_to change { pending_user.reload.state } | ||||
|           expect(response).to have_gitlab_http_status(:forbidden) | ||||
|           expect(json_response['message']).to eq('You are not allowed to approve a user') | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'performed by an admin user' do | ||||
|         let(:api_user) { admin } | ||||
| 
 | ||||
|         context 'for a deactivated user' do | ||||
|           let(:user_id) { deactivated_user.id } | ||||
| 
 | ||||
|           it 'does not approve a deactivated user' do | ||||
|             expect { approve }.not_to change { deactivated_user.reload.state } | ||||
|             expect(response).to have_gitlab_http_status(:conflict) | ||||
|             expect(json_response['message']).to eq('The user you are trying to approve is not pending an approval') | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         context 'for an pending approval user' do | ||||
|           let(:user_id) { pending_user.id } | ||||
| 
 | ||||
|           it 'returns 201' do | ||||
|             expect { approve }.to change { pending_user.reload.state }.to('active') | ||||
|             expect(response).to have_gitlab_http_status(:created) | ||||
|             expect(json_response['message']).to eq('Success') | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         context 'for an active user' do | ||||
|           let(:user_id) { user.id } | ||||
| 
 | ||||
|           it 'returns 201' do | ||||
|             expect { approve }.not_to change { user.reload.state } | ||||
|             expect(response).to have_gitlab_http_status(:conflict) | ||||
|             expect(json_response['message']).to eq('The user you are trying to approve is not pending an approval') | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         context 'for a blocked user' do | ||||
|           let(:user_id) { blocked_user.id } | ||||
| 
 | ||||
|           it 'returns 403' do | ||||
|             expect { approve }.not_to change { blocked_user.reload.state } | ||||
|             expect(response).to have_gitlab_http_status(:conflict) | ||||
|             expect(json_response['message']).to eq('The user you are trying to approve is not pending an approval') | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         context 'for a ldap blocked user' do | ||||
|           let(:user_id) { ldap_blocked_user.id } | ||||
| 
 | ||||
|           it 'returns 403' do | ||||
|             expect { approve }.not_to change { ldap_blocked_user.reload.state } | ||||
|             expect(response).to have_gitlab_http_status(:conflict) | ||||
|             expect(json_response['message']).to eq('The user you are trying to approve is not pending an approval') | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         context 'for a user that does not exist' do | ||||
|           let(:user_id) { non_existing_record_id } | ||||
| 
 | ||||
|           before do | ||||
|             approve | ||||
|           end | ||||
| 
 | ||||
|           it_behaves_like '404' | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe 'POST /users/:id/block' do | ||||
|     let(:blocked_user) { create(:user, state: 'blocked') } | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,124 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'fast_spec_helper' | ||||
| require 'rubocop' | ||||
| require_relative '../../../../rubocop/cop/lint/last_keyword_argument' | ||||
| 
 | ||||
| RSpec.describe RuboCop::Cop::Lint::LastKeywordArgument, type: :rubocop do | ||||
|   include CopHelper | ||||
| 
 | ||||
|   subject(:cop) { described_class.new } | ||||
| 
 | ||||
|   before do | ||||
|     described_class.instance_variable_set(:@keyword_warnings, nil) | ||||
|   end | ||||
| 
 | ||||
|   context 'deprecation files does not exist' do | ||||
|     before do | ||||
|       allow(Dir).to receive(:glob).and_return([]) | ||||
|       allow(File).to receive(:exist?).and_return(false) | ||||
|     end | ||||
| 
 | ||||
|     it 'does not register an offense' do | ||||
|       expect_no_offenses(<<~SOURCE) | ||||
|         users.call(params) | ||||
|       SOURCE | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'deprecation files does exist' do | ||||
|     let(:create_spec_yaml) do | ||||
|       <<~YAML | ||||
|       --- | ||||
|       test_mutations/boards/lists/create#resolve_with_proper_permissions_backlog_list_creates_one_and_only_one_backlog: | ||||
|       - | | ||||
|         DEPRECATION WARNING: /Users/tkuah/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/batch-loader-1.4.0/lib/batch_loader/graphql.rb:38: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call | ||||
|         /Users/tkuah/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/batch-loader-1.4.0/lib/batch_loader.rb:26: warning: The called method `batch' is defined here | ||||
|       test_mutations/boards/lists/create#ready?_raises_an_error_if_required_arguments_are_missing: | ||||
|       - | | ||||
|         DEPRECATION WARNING: /Users/tkuah/code/ee-gdk/gitlab/create_service.rb:1: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call | ||||
|         /Users/tkuah/code/ee-gdk/gitlab/user.rb:17: warning: The called method `call' is defined here | ||||
|       YAML | ||||
|     end | ||||
| 
 | ||||
|     let(:projects_spec_yaml) do | ||||
|       <<~YAML | ||||
|       --- | ||||
|       test_api/projects_get_/projects_when_unauthenticated_behaves_like_projects_response_returns_an_array_of_projects: | ||||
|       - | | ||||
|         DEPRECATION WARNING: /Users/tkuah/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/state_machines-activerecord-0.6.0/lib/state_machines/integrations/active_record.rb:511: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call | ||||
|         /Users/tkuah/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/activerecord-6.0.3.3/lib/active_record/suppressor.rb:43: warning: The called method `save' is defined here | ||||
|       - | | ||||
|         DEPRECATION WARNING: /Users/tkuah/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/rack-2.2.3/lib/rack/builder.rb:158: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call | ||||
|         /Users/tkuah/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/grape-1.4.0/lib/grape/middleware/error.rb:30: warning: The called method `initialize' is defined here | ||||
|       YAML | ||||
|     end | ||||
| 
 | ||||
|     before do | ||||
|       allow(Dir).to receive(:glob).and_return(['deprecations/service/create_spec.yml', 'deprecations/api/projects_spec.yml']) | ||||
|       allow(File).to receive(:read).and_return(create_spec_yaml, projects_spec_yaml) | ||||
|     end | ||||
| 
 | ||||
|     it 'registers an offense' do | ||||
|       expect_offense(<<~SOURCE, 'create_service.rb') | ||||
|         users.call(params) | ||||
|                    ^^^^^^ Using the last argument as keyword parameters is deprecated | ||||
|       SOURCE | ||||
| 
 | ||||
|       expect_correction(<<~SOURCE) | ||||
|         users.call(**params) | ||||
|       SOURCE | ||||
|     end | ||||
| 
 | ||||
|     it 'registers an offense and corrects by converting hash to kwarg' do | ||||
|       expect_offense(<<~SOURCE, 'create_service.rb') | ||||
|         users.call(id, { a: :b, c: :d }) | ||||
|                        ^^^^^^^^^^^^^^^^ Using the last argument as keyword parameters is deprecated | ||||
|       SOURCE | ||||
| 
 | ||||
|       expect_correction(<<~SOURCE) | ||||
|         users.call(id, a: :b, c: :d) | ||||
|       SOURCE | ||||
|     end | ||||
| 
 | ||||
|     it 'registers an offense and corrects by converting splat to double splat' do | ||||
|       expect_offense(<<~SOURCE, 'create_service.rb') | ||||
|         users.call(id, *params) | ||||
|                        ^^^^^^^ Using the last argument as keyword parameters is deprecated | ||||
|       SOURCE | ||||
| 
 | ||||
|       expect_correction(<<~SOURCE) | ||||
|         users.call(id, **params) | ||||
|       SOURCE | ||||
|     end | ||||
| 
 | ||||
|     it 'does not register an offense if already a kwarg', :aggregate_failures do | ||||
|       expect_no_offenses(<<~SOURCE, 'create_service.rb') | ||||
|         users.call(**params) | ||||
|       SOURCE | ||||
| 
 | ||||
|       expect_no_offenses(<<~SOURCE, 'create_service.rb') | ||||
|         users.call(id, a: :b, c: :d) | ||||
|       SOURCE | ||||
|     end | ||||
| 
 | ||||
|     it 'does not register an offense if the method name does not match' do | ||||
|       expect_no_offenses(<<~SOURCE, 'create_service.rb') | ||||
|         users.process(params) | ||||
|       SOURCE | ||||
|     end | ||||
| 
 | ||||
|     it 'does not register an offense if the line number does not match' do | ||||
|       expect_no_offenses(<<~SOURCE, 'create_service.rb') | ||||
|         users.process | ||||
|         users.call(params) | ||||
|       SOURCE | ||||
|     end | ||||
| 
 | ||||
|     it 'does not register an offense if the filename does not match' do | ||||
|       expect_no_offenses(<<~SOURCE, 'update_service.rb') | ||||
|         users.call(params) | ||||
|       SOURCE | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -8,6 +8,8 @@ if $".include?(File.expand_path('fast_spec_helper.rb', __dir__)) | |||
|   abort 'Aborting...' | ||||
| end | ||||
| 
 | ||||
| require './spec/deprecation_toolkit_env' | ||||
| 
 | ||||
| require './spec/simplecov_env' | ||||
| SimpleCovEnv.start! | ||||
| 
 | ||||
|  |  | |||
|  | @ -93,13 +93,6 @@ module StubObjectStorage | |||
|   end | ||||
| 
 | ||||
|   def stub_terraform_state_object_storage(**params) | ||||
|     stub_object_storage_uploader(config: Gitlab.config.terraform_state.object_store, | ||||
|                                  uploader: Terraform::VersionedStateUploader, | ||||
|                                  remote_directory: 'terraform', | ||||
|                                  **params) | ||||
|   end | ||||
| 
 | ||||
|   def stub_terraform_state_version_object_storage(**params) | ||||
|     stub_object_storage_uploader(config: Gitlab.config.terraform_state.object_store, | ||||
|                                  uploader: Terraform::StateUploader, | ||||
|                                  remote_directory: 'terraform', | ||||
|  |  | |||
|  | @ -3,23 +3,45 @@ | |||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe Terraform::StateUploader do | ||||
|   subject { terraform_state.file } | ||||
|   subject { state_version.file } | ||||
| 
 | ||||
|   let(:terraform_state) { create(:terraform_state, :with_file) } | ||||
|   let(:state_version) { create(:terraform_state_version) } | ||||
| 
 | ||||
|   before do | ||||
|     stub_terraform_state_object_storage | ||||
|   end | ||||
| 
 | ||||
|   describe '#filename' do | ||||
|     it 'contains the version of the terraform state record' do | ||||
|       expect(subject.filename).to eq("#{state_version.version}.tfstate") | ||||
|     end | ||||
| 
 | ||||
|     context 'legacy state with versioning disabled' do | ||||
|       let(:state) { create(:terraform_state, versioning_enabled: false) } | ||||
|       let(:state_version) { create(:terraform_state_version, terraform_state: state) } | ||||
| 
 | ||||
|       it 'contains the UUID of the terraform state record' do | ||||
|       expect(subject.filename).to include(terraform_state.uuid) | ||||
|         expect(subject.filename).to eq("#{state_version.uuid}.tfstate") | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#store_dir' do | ||||
|     it 'hashes the project ID and UUID' do | ||||
|       expect(Gitlab::HashedPath).to receive(:new) | ||||
|         .with(state_version.uuid, root_hash: state_version.project_id) | ||||
|         .and_return(:store_dir) | ||||
| 
 | ||||
|       expect(subject.store_dir).to eq(:store_dir) | ||||
|     end | ||||
| 
 | ||||
|     context 'legacy state with versioning disabled' do | ||||
|       let(:state) { create(:terraform_state, versioning_enabled: false) } | ||||
|       let(:state_version) { create(:terraform_state_version, terraform_state: state) } | ||||
| 
 | ||||
|       it 'contains the ID of the project' do | ||||
|       expect(subject.store_dir).to include(terraform_state.project_id.to_s) | ||||
|         expect(subject.store_dir).to include(state_version.project_id.to_s) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|  | @ -27,7 +49,7 @@ RSpec.describe Terraform::StateUploader do | |||
|     it 'creates a digest with a secret key and the project id' do | ||||
|       expect(OpenSSL::HMAC) | ||||
|         .to receive(:digest) | ||||
|         .with('SHA256', Gitlab::Application.secrets.db_key_base, terraform_state.project_id.to_s) | ||||
|         .with('SHA256', Gitlab::Application.secrets.db_key_base, state_version.project_id.to_s) | ||||
|         .and_return('digest') | ||||
| 
 | ||||
|       expect(subject.key).to eq('digest') | ||||
|  |  | |||
|  | @ -1,47 +0,0 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe Terraform::VersionedStateUploader do | ||||
|   subject { model.file } | ||||
| 
 | ||||
|   let(:model) { create(:terraform_state_version, :with_file) } | ||||
| 
 | ||||
|   before do | ||||
|     stub_terraform_state_object_storage | ||||
|   end | ||||
| 
 | ||||
|   describe '#filename' do | ||||
|     it 'contains the version of the terraform state record' do | ||||
|       expect(subject.filename).to eq("#{model.version}.tfstate") | ||||
|     end | ||||
| 
 | ||||
|     context 'legacy state with versioning disabled' do | ||||
|       let(:state) { create(:legacy_terraform_state) } | ||||
|       let(:model) { create(:terraform_state_version, terraform_state: state) } | ||||
| 
 | ||||
|       it 'contains the UUID of the terraform state record' do | ||||
|         expect(subject.filename).to eq("#{model.uuid}.tfstate") | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#store_dir' do | ||||
|     it 'hashes the project ID and UUID' do | ||||
|       expect(Gitlab::HashedPath).to receive(:new) | ||||
|         .with(model.uuid, root_hash: model.project_id) | ||||
|         .and_return(:store_dir) | ||||
| 
 | ||||
|       expect(subject.store_dir).to eq(:store_dir) | ||||
|     end | ||||
| 
 | ||||
|     context 'legacy state with versioning disabled' do | ||||
|       let(:state) { create(:legacy_terraform_state) } | ||||
|       let(:model) { create(:terraform_state_version, terraform_state: state) } | ||||
| 
 | ||||
|       it 'contains the ID of the project' do | ||||
|         expect(subject.store_dir).to include(model.project_id.to_s) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
		Loading…
	
		Reference in New Issue