diff --git a/app/assets/javascripts/access_tokens/components/projects_field.vue b/app/assets/javascripts/access_tokens/components/projects_field.vue index e58f74b6ad4..dfe4e07662b 100644 --- a/app/assets/javascripts/access_tokens/components/projects_field.vue +++ b/app/assets/javascripts/access_tokens/components/projects_field.vue @@ -1,11 +1,12 @@ @@ -32,7 +49,8 @@ export default { {{ __('Selected projects') }} - + + diff --git a/app/assets/javascripts/access_tokens/components/projects_token_selector.vue b/app/assets/javascripts/access_tokens/components/projects_token_selector.vue new file mode 100644 index 00000000000..839f32bd488 --- /dev/null +++ b/app/assets/javascripts/access_tokens/components/projects_token_selector.vue @@ -0,0 +1,141 @@ + + + diff --git a/app/assets/javascripts/access_tokens/graphql/queries/get_projects.query.graphql b/app/assets/javascripts/access_tokens/graphql/queries/get_projects.query.graphql new file mode 100644 index 00000000000..40d475cb571 --- /dev/null +++ b/app/assets/javascripts/access_tokens/graphql/queries/get_projects.query.graphql @@ -0,0 +1,22 @@ +#import "~/graphql_shared/fragments/pageInfo.fragment.graphql" + +query getProjects($search: String!, $after: String = "", $first: Int!) { + projects( + search: $search + after: $after + first: $first + membership: true + searchNamespaces: true + sort: "UPDATED_ASC" + ) { + nodes { + id + name + nameWithNamespace + avatarUrl + } + pageInfo { + ...PageInfo + } + } +} diff --git a/app/assets/javascripts/access_tokens/index.js b/app/assets/javascripts/access_tokens/index.js index e29ec5adb42..6ab17092999 100644 --- a/app/assets/javascripts/access_tokens/index.js +++ b/app/assets/javascripts/access_tokens/index.js @@ -1,4 +1,6 @@ import Vue from 'vue'; +import createFlash from '~/flash'; +import { __ } from '~/locale'; import ExpiresAtField from './components/expires_at_field.vue'; @@ -43,17 +45,46 @@ export const initProjectsField = () => { const inputAttrs = getInputAttrs(el); if (window.gon.features.personalAccessTokensScopedToProjects) { - const ProjectsField = () => import('./components/projects_field.vue'); + return new Promise((resolve) => { + Promise.all([ + import('./components/projects_field.vue'), + import('vue-apollo'), + import('~/lib/graphql'), + ]) + .then( + ([ + { default: ProjectsField }, + { default: VueApollo }, + { default: createDefaultClient }, + ]) => { + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); - return new Vue({ - el, - render(h) { - return h(ProjectsField, { - props: { - inputAttrs, + Vue.use(VueApollo); + + resolve( + new Vue({ + el, + apolloProvider, + render(h) { + return h(ProjectsField, { + props: { + inputAttrs, + }, + }); + }, + }), + ); }, + ) + .catch(() => { + createFlash({ + message: __( + 'An error occurred while loading the access tokens form, please try again.', + ), + }); }); - }, }); } diff --git a/app/assets/javascripts/pages/profiles/index.js b/app/assets/javascripts/pages/profiles/index.js index 535fe5fa4eb..80bc32dd43f 100644 --- a/app/assets/javascripts/pages/profiles/index.js +++ b/app/assets/javascripts/pages/profiles/index.js @@ -3,21 +3,19 @@ import '~/profile/gl_crop'; import Profile from '~/profile/profile'; import initSearchSettings from '~/search_settings'; -document.addEventListener('DOMContentLoaded', () => { - // eslint-disable-next-line func-names - $(document).on('input.ssh_key', '#key_key', function () { - const $title = $('#key_title'); - const comment = $(this) - .val() - .match(/^\S+ \S+ (.+)\n?$/); +// eslint-disable-next-line func-names +$(document).on('input.ssh_key', '#key_key', function () { + const $title = $('#key_title'); + const comment = $(this) + .val() + .match(/^\S+ \S+ (.+)\n?$/); - // Extract the SSH Key title from its comment - if (comment && comment.length > 1) { - $title.val(comment[1]).change(); - } - }); - - new Profile(); // eslint-disable-line no-new - - initSearchSettings(); + // Extract the SSH Key title from its comment + if (comment && comment.length > 1) { + $title.val(comment[1]).change(); + } }); + +new Profile(); // eslint-disable-line no-new + +initSearchSettings(); diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 7c6a444ce7a..7f7d38a09c5 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -237,6 +237,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController [ *::ApplicationSettingsHelper.visible_attributes, *::ApplicationSettingsHelper.external_authorization_service_attributes, + *ApplicationSetting.repository_storages_weighted_attributes, *ApplicationSetting.kroki_formats_attributes.keys.map { |key| "kroki_formats_#{key}".to_sym }, :lets_encrypt_notification_email, :lets_encrypt_terms_of_service_accepted, @@ -247,8 +248,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :default_branch_name, disabled_oauth_sign_in_sources: [], import_sources: [], - restricted_visibility_levels: [], - repository_storages_weighted: {} + repository_storages: [], + restricted_visibility_levels: [] ] end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index e3743ef24f2..8b9d42e00e6 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -90,21 +90,13 @@ class ProjectsController < Projects::ApplicationController # Refresh the repo in case anything changed @repository = @project.repository - respond_to do |format| - if result[:status] == :success - flash[:notice] = _("Project '%{project_name}' was successfully updated.") % { project_name: @project.name } - - format.html do - redirect_to(edit_project_path(@project, anchor: 'js-general-project-settings')) - end - else - flash[:alert] = result[:message] - @project.reset - - format.html { render_edit } - end - - format.js + if result[:status] == :success + flash[:notice] = _("Project '%{project_name}' was successfully updated.") % { project_name: @project.name } + redirect_to(edit_project_path(@project, anchor: 'js-general-project-settings')) + else + flash[:alert] = result[:message] + @project.reset + render 'edit' end end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 551e08d1e31..244238e246d 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -37,8 +37,13 @@ module ApplicationSettingsHelper end def storage_weights - Gitlab.config.repositories.storages.keys.each_with_object(OpenStruct.new) do |storage, weights| - weights[storage.to_sym] = @application_setting.repository_storages_weighted[storage] || 0 + ApplicationSetting.repository_storages_weighted_attributes.map do |attribute| + storage = attribute.to_s.delete_prefix('repository_storages_weighted_') + { + name: attribute, + label: storage, + value: @application_setting.repository_storages_weighted[storage] || 0 + } end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 44eb2fefb3f..4959401eb27 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -25,6 +25,10 @@ class ApplicationSetting < ApplicationRecord alias_attribute :instance_group_id, :instance_administrators_group_id alias_attribute :instance_administrators_group, :instance_group + def self.repository_storages_weighted_attributes + @repository_storages_weighted_atributes ||= Gitlab.config.repositories.storages.keys.map { |k| "repository_storages_weighted_#{k}".to_sym }.freeze + end + def self.kroki_formats_attributes { blockdiag: { @@ -40,6 +44,7 @@ class ApplicationSetting < ApplicationRecord end store_accessor :kroki_formats, *ApplicationSetting.kroki_formats_attributes.keys, prefix: true + store_accessor :repository_storages_weighted, *Gitlab.config.repositories.storages.keys, prefix: true # Include here so it can override methods from # `add_authentication_token_field` @@ -498,7 +503,6 @@ class ApplicationSetting < ApplicationRecord inclusion: { in: [true, false], message: _('must be a boolean value') } before_validation :ensure_uuid! - before_validation :coerce_repository_storages_weighted, if: :repository_storages_weighted_changed? before_save :ensure_runners_registration_token before_save :ensure_health_check_access_token @@ -579,6 +583,12 @@ class ApplicationSetting < ApplicationRecord recaptcha_enabled || login_recaptcha_protection_enabled end + repository_storages_weighted_attributes.each do |attribute| + define_method :"#{attribute}=" do |value| + super(value.to_i) + end + end + kroki_formats_attributes.keys.each do |key| define_method :"kroki_formats_#{key}=" do |value| super(::Gitlab::Utils.to_boolean(value)) diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index 731a3b5fed8..08c16930b13 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -298,6 +298,10 @@ module ApplicationSettingImplementation Array(read_attribute(:repository_storages)) end + def repository_storages_weighted + read_attribute(:repository_storages_weighted) + end + def commit_email_hostname super.presence || self.class.default_commit_email_hostname end @@ -329,10 +333,9 @@ module ApplicationSettingImplementation def normalized_repository_storage_weights strong_memoize(:normalized_repository_storage_weights) do - repository_storages_weights = repository_storages_weighted.slice(*Gitlab.config.repositories.storages.keys) - weights_total = repository_storages_weights.values.reduce(:+) + weights_total = repository_storages_weighted.values.reduce(:+) - repository_storages_weights.transform_values do |w| + repository_storages_weighted.transform_values do |w| next w if weights_total == 0 w.to_f / weights_total @@ -470,20 +473,16 @@ module ApplicationSettingImplementation invalid.empty? end - def coerce_repository_storages_weighted - repository_storages_weighted.transform_values!(&:to_i) - end - def check_repository_storages_weighted invalid = repository_storages_weighted.keys - Gitlab.config.repositories.storages.keys - errors.add(:repository_storages_weighted, _("can't include: %{invalid_storages}") % { invalid_storages: invalid.join(", ") }) unless + errors.add(:repository_storages_weighted, "can't include: %{invalid_storages}" % { invalid_storages: invalid.join(", ") }) unless invalid.empty? repository_storages_weighted.each do |key, val| next unless val.present? - errors.add(:repository_storages_weighted, _("value for '%{storage}' must be an integer") % { storage: key }) unless val.is_a?(Integer) - errors.add(:repository_storages_weighted, _("value for '%{storage}' must be between 0 and 100") % { storage: key }) unless val.between?(0, 100) + errors.add(:"repository_storages_weighted_#{key}", "value must be an integer") unless val.is_a?(Integer) + errors.add(:"repository_storages_weighted_#{key}", "value must be between 0 and 100") unless val.between?(0, 100) end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 07092e50680..349297c776c 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -191,8 +191,12 @@ class MergeRequest < ApplicationRecord end state_machine :merge_status, initial: :unchecked do + event :mark_as_preparing do + transition unchecked: :preparing + end + event :mark_as_unchecked do - transition [:can_be_merged, :checking, :unchecked] => :unchecked + transition [:preparing, :can_be_merged, :checking, :unchecked] => :unchecked transition [:cannot_be_merged, :cannot_be_merged_rechecking, :cannot_be_merged_recheck] => :cannot_be_merged_recheck end @@ -237,7 +241,7 @@ class MergeRequest < ApplicationRecord # Returns current merge_status except it returns `cannot_be_merged_rechecking` as `checking` # to avoid exposing unnecessary internal state def public_merge_status - cannot_be_merged_rechecking? ? 'checking' : merge_status + cannot_be_merged_rechecking? || preparing? ? 'checking' : merge_status end validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_or_merged_without_fork?] @@ -1054,6 +1058,8 @@ class MergeRequest < ApplicationRecord end def mergeable?(skip_ci_check: false, skip_discussions_check: false) + return false if preparing? + return false unless mergeable_state?(skip_ci_check: skip_ci_check, skip_discussions_check: skip_discussions_check) diff --git a/app/services/merge_requests/after_create_service.rb b/app/services/merge_requests/after_create_service.rb index 03fcb5a4c1b..9bbb47b8058 100644 --- a/app/services/merge_requests/after_create_service.rb +++ b/app/services/merge_requests/after_create_service.rb @@ -3,6 +3,13 @@ module MergeRequests class AfterCreateService < MergeRequests::BaseService def execute(merge_request) + prepare_merge_request(merge_request) + merge_request.mark_as_unchecked! if merge_request.preparing? + end + + private + + def prepare_merge_request(merge_request) event_service.open_mr(merge_request, current_user) merge_request_activity_counter.track_create_mr_action(user: current_user) notification_service.new_merge_request(merge_request, current_user) diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb index ac84a13f437..f4d14aaf7d1 100644 --- a/app/services/merge_requests/create_service.rb +++ b/app/services/merge_requests/create_service.rb @@ -14,6 +14,8 @@ module MergeRequests end def after_create(issuable) + issuable.mark_as_preparing + # Add new items to MergeRequests::AfterCreateService if they can # be performed in Sidekiq NewMergeRequestWorker.perform_async(issuable.id, current_user.id) diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml index 437189cf47f..0862d1bf0b6 100644 --- a/app/views/admin/application_settings/_repository_storage.html.haml +++ b/app/views/admin/application_settings/_repository_storage.html.haml @@ -18,9 +18,8 @@ = _('Enter weights for storages for new repositories.') = link_to sprite_icon('question-o'), help_page_path('administration/repository_storage_paths') .form-check - = f.fields_for :repository_storages_weighted, storage_weights do |storage_form| - - Gitlab.config.repositories.storages.keys.each do |storage| - = storage_form.text_field storage, class: 'form-text-input' - = storage_form.label storage, storage, class: 'label-bold form-check-label' - %br + - storage_weights.each do |attribute| + = f.text_field attribute[:name], class: 'form-text-input', value: attribute[:value] + = f.label attribute[:label], attribute[:label], class: 'label-bold form-check-label' + %br = f.submit _('Save changes'), class: "gl-button btn btn-success qa-save-changes-button" diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 2922ba4fb6e..9388c5fad6d 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -19,7 +19,7 @@ %p= _('Choose visibility level, enable/disable project features and their permissions, disable email notifications, and show default award emoji.') .settings-content - = form_for @project, remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f| + = form_for @project, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f| %input{ name: 'update_section', type: 'hidden', value: 'js-shared-permissions' } %template.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data_json(@project) .js-project-permissions-form @@ -36,7 +36,7 @@ .settings-content = render_if_exists 'shared/promotions/promote_mr_features' - = form_for @project, remote: true, html: { multipart: true, class: "merge-request-settings-form js-mr-settings-form" }, authenticity_token: true do |f| + = form_for @project, html: { multipart: true, class: "merge-request-settings-form js-mr-settings-form" }, authenticity_token: true do |f| %input{ name: 'update_section', type: 'hidden', value: 'js-merge-request-settings' } = render 'projects/merge_request_settings', form: f = f.submit _('Save changes'), class: "btn gl-button btn-success rspec-save-merge-request-changes", data: { qa_selector: 'save_merge_request_changes_button' } diff --git a/app/views/projects/settings/_general.html.haml b/app/views/projects/settings/_general.html.haml index bd365123b81..d1a95886115 100644 --- a/app/views/projects/settings/_general.html.haml +++ b/app/views/projects/settings/_general.html.haml @@ -1,6 +1,5 @@ -= form_for [@project], remote: true, html: { multipart: true, class: "edit-project js-general-settings-form" }, authenticity_token: true do |f| += form_for [@project], html: { multipart: true, class: "edit-project js-general-settings-form" }, authenticity_token: true do |f| %input{ name: 'update_section', type: 'hidden', value: 'js-general-settings' } - = form_errors(@project) %fieldset .row diff --git a/app/views/projects/update.js.haml b/app/views/projects/update.js.haml deleted file mode 100644 index c5eecc900b2..00000000000 --- a/app/views/projects/update.js.haml +++ /dev/null @@ -1,10 +0,0 @@ -- if @project.valid? - :plain - location.href = "#{edit_project_path(@project, anchor: params[:update_section])}"; - location.reload(); -- else - :plain - $(".flash-container").html("#{escape_javascript(render('errors'))}"); - $('.save-project-loader').hide(); - $('.project-edit-container').show(); - $('.edit-project .js-btn-success-general-project-settings').enable(); diff --git a/changelogs/unreleased/291012-preparing-mr-state.yml b/changelogs/unreleased/291012-preparing-mr-state.yml new file mode 100644 index 00000000000..d52200f6697 --- /dev/null +++ b/changelogs/unreleased/291012-preparing-mr-state.yml @@ -0,0 +1,5 @@ +--- +title: Implement new preparing internal merge_status +merge_request: 54900 +author: +type: other diff --git a/changelogs/unreleased/bump_auto_deploy_image_version.yml b/changelogs/unreleased/bump_auto_deploy_image_version.yml new file mode 100644 index 00000000000..533f4072cd5 --- /dev/null +++ b/changelogs/unreleased/bump_auto_deploy_image_version.yml @@ -0,0 +1,4 @@ +title: Bump auto-deploy-image tag in Deploy.latest.gitlab-ci.yml to v2.6.0, which includes changes to ciliumnetworkpolicies. +merge_request: 54983 +author: +type: added diff --git a/changelogs/unreleased/fix_repo_storage_weights_admin.yml b/changelogs/unreleased/fix_repo_storage_weights_admin.yml deleted file mode 100644 index c82ef578e01..00000000000 --- a/changelogs/unreleased/fix_repo_storage_weights_admin.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Allow saving repository weights after a storage has been removed -merge_request: 53803 -author: -type: fixed diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md index b2f15c22429..ad16f3201e2 100644 --- a/doc/development/testing_guide/best_practices.md +++ b/doc/development/testing_guide/best_practices.md @@ -986,6 +986,7 @@ GitLab uses [factory_bot](https://github.com/thoughtbot/factory_bot) as a test f See [issue #262624](https://gitlab.com/gitlab-org/gitlab/-/issues/262624) for further context. - Factories don't have to be limited to `ActiveRecord` objects. [See example](https://gitlab.com/gitlab-org/gitlab-foss/commit/0b8cefd3b2385a21cfed779bd659978c0402766d). +- Factories and their traits should produce valid objects that are [verified by specs](https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/factories_spec.rb). ### Fixtures diff --git a/doc/user/group/index.md b/doc/user/group/index.md index e0361f6033c..52d42584407 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -720,8 +720,8 @@ To enable prevent project forking: access each project's settings, and remove any project, all from the same screen. - **Webhooks**: Configure [webhooks](../project/integrations/webhooks.md) for your group. - **Kubernetes cluster integration**: Connect your GitLab group with [Kubernetes clusters](clusters/index.md). -- **Audit Events**: View [Audit Events](../../administration/audit_events.md) - for the group. **(PREMIUM SELF)** +- **Audit Events**: View [Audit Events](../../administration/audit_events.md#group-events) + for the group. - **Pipelines quota**: Keep track of the [pipeline quota](../admin_area/settings/continuous_integration.md) for the group. - **Integrations**: Configure [integrations](../admin_area/settings/project_integration_management.md) for your group. diff --git a/doc/user/project/clusters/add_eks_clusters.md b/doc/user/project/clusters/add_eks_clusters.md index 141e8267d75..e329ec4f903 100644 --- a/doc/user/project/clusters/add_eks_clusters.md +++ b/doc/user/project/clusters/add_eks_clusters.md @@ -140,13 +140,11 @@ To create and add a new Kubernetes cluster to your project, group, or instance: 1. Click **Review policy**. 1. Enter a suitable name for this policy, and click **Create Policy**. You can now close this window. -1. In the [IAM Management Console](https://console.aws.amazon.com/iam/home), create an EKS management IAM role. - To do so, follow the [Amazon EKS cluster IAM role](https://docs.aws.amazon.com/eks/latest/userguide/service_IAM_role.html) instructions - to create a IAM role suitable for managing the AWS EKS cluster's resources on your behalf. +1. In the [IAM Management Console](https://console.aws.amazon.com/iam/home), create an **EKS IAM role** following the [Amazon EKS cluster IAM role instructions](https://docs.aws.amazon.com/eks/latest/userguide/service_IAM_role.html). This role should exist so that Kubernetes clusters managed by Amazon EKS can make calls to other AWS services on your behalf to manage the resources that you use with the service. In addition to the policies that guide suggests, you must also include the `AmazonEKSClusterPolicy` policy for this role in order for GitLab to manage the EKS cluster correctly. -1. In the [IAM Management Console](https://console.aws.amazon.com/iam/home), create an IAM role: - 1. From the left panel, select **Roles**. +1. In the [IAM Management Console](https://console.aws.amazon.com/iam/home), create another IAM role which will be used by GitLab to authenticate with AWS. Follow these steps to create it: + 1. On the AWS IAM console, select **Roles** from the left panel. 1. Click **Create role**. 1. Under `Select type of trusted entity`, select **Another AWS account**. 1. Enter the Account ID from GitLab into the `Account ID` field. diff --git a/lib/bitbucket/collection.rb b/lib/bitbucket/collection.rb index 9c496daccaa..a6a31de3ce6 100644 --- a/lib/bitbucket/collection.rb +++ b/lib/bitbucket/collection.rb @@ -11,13 +11,5 @@ module Bitbucket lazy end - - def method_missing(method, *args) - return super unless self.respond_to?(method) - - self.__send__(method, *args) do |item| # rubocop:disable GitlabSecurity/PublicSend - block_given? ? yield(item) : item - end - end end end diff --git a/lib/bitbucket_server/collection.rb b/lib/bitbucket_server/collection.rb index f549acbd87f..2880f587202 100644 --- a/lib/bitbucket_server/collection.rb +++ b/lib/bitbucket_server/collection.rb @@ -35,13 +35,5 @@ module BitbucketServer current_page + 1 end - - def method_missing(method, *args) - return super unless self.respond_to?(method) - - self.__send__(method, *args) do |item| # rubocop:disable GitlabSecurity/PublicSend - block_given? ? yield(item) : item - end - end end end diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml index abf6bf1cb05..7b897bfe7c1 100644 --- a/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml @@ -1,5 +1,5 @@ .auto-deploy: - image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v2.0.0" + image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v2.6.0" dependencies: [] review: diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 46fd7065c11..c030018c6c2 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3467,6 +3467,9 @@ msgstr "" msgid "An error occurred while loading project creation UI" msgstr "" +msgid "An error occurred while loading the access tokens form, please try again." +msgstr "" + msgid "An error occurred while loading the data. Please try again." msgstr "" @@ -34923,9 +34926,6 @@ msgstr "" msgid "can't be enabled because signed commits are required for this project" msgstr "" -msgid "can't include: %{invalid_storages}" -msgstr "" - msgid "cannot be a date in the past" msgstr "" @@ -36332,12 +36332,6 @@ msgstr "" msgid "v%{version} published %{timeAgo}" msgstr "" -msgid "value for '%{storage}' must be an integer" -msgstr "" - -msgid "value for '%{storage}' must be between 0 and 100" -msgstr "" - msgid "verify ownership" msgstr "" diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb index 2b562e2dd64..71abf3191b8 100644 --- a/spec/controllers/admin/application_settings_controller_spec.rb +++ b/spec/controllers/admin/application_settings_controller_spec.rb @@ -144,10 +144,10 @@ RSpec.describe Admin::ApplicationSettingsController do end it 'updates repository_storages_weighted setting' do - put :update, params: { application_setting: { repository_storages_weighted: { default: 75 } } } + put :update, params: { application_setting: { repository_storages_weighted_default: 75 } } expect(response).to redirect_to(general_admin_application_settings_path) - expect(ApplicationSetting.current.repository_storages_weighted).to eq('default' => 75) + expect(ApplicationSetting.current.repository_storages_weighted_default).to eq(75) end it 'updates kroki_formats setting' do diff --git a/spec/factories_spec.rb b/spec/factories_spec.rb index 4081ef7b2ce..edce92d1948 100644 --- a/spec/factories_spec.rb +++ b/spec/factories_spec.rb @@ -5,6 +5,10 @@ require 'spec_helper' RSpec.describe 'factories' do include Database::DatabaseHelpers + # https://gitlab.com/groups/gitlab-org/-/epics/5464 tracks the remaining + # skipped traits. + # + # Consider adding a code comment if a trait cannot produce a valid object. def skipped_traits [ [:audit_event, :unauthenticated], diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index 249621f5835..52f39f65bd0 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -384,20 +384,7 @@ RSpec.describe 'Admin updates settings' do click_button 'Save changes' end - expect(current_settings.repository_storages_weighted).to eq('default' => 50) - end - - it 'still saves when settings are outdated' do - current_settings.update_attribute :repository_storages_weighted, { 'default' => 100, 'outdated' => 100 } - - visit repository_admin_application_settings_path - - page.within('.as-repository-storage') do - fill_in 'application_setting_repository_storages_weighted_default', with: 50 - click_button 'Save changes' - end - - expect(current_settings.repository_storages_weighted).to eq('default' => 50) + expect(current_settings.repository_storages_weighted_default).to be 50 end end diff --git a/spec/frontend/access_tokens/components/projects_field_spec.js b/spec/frontend/access_tokens/components/projects_field_spec.js index 7e9f06b9022..75b85fff8d7 100644 --- a/spec/frontend/access_tokens/components/projects_field_spec.js +++ b/spec/frontend/access_tokens/components/projects_field_spec.js @@ -1,6 +1,7 @@ -import { within } from '@testing-library/dom'; +import { within, fireEvent } from '@testing-library/dom'; import { mount } from '@vue/test-utils'; import ProjectsField from '~/access_tokens/components/projects_field.vue'; +import ProjectsTokenSelector from '~/access_tokens/components/projects_token_selector.vue'; describe('ProjectsField', () => { let wrapper; @@ -18,6 +19,10 @@ describe('ProjectsField', () => { const queryByLabelText = (text) => within(wrapper.element).queryByLabelText(text); const queryByText = (text) => within(wrapper.element).queryByText(text); + const findAllProjectsRadio = () => queryByLabelText('All projects'); + const findSelectedProjectsRadio = () => queryByLabelText('Selected projects'); + const findProjectsTokenSelector = () => wrapper.findComponent(ProjectsTokenSelector); + const findHiddenInput = () => wrapper.find('input[type="hidden"]'); beforeEach(() => { createComponent(); @@ -34,25 +39,66 @@ describe('ProjectsField', () => { }); it('renders "All projects" radio selected by default', () => { - const allProjectsRadio = queryByLabelText('All projects'); + const allProjectsRadio = findAllProjectsRadio(); expect(allProjectsRadio).not.toBe(null); expect(allProjectsRadio.checked).toBe(true); }); it('renders "Selected projects" radio unchecked by default', () => { - const selectedProjectsRadio = queryByLabelText('Selected projects'); + const selectedProjectsRadio = findSelectedProjectsRadio(); expect(selectedProjectsRadio).not.toBe(null); expect(selectedProjectsRadio.checked).toBe(false); }); + it('renders `projects-token-selector` component', () => { + expect(findProjectsTokenSelector().exists()).toBe(true); + }); + it('renders hidden input with correct `name` and `id` attributes', () => { - expect(wrapper.find('input[type="hidden"]').attributes()).toEqual( + expect(findHiddenInput().attributes()).toEqual( expect.objectContaining({ id: 'projects', name: 'projects', }), ); }); + + describe('when `projects-token-selector` is focused', () => { + beforeEach(() => { + findProjectsTokenSelector().vm.$emit('focus'); + }); + + it('auto selects the "Selected projects" radio', () => { + expect(findSelectedProjectsRadio().checked).toBe(true); + }); + + describe('when `projects-token-selector` is changed', () => { + beforeEach(() => { + findProjectsTokenSelector().vm.$emit('input', [ + { + id: 1, + }, + { + id: 2, + }, + ]); + }); + + it('updates the hidden input value to a comma separated list of project IDs', () => { + expect(findHiddenInput().attributes('value')).toBe('1,2'); + }); + + describe('when radio is changed back to "All projects"', () => { + beforeEach(() => { + fireEvent.click(findAllProjectsRadio()); + }); + + it('removes the hidden input value', () => { + expect(findHiddenInput().attributes('value')).toBe(''); + }); + }); + }); + }); }); diff --git a/spec/frontend/access_tokens/components/projects_token_selector_spec.js b/spec/frontend/access_tokens/components/projects_token_selector_spec.js new file mode 100644 index 00000000000..712bba9e0ca --- /dev/null +++ b/spec/frontend/access_tokens/components/projects_token_selector_spec.js @@ -0,0 +1,224 @@ +import { + GlAvatar, + GlAvatarLabeled, + GlIntersectionObserver, + GlToken, + GlTokenSelector, + GlLoadingIcon, +} from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import produce from 'immer'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; + +import { getJSONFixture } from 'helpers/fixtures'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import ProjectsTokenSelector from '~/access_tokens/components/projects_token_selector.vue'; +import getProjectsQuery from '~/access_tokens/graphql/queries/get_projects.query.graphql'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; + +describe('ProjectsTokenSelector', () => { + const getProjectsQueryResponse = getJSONFixture( + 'graphql/projects/access_tokens/get_projects.query.graphql.json', + ); + const getProjectsQueryResponsePage2 = produce( + getProjectsQueryResponse, + (getProjectsQueryResponseDraft) => { + /* eslint-disable no-param-reassign */ + getProjectsQueryResponseDraft.data.projects.pageInfo.hasNextPage = false; + getProjectsQueryResponseDraft.data.projects.pageInfo.endCursor = null; + getProjectsQueryResponseDraft.data.projects.nodes.splice(1, 1); + getProjectsQueryResponseDraft.data.projects.nodes[0].id = 'gid://gitlab/Project/100'; + /* eslint-enable no-param-reassign */ + }, + ); + + const runDebounce = () => jest.runAllTimers(); + + const { pageInfo, nodes: projects } = getProjectsQueryResponse.data.projects; + const project1 = projects[0]; + const project2 = projects[1]; + + let wrapper; + + let resolveGetProjectsQuery; + const getProjectsQueryRequestHandler = jest.fn( + () => + new Promise((resolve) => { + resolveGetProjectsQuery = resolve; + }), + ); + + const createComponent = ({ + propsData = {}, + apolloProvider = createMockApollo([[getProjectsQuery, getProjectsQueryRequestHandler]]), + resolveQueries = true, + } = {}) => { + Vue.use(VueApollo); + + wrapper = extendedWrapper( + mount(ProjectsTokenSelector, { + apolloProvider, + propsData: { + selectedProjects: [], + ...propsData, + }, + stubs: ['gl-intersection-observer'], + }), + ); + + runDebounce(); + + if (resolveQueries) { + resolveGetProjectsQuery(getProjectsQueryResponse); + + return waitForPromises(); + } + + return Promise.resolve(); + }; + + const findTokenSelector = () => wrapper.findComponent(GlTokenSelector); + const findTokenSelectorInput = () => findTokenSelector().find('input[type="text"]'); + const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver); + + it('renders dropdown items with project avatars', async () => { + await createComponent(); + + wrapper.findAllComponents(GlAvatarLabeled).wrappers.forEach((avatarLabeledWrapper, index) => { + const project = projects[index]; + + expect(avatarLabeledWrapper.attributes()).toEqual( + expect.objectContaining({ + 'entity-id': `${getIdFromGraphQLId(project.id)}`, + 'entity-name': project.name, + ...(project.avatarUrl && { src: project.avatarUrl }), + }), + ); + + expect(avatarLabeledWrapper.props()).toEqual( + expect.objectContaining({ + label: project.name, + subLabel: project.nameWithNamespace, + }), + ); + }); + }); + + it('renders tokens with project avatars', () => { + createComponent({ + propsData: { + selectedProjects: [{ ...project2, id: getIdFromGraphQLId(project2.id) }], + }, + }); + + const token = wrapper.findComponent(GlToken); + const avatar = token.findComponent(GlAvatar); + + expect(token.text()).toContain(project2.nameWithNamespace); + expect(avatar.attributes('src')).toBe(project2.avatarUrl); + expect(avatar.props()).toEqual( + expect.objectContaining({ + entityId: getIdFromGraphQLId(project2.id), + entityName: project2.name, + }), + ); + }); + + describe('when `enter` key is pressed', () => { + it('calls `preventDefault` so form is not submitted when user selects a project from the dropdown', () => { + createComponent(); + + const event = { + preventDefault: jest.fn(), + }; + + findTokenSelectorInput().trigger('keydown.enter', event); + + expect(event.preventDefault).toHaveBeenCalled(); + }); + }); + + describe('when text input is typed in', () => { + const searchTerm = 'foo bar'; + + beforeEach(async () => { + await createComponent(); + + await findTokenSelectorInput().setValue(searchTerm); + runDebounce(); + }); + + it('makes GraphQL request with `search` variable set', async () => { + expect(getProjectsQueryRequestHandler).toHaveBeenLastCalledWith({ + search: searchTerm, + after: null, + first: 20, + }); + }); + + it('sets loading state while waiting for GraphQL request to resolve', async () => { + expect(findTokenSelector().props('loading')).toBe(true); + + resolveGetProjectsQuery(getProjectsQueryResponse); + await waitForPromises(); + + expect(findTokenSelector().props('loading')).toBe(false); + }); + }); + + describe('when there is a next page of projects and user scrolls to the bottom of the dropdown', () => { + beforeEach(async () => { + await createComponent(); + + findIntersectionObserver().vm.$emit('appear'); + }); + + it('makes GraphQL request with `after` variable set', async () => { + expect(getProjectsQueryRequestHandler).toHaveBeenLastCalledWith({ + after: pageInfo.endCursor, + first: 20, + search: '', + }); + }); + + it('displays loading icon while waiting for GraphQL request to resolve', async () => { + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + + resolveGetProjectsQuery(getProjectsQueryResponsePage2); + await waitForPromises(); + + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); + }); + }); + + describe('when there is not a next page of projects', () => { + it('does not render `GlIntersectionObserver`', async () => { + createComponent({ resolveQueries: false }); + + resolveGetProjectsQuery(getProjectsQueryResponsePage2); + await waitForPromises(); + + expect(findIntersectionObserver().exists()).toBe(false); + }); + }); + + describe('when `GlTokenSelector` emits `input` event', () => { + it('emits `input` event used by `v-model`', () => { + findTokenSelector().vm.$emit('input', project1); + + expect(wrapper.emitted('input')[0]).toEqual([project1]); + }); + }); + + describe('when `GlTokenSelector` emits `focus` event', () => { + it('emits `focus` event', () => { + const event = { fakeEvent: 'foo' }; + findTokenSelector().vm.$emit('focus', event); + + expect(wrapper.emitted('focus')[0]).toEqual([event]); + }); + }); +}); diff --git a/spec/frontend/access_tokens/index_spec.js b/spec/frontend/access_tokens/index_spec.js index 2225e23e09c..7aeca263f21 100644 --- a/spec/frontend/access_tokens/index_spec.js +++ b/spec/frontend/access_tokens/index_spec.js @@ -1,19 +1,27 @@ import { createWrapper } from '@vue/test-utils'; - -import waitForPromises from 'helpers/wait_for_promises'; +import Vue from 'vue'; import { initExpiresAtField, initProjectsField } from '~/access_tokens'; -import ExpiresAtField from '~/access_tokens/components/expires_at_field.vue'; -import ProjectsField from '~/access_tokens/components/projects_field.vue'; +import * as ExpiresAtField from '~/access_tokens/components/expires_at_field.vue'; +import * as ProjectsField from '~/access_tokens/components/projects_field.vue'; describe('access tokens', () => { + const FakeComponent = Vue.component('FakeComponent', { + props: { + inputAttrs: { + type: Object, + required: true, + }, + }, + render: () => null, + }); + beforeEach(() => { window.gon = { features: { personalAccessTokensScopedToProjects: true } }; }); afterEach(() => { document.body.innerHTML = ''; - window.gon = {}; }); describe.each` @@ -34,15 +42,17 @@ describe('access tokens', () => { mountEl.appendChild(input); document.body.appendChild(mountEl); + + // Mock component so we don't have to deal with mocking Apollo + // eslint-disable-next-line no-param-reassign + expectedComponent.default = FakeComponent; }); - it(`mounts component and sets \`inputAttrs\` prop`, async () => { - const wrapper = createWrapper(initFunction()); + it('mounts component and sets `inputAttrs` prop', async () => { + const vueInstance = await initFunction(); - // Wait for dynamic imports to resolve - await waitForPromises(); - - const component = wrapper.findComponent(expectedComponent); + const wrapper = createWrapper(vueInstance); + const component = wrapper.findComponent(FakeComponent); expect(component.exists()).toBe(true); expect(component.props('inputAttrs')).toEqual({ diff --git a/spec/frontend/fixtures/projects.rb b/spec/frontend/fixtures/projects.rb index aa2f7dbed36..778ae218160 100644 --- a/spec/frontend/fixtures/projects.rb +++ b/spec/frontend/fixtures/projects.rb @@ -3,13 +3,14 @@ require 'spec_helper' RSpec.describe 'Projects (JavaScript fixtures)', type: :controller do + include ApiHelpers include JavaScriptFixturesHelpers runners_token = 'runnerstoken:intabulasreferre' let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} - let(:project) { create(:project, namespace: namespace, path: 'builds-project', runners_token: runners_token) } - let(:project_with_repo) { create(:project, :repository, description: 'Code and stuff') } + let(:project) { create(:project, namespace: namespace, path: 'builds-project', runners_token: runners_token, avatar: fixture_file_upload('spec/fixtures/dk.png', 'image/png')) } + let(:project_with_repo) { create(:project, :repository, description: 'Code and stuff', avatar: fixture_file_upload('spec/fixtures/dk.png', 'image/png')) } let(:project_variable_populated) { create(:project, namespace: namespace, path: 'builds-project2', runners_token: runners_token) } let(:user) { project.owner } @@ -22,7 +23,6 @@ RSpec.describe 'Projects (JavaScript fixtures)', type: :controller do before do project_with_repo.add_maintainer(user) sign_in(user) - allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon') end after do @@ -48,4 +48,31 @@ RSpec.describe 'Projects (JavaScript fixtures)', type: :controller do expect(response).to be_successful end end + + describe GraphQL::Query, type: :request do + include GraphqlHelpers + + context 'access token projects query' do + before do + project_variable_populated.add_maintainer(user) + end + + before(:all) do + clean_frontend_fixtures('graphql/projects/access_tokens') + end + + fragment_paths = ['graphql_shared/fragments/pageInfo.fragment.graphql'] + base_input_path = 'access_tokens/graphql/queries/' + base_output_path = 'graphql/projects/access_tokens/' + query_name = 'get_projects.query.graphql' + + it "#{base_output_path}#{query_name}.json" do + query = get_graphql_query_as_string("#{base_input_path}#{query_name}", fragment_paths) + + post_graphql(query, current_user: user, variables: { search: '', first: 2 }) + + expect_graphql_errors_to_be_empty + end + end + end end diff --git a/spec/helpers/application_settings_helper_spec.rb b/spec/helpers/application_settings_helper_spec.rb index c74ee3ce0ec..2cd01451e0d 100644 --- a/spec/helpers/application_settings_helper_spec.rb +++ b/spec/helpers/application_settings_helper_spec.rb @@ -130,15 +130,20 @@ RSpec.describe ApplicationSettingsHelper do before do helper.instance_variable_set(:@application_setting, application_setting) stub_storage_settings({ 'default': {}, 'storage_1': {}, 'storage_2': {} }) + allow(ApplicationSetting).to receive(:repository_storages_weighted_attributes).and_return( + [:repository_storages_weighted_default, + :repository_storages_weighted_storage_1, + :repository_storages_weighted_storage_2]) + stub_application_setting(repository_storages_weighted: { 'default' => 100, 'storage_1' => 50, 'storage_2' => nil }) end it 'returns storages correctly' do - expect(helper.storage_weights).to eq(OpenStruct.new( - default: 100, - storage_1: 50, - storage_2: 0 - )) + expect(helper.storage_weights).to eq([ + { name: :repository_storages_weighted_default, label: 'default', value: 100 }, + { name: :repository_storages_weighted_storage_1, label: 'storage_1', value: 50 }, + { name: :repository_storages_weighted_storage_2, label: 'storage_2', value: 0 } + ]) end end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 808932ce7e4..22bdaabce23 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -105,14 +105,14 @@ RSpec.describe ApplicationSetting do it { is_expected.not_to allow_value(false).for(:hashed_storage_enabled) } - it { is_expected.to allow_value('default' => 0).for(:repository_storages_weighted) } - it { is_expected.to allow_value('default' => 50).for(:repository_storages_weighted) } - it { is_expected.to allow_value('default' => 100).for(:repository_storages_weighted) } - it { is_expected.to allow_value('default' => '90').for(:repository_storages_weighted) } - it { is_expected.to allow_value('default' => nil).for(:repository_storages_weighted) } - it { is_expected.not_to allow_value('default' => -1).for(:repository_storages_weighted).with_message("value for 'default' must be between 0 and 100") } - it { is_expected.not_to allow_value('default' => 101).for(:repository_storages_weighted).with_message("value for 'default' must be between 0 and 100") } - it { is_expected.not_to allow_value('default' => 100, shouldntexist: 50).for(:repository_storages_weighted).with_message("can't include: shouldntexist") } + it { is_expected.not_to allow_value(101).for(:repository_storages_weighted_default) } + it { is_expected.to allow_value('90').for(:repository_storages_weighted_default) } + it { is_expected.not_to allow_value(-1).for(:repository_storages_weighted_default) } + it { is_expected.to allow_value(100).for(:repository_storages_weighted_default) } + it { is_expected.to allow_value(0).for(:repository_storages_weighted_default) } + it { is_expected.to allow_value(50).for(:repository_storages_weighted_default) } + it { is_expected.to allow_value(nil).for(:repository_storages_weighted_default) } + it { is_expected.not_to allow_value({ default: 100, shouldntexist: 50 }).for(:repository_storages_weighted) } it { is_expected.to allow_value(400).for(:notes_create_limit) } it { is_expected.not_to allow_value('two').for(:notes_create_limit) } @@ -984,6 +984,12 @@ RSpec.describe ApplicationSetting do it_behaves_like 'application settings examples' + describe 'repository_storages_weighted_attributes' do + it 'returns the keys for repository_storages_weighted' do + expect(subject.class.repository_storages_weighted_attributes).to eq([:repository_storages_weighted_default]) + end + end + describe 'kroki_format_supported?' do it 'returns true when Excalidraw is enabled' do subject.kroki_formats_excalidraw = true @@ -1027,4 +1033,11 @@ RSpec.describe ApplicationSetting do expect(subject.kroki_formats_excalidraw).to eq(true) end end + + it 'does not allow to set weight for non existing storage' do + setting.repository_storages_weighted = { invalid_storage: 100 } + + expect(setting).not_to be_valid + expect(setting.errors.messages[:repository_storages_weighted]).to match_array(["can't include: invalid_storage"]) + end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 2157214be23..176667c9361 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -2885,6 +2885,11 @@ RSpec.describe MergeRequest, factory_default: :keep do describe '#mergeable?' do subject { build_stubbed(:merge_request) } + it 'returns false if still preparing' do + expect(subject).to receive(:preparing?) { true } + expect(subject.mergeable?).to be_falsey + end + it 'returns false if #mergeable_state? is false' do expect(subject).to receive(:mergeable_state?) { false } @@ -3075,6 +3080,7 @@ RSpec.describe MergeRequest, factory_default: :keep do subject { build(:merge_request, merge_status: status) } where(:status, :public_status) do + 'preparing' | 'checking' 'cannot_be_merged_rechecking' | 'checking' 'checking' | 'checking' 'cannot_be_merged' | 'cannot_be_merged' diff --git a/spec/services/merge_requests/after_create_service_spec.rb b/spec/services/merge_requests/after_create_service_spec.rb index f21feb70bc5..22af346634f 100644 --- a/spec/services/merge_requests/after_create_service_spec.rb +++ b/spec/services/merge_requests/after_create_service_spec.rb @@ -67,5 +67,27 @@ RSpec.describe MergeRequests::AfterCreateService do it_behaves_like 'records an onboarding progress action', :merge_request_created do let(:namespace) { merge_request.target_project.namespace } end + + context 'when merge request is in unchecked state' do + before do + merge_request.mark_as_unchecked! + execute_service + end + + it 'does not change its state' do + expect(merge_request.reload).to be_unchecked + end + end + + context 'when merge request is in preparing state' do + before do + merge_request.mark_as_preparing! + execute_service + end + + it 'marks the merge request as unchecked' do + expect(merge_request.reload).to be_unchecked + end + end end end diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index 4f47a22b07c..e63e678bdd8 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -67,6 +67,10 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do expect(Event.where(attributes).count).to eq(1) end + it 'sets the merge_status to preparing' do + expect(merge_request.reload).to be_preparing + end + describe 'when marked with /wip' do context 'in title and in description' do let(:opts) do diff --git a/spec/support/shared_examples/models/application_setting_shared_examples.rb b/spec/support/shared_examples/models/application_setting_shared_examples.rb index 60a02d85a1e..92fd4363134 100644 --- a/spec/support/shared_examples/models/application_setting_shared_examples.rb +++ b/spec/support/shared_examples/models/application_setting_shared_examples.rb @@ -289,7 +289,6 @@ RSpec.shared_examples 'application settings examples' do describe '#pick_repository_storage' do before do - allow(Gitlab.config.repositories.storages).to receive(:keys).and_return(%w(default backup)) allow(setting).to receive(:repository_storages_weighted).and_return({ 'default' => 20, 'backup' => 80 }) end @@ -305,19 +304,15 @@ RSpec.shared_examples 'application settings examples' do describe '#normalized_repository_storage_weights' do using RSpec::Parameterized::TableSyntax - where(:config_storages, :storages, :normalized) do - %w(default backup) | { 'default' => 0, 'backup' => 100 } | { 'default' => 0.0, 'backup' => 1.0 } - %w(default backup) | { 'default' => 100, 'backup' => 100 } | { 'default' => 0.5, 'backup' => 0.5 } - %w(default backup) | { 'default' => 20, 'backup' => 80 } | { 'default' => 0.2, 'backup' => 0.8 } - %w(default backup) | { 'default' => 0, 'backup' => 0 } | { 'default' => 0.0, 'backup' => 0.0 } - %w(default) | { 'default' => 0, 'backup' => 100 } | { 'default' => 0.0 } - %w(default) | { 'default' => 100, 'backup' => 100 } | { 'default' => 1.0 } - %w(default) | { 'default' => 20, 'backup' => 80 } | { 'default' => 1.0 } + where(:storages, :normalized) do + { 'default' => 0, 'backup' => 100 } | { 'default' => 0.0, 'backup' => 1.0 } + { 'default' => 100, 'backup' => 100 } | { 'default' => 0.5, 'backup' => 0.5 } + { 'default' => 20, 'backup' => 80 } | { 'default' => 0.2, 'backup' => 0.8 } + { 'default' => 0, 'backup' => 0 } | { 'default' => 0.0, 'backup' => 0.0 } end with_them do before do - allow(Gitlab.config.repositories.storages).to receive(:keys).and_return(config_storages) allow(setting).to receive(:repository_storages_weighted).and_return(storages) end diff --git a/spec/views/admin/application_settings/_repository_storage.html.haml_spec.rb b/spec/views/admin/application_settings/_repository_storage.html.haml_spec.rb index dc8f259eb56..2915fe1964f 100644 --- a/spec/views/admin/application_settings/_repository_storage.html.haml_spec.rb +++ b/spec/views/admin/application_settings/_repository_storage.html.haml_spec.rb @@ -3,49 +3,34 @@ require 'spec_helper' RSpec.describe 'admin/application_settings/_repository_storage.html.haml' do - let(:app_settings) { build(:application_setting, repository_storages_weighted: repository_storages_weighted) } - - before do - stub_storage_settings({ 'default': {}, 'mepmep': {}, 'foobar': {} }) - assign(:application_setting, app_settings) + let(:app_settings) { create(:application_setting) } + let(:repository_storages_weighted_attributes) { [:repository_storages_weighted_default, :repository_storages_weighted_mepmep, :repository_storages_weighted_foobar]} + let(:repository_storages_weighted) do + { + "default" => 100, + "mepmep" => 50 + } end - context 'additional storage config' do - let(:repository_storages_weighted) do - { - 'default' => 100, - 'mepmep' => 50 - } - end + before do + allow(app_settings).to receive(:repository_storages_weighted).and_return(repository_storages_weighted) + allow(app_settings).to receive(:repository_storages_weighted_mepmep).and_return(100) + allow(app_settings).to receive(:repository_storages_weighted_foobar).and_return(50) + assign(:application_setting, app_settings) + allow(ApplicationSetting).to receive(:repository_storages_weighted_attributes).and_return(repository_storages_weighted_attributes) + end + context 'when multiple storages are available' do it 'lists them all' do render - Gitlab.config.repositories.storages.keys.each do |storage_name| + # lists storages that are saved with weights + repository_storages_weighted.each do |storage_name, storage_weight| expect(rendered).to have_content(storage_name) end + # lists storage not saved with weight expect(rendered).to have_content('foobar') end end - - context 'fewer storage configs' do - let(:repository_storages_weighted) do - { - 'default' => 100, - 'mepmep' => 50, - 'something_old' => 100 - } - end - - it 'lists only configured storages' do - render - - Gitlab.config.repositories.storages.keys.each do |storage_name| - expect(rendered).to have_content(storage_name) - end - - expect(rendered).not_to have_content('something_old') - end - end end