diff --git a/.gitlab/ci/gitlab-gems.gitlab-ci.yml b/.gitlab/ci/gitlab-gems.gitlab-ci.yml index c996f96b823..eb0c8b8536e 100644 --- a/.gitlab/ci/gitlab-gems.gitlab-ci.yml +++ b/.gitlab/ci/gitlab-gems.gitlab-ci.yml @@ -5,9 +5,6 @@ include: - local: .gitlab/ci/templates/gem.gitlab-ci.yml inputs: gem_name: "click_house-client" - - local: .gitlab/ci/templates/gem.gitlab-ci.yml - inputs: - gem_name: "gitlab-ipynbdiff" - local: .gitlab/ci/templates/gem.gitlab-ci.yml inputs: gem_name: "gitlab-rspec" @@ -17,6 +14,9 @@ include: - local: .gitlab/ci/templates/gem.gitlab-ci.yml inputs: gem_name: "gitlab-utils" + - local: .gitlab/ci/templates/gem.gitlab-ci.yml + inputs: + gem_name: "ipynbdiff" - local: .gitlab/ci/templates/gem.gitlab-ci.yml inputs: gem_name: "rspec_flaky" diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml index 96a5e7ceea3..84c3de51f9a 100644 --- a/.gitlab/ci/rails.gitlab-ci.yml +++ b/.gitlab/ci/rails.gitlab-ci.yml @@ -275,7 +275,7 @@ rspec:deprecations: script: - grep -h -R "keyword" deprecations/ | awk '{$1=$1};1' | sort | uniq -c | sort - grep -R "keyword" deprecations/ | wc - - run_timed_command "fail_on_warnings bundle exec rubocop --only Lint/LastKeywordArgument --parallel" + - run_timed_command "fail_on_warnings bundle exec rubocop --config .rubocop.yml --only Lint/LastKeywordArgument --parallel" artifacts: expire_in: 31d when: always diff --git a/.gitlab/ci/static-analysis.gitlab-ci.yml b/.gitlab/ci/static-analysis.gitlab-ci.yml index 29887881da5..40582170cc9 100644 --- a/.gitlab/ci/static-analysis.gitlab-ci.yml +++ b/.gitlab/ci/static-analysis.gitlab-ci.yml @@ -137,7 +137,7 @@ rubocop: select_existing_files < "${RSPEC_CHANGED_FILES_PATH}" > "${RUBOCOP_TARGET_FILES}" # Skip running RuboCop if there's no target files if [ -s "${RUBOCOP_TARGET_FILES}" ]; then - run_timed_command "fail_on_warnings bundle exec rubocop --parallel --force-exclusion $(cat ${RUBOCOP_TARGET_FILES})" + run_timed_command "fail_on_warnings bundle exec rubocop --config .rubocop.yml --parallel --force-exclusion $(cat ${RUBOCOP_TARGET_FILES})" else echoinfo "Nothing interesting changed for RuboCop. Skipping." fi @@ -177,7 +177,7 @@ feature-flags-usage: script: # We need to disable the cache for this cop since it creates files under tmp/feature_flags/*.used, # the cache would prevent these files from being created. - - run_timed_command "fail_on_warnings bundle exec rubocop --only Gitlab/MarkUsedFeatureFlags --cache false" + - run_timed_command "fail_on_warnings bundle exec rubocop --config .rubocop.yml --only Gitlab/MarkUsedFeatureFlags --cache false" artifacts: expire_in: 31d when: always diff --git a/.gitlab/ci/templates/gem.gitlab-ci.yml b/.gitlab/ci/templates/gem.gitlab-ci.yml index 1f995e8040f..46c5e1342c6 100644 --- a/.gitlab/ci/templates/gem.gitlab-ci.yml +++ b/.gitlab/ci/templates/gem.gitlab-ci.yml @@ -14,6 +14,7 @@ spec: - if: '$CI_MERGE_REQUEST_EVENT_TYPE == "merged_result" || $CI_MERGE_REQUEST_EVENT_TYPE == "detached"' changes: - "$[[inputs.gem_path_prefix]]$[[inputs.gem_name]]/**/*" + - ".gitlab/ci/gitlab-gems.gitlab-ci.yml" - ".gitlab/ci/templates/gem.gitlab-ci.yml" - "gems/gem.gitlab-ci.yml" diff --git a/Gemfile b/Gemfile index 8db4bd0980d..53c2c2241af 100644 --- a/Gemfile +++ b/Gemfile @@ -184,7 +184,7 @@ gem 'elasticsearch-rails', '~> 7.2', require: 'elasticsearch/rails/instrumentati gem 'elasticsearch-api', '7.13.3' gem 'aws-sdk-core', '~> 3.178.0' gem 'aws-sdk-cloudformation', '~> 1' -gem 'aws-sdk-s3', '~> 1.128.0' +gem 'aws-sdk-s3', '~> 1.129.0' gem 'faraday_middleware-aws-sigv4', '~>0.3.0' gem 'typhoeus', '~> 1.4.0' # Used with Elasticsearch to support http keep-alive connections diff --git a/Gemfile.checksum b/Gemfile.checksum index 2e517949024..0361b4afa44 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -39,7 +39,7 @@ {"name":"aws-sdk-cloudformation","version":"1.41.0","platform":"ruby","checksum":"31e47539719734413671edf9b1a31f8673fbf9688549f50c41affabbcb1c6b26"}, {"name":"aws-sdk-core","version":"3.178.0","platform":"ruby","checksum":"192485a032536ff8c8eb037f1204b432129a612f4de13e36c0d2cf0dec8165cb"}, {"name":"aws-sdk-kms","version":"1.64.0","platform":"ruby","checksum":"40de596c95047bfc6e1aacea24f3df6241aa716b6f7ce08ac4c5f7e3120395ad"}, -{"name":"aws-sdk-s3","version":"1.128.0","platform":"ruby","checksum":"5b1420d5be9654a9b1b5c8309d75ce72592f3a1e29def15ea07a853b96999d85"}, +{"name":"aws-sdk-s3","version":"1.129.0","platform":"ruby","checksum":"82b8eab53d22754e5855dbec3e7a9a53c348de2bbf202774b4483f9b06cb0f1a"}, {"name":"aws-sigv4","version":"1.6.0","platform":"ruby","checksum":"ca9e6a15cd424f1f32b524b9760995331459bc22e67d3daad4fcf0c0084b087d"}, {"name":"axe-core-api","version":"4.6.0","platform":"ruby","checksum":"1b0ddec3353f108dc10363baf2282f43a5ff7f13d4e25f99071294e78f8a6c62"}, {"name":"axe-core-rspec","version":"4.6.0","platform":"ruby","checksum":"11c25bc9dd388c137ba4e5e63d64d20092bf22c884d8ffc829a22acfbacd747f"}, diff --git a/Gemfile.lock b/Gemfile.lock index f9ddaa75f2c..f6536bcf525 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -262,7 +262,7 @@ GEM aws-sdk-kms (1.64.0) aws-sdk-core (~> 3, >= 3.165.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.128.0) + aws-sdk-s3 (1.129.0) aws-sdk-core (~> 3, >= 3.177.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.6) @@ -1740,7 +1740,7 @@ DEPENDENCIES awesome_print aws-sdk-cloudformation (~> 1) aws-sdk-core (~> 3.178.0) - aws-sdk-s3 (~> 1.128.0) + aws-sdk-s3 (~> 1.129.0) axe-core-rspec babosa (~> 2.0) base32 (~> 0.3.0) diff --git a/app/assets/javascripts/milestones/index.js b/app/assets/javascripts/milestones/index.js index 420f7cee4d2..403db0865f0 100644 --- a/app/assets/javascripts/milestones/index.js +++ b/app/assets/javascripts/milestones/index.js @@ -1,10 +1,9 @@ -import $ from 'jquery'; import Vue from 'vue'; import initDatePicker from '~/behaviors/date_picker'; -import GLForm from '~/gl_form'; import { BV_SHOW_MODAL } from '~/lib/utils/constants'; import Milestone from '~/milestones/milestone'; import { renderGFM } from '~/behaviors/markdown/render_gfm'; +import { mountMarkdownEditor } from '~/vue_shared/components/markdown/mount_markdown_editor'; import Sidebar from '~/right_sidebar'; import MountMilestoneSidebar from '~/sidebar/mount_milestone_sidebar'; import Translate from '~/vue_shared/translate'; @@ -22,22 +21,10 @@ export const MILESTONE_DESCRIPTION_ELEMENT = '.milestone-detail .description'; export const MILESTONE_DESCRIPTION_TASK_LIST_CONTAINER_ELEMENT = `${MILESTONE_DESCRIPTION_ELEMENT}.js-task-list-container`; export const MILESTONE_DETAIL_ELEMENT = '.milestone-detail'; -export function initForm(initGFM = true) { +export function initForm() { + mountMarkdownEditor(); new ZenMode(); // eslint-disable-line no-new initDatePicker(); - - // eslint-disable-next-line no-new - new GLForm($('.milestone-form'), { - emojis: true, - members: initGFM, - issues: initGFM, - mergeRequests: initGFM, - epics: initGFM, - milestones: initGFM, - labels: initGFM, - snippets: initGFM, - vulnerabilities: initGFM, - }); } export function initShow() { diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue index 4f9774dba4a..c690e8fac43 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue @@ -43,7 +43,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, - inject: ['isGroupPage'], + inject: ['isGroupPage', 'canDeletePackages'], props: { packageEntity: { type: Object, @@ -122,7 +122,7 @@ export default { diff --git a/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js b/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js index e12815f0094..b7b154bfc23 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js +++ b/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js @@ -79,6 +79,7 @@ export function mountMarkdownEditor(options = {}) { const supportsQuickActions = parseBoolean(el.dataset.supportsQuickActions ?? true); const enableAutocomplete = parseBoolean(el.dataset.enableAutocomplete ?? true); const disableAttachments = parseBoolean(el.dataset.disableAttachments ?? false); + const autofocus = parseBoolean(el.dataset.autofocus ?? true); const hiddenInput = el.querySelector('input[type="hidden"]'); const formFieldName = hiddenInput.getAttribute('name'); const formFieldId = hiddenInput.getAttribute('id'); @@ -128,7 +129,7 @@ export function mountMarkdownEditor(options = {}) { autocompleteDataSources: gl.GfmAutoComplete?.dataSources, supportsQuickActions, disableAttachments, - autofocus: true, + autofocus, }, }); }, diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 207f8ec556a..058cce9af36 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -76,6 +76,33 @@ } } +.settings-section { + @include gl-pt-6; + + &::after { + content: ''; + display: block; + @include gl-pb-7; + } +} + +.settings-section:first-of-type, +.settings-section-no-bottom + .settings-section { + @include gl-pt-0; +} + +.settings-section:not(.settings-section-no-bottom) + .settings-section { + @include gl-border-t; +} + +.settings-section-no-bottom::after { + @include gl-pb-0; + + @include media-breakpoint-up(sm) { + @include gl-pb-5; + } +} + $sticky-header-z-index: 98; .settings-sticky-header, diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 5f6b55ea928..cbed75019f2 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -9,6 +9,10 @@ class Groups::MilestonesController < Groups::ApplicationController feature_category :team_planning urgency :low + before_action do + push_frontend_feature_flag(:content_editor_on_issues, group) + end + def index respond_to do |format| format.html do diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index 35b65dbce7e..1f4e5b54500 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -24,6 +24,10 @@ class Projects::MilestonesController < Projects::ApplicationController feature_category :team_planning urgency :low + before_action do + push_frontend_feature_flag(:content_editor_on_issues, @project) + end + def index @sort = params[:sort] || 'due_date_asc' @milestones = milestones.sort_by_attribute(@sort) diff --git a/app/controllers/projects/service_desk/custom_email_controller.rb b/app/controllers/projects/service_desk/custom_email_controller.rb new file mode 100644 index 00000000000..fb5e87f9a97 --- /dev/null +++ b/app/controllers/projects/service_desk/custom_email_controller.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +module Projects + module ServiceDesk + class CustomEmailController < Projects::ApplicationController + before_action :check_feature_flag_enabled + before_action :authorize_admin_project! + + feature_category :service_desk + urgency :low + + def create + response = ::ServiceDesk::CustomEmails::CreateService.new( + project: project, + current_user: current_user, + params: params + ).execute + + json_response(service_response: response) + end + + def update + response = ServiceDeskSettings::UpdateService.new(project, current_user, update_setting_params).execute + + if response.error? + json_response( + error_message: s_("ServiceDesk|Cannot update custom email"), + status: :unprocessable_entity + ) + return + end + + json_response + end + + def destroy + response = ::ServiceDesk::CustomEmails::DestroyService.new( + project: project, + current_user: current_user + ).execute + + json_response(service_response: response) + end + + def show + json_response + end + + private + + def update_setting_params + params.permit(:custom_email_enabled) + end + + def json_response(error_message: nil, status: :ok, service_response: nil) + if service_response.present? + status = service_response.success? ? :ok : :unprocessable_entity + error_message = service_response.message + end + + respond_to do |format| + format.json { render json: custom_email_attributes(error_message: error_message), status: status } + end + end + + def custom_email_attributes(error_message:) + setting = project.service_desk_setting + + { + custom_email: setting&.custom_email, + custom_email_enabled: setting&.custom_email_enabled || false, + custom_email_verification_state: setting&.custom_email_verification&.state, + custom_email_verification_error: setting&.custom_email_verification&.error, + custom_email_smtp_address: setting&.custom_email_credential&.smtp_address, + error_message: error_message + } + end + + def check_feature_flag_enabled + render_404 unless Feature.enabled?(:service_desk_custom_email, @project) + end + end + end +end diff --git a/app/helpers/packages_helper.rb b/app/helpers/packages_helper.rb index 8861f1ffe9a..31fcc77925b 100644 --- a/app/helpers/packages_helper.rb +++ b/app/helpers/packages_helper.rb @@ -74,6 +74,16 @@ module PackagesHelper Ability.allowed?(current_user, :admin_group, group) end + def can_delete_packages?(project) + Gitlab.config.packages.enabled && + Ability.allowed?(current_user, :destroy_package, project) + end + + def can_delete_group_packages?(group) + group.packages_feature_enabled? && + Ability.allowed?(current_user, :destroy_package, group) + end + def cleanup_settings_data { project_id: @project.id, diff --git a/app/models/ml/experiment.rb b/app/models/ml/experiment.rb index d1277efac7b..5c5f8d3b2db 100644 --- a/app/models/ml/experiment.rb +++ b/app/models/ml/experiment.rb @@ -11,6 +11,7 @@ module Ml belongs_to :project belongs_to :user + belongs_to :model, optional: true, inverse_of: :default_experiment has_many :candidates, class_name: 'Ml::Candidate' has_many :metadata, class_name: 'Ml::ExperimentMetadata' @@ -22,10 +23,21 @@ module Ml has_internal_id :iid, scope: :project + before_destroy :stop_destroy + def package_name "#{PACKAGE_PREFIX}#{iid}" end + def stop_destroy + return unless model_id + + errors[:base] << "Cannot delete an experiment associated to a model" + # According to docs, throw is the correct way to stop on a callback + # https://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html#module-ActiveRecord::Callbacks-label-Canceling+callbacks + throw :abort # rubocop:disable Cop/BanCatchThrow + end + class << self def by_project_id_and_iid(project_id, iid) find_by(project_id: project_id, iid: iid) diff --git a/app/models/ml/model.rb b/app/models/ml/model.rb new file mode 100644 index 00000000000..b23aa724fad --- /dev/null +++ b/app/models/ml/model.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Ml + class Model < ApplicationRecord + validates :project, :default_experiment, presence: true + validates :name, + format: Gitlab::Regex.ml_model_name_regex, + uniqueness: { scope: :project }, + presence: true, + length: { maximum: 255 } + + validate :valid_default_experiment? + + has_one :default_experiment, class_name: 'Ml::Experiment' + belongs_to :project + + def valid_default_experiment? + return unless default_experiment + + errors.add(:default_experiment) unless default_experiment.name == name + errors.add(:default_experiment) unless default_experiment.project_id == project_id + end + end +end diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml index 89f460606cb..e84fd7a8692 100644 --- a/app/views/groups/milestones/_form.html.haml +++ b/app/views/groups/milestones/_form.html.haml @@ -10,12 +10,16 @@ = render "shared/milestones/form_dates", f: f .form-group = f.label :description, _("Description") - = render layout: 'shared/md_preview', locals: { url: group_preview_markdown_path } do - = render 'shared/zen', f: f, attr: :description, - classes: 'note-textarea', - qa_selector: 'milestone_description_field', - supports_autocomplete: true, - placeholder: _('Write milestone description...') + - @gfm_form = true + .js-markdown-editor{ data: { render_markdown_path: group_preview_markdown_path, + markdown_docs_path: help_page_path('user/markdown'), + qa_selector: 'milestone_description_field', + form_field_placeholder: _('Write milestone description...'), + supports_quick_actions: 'false', + enable_autocomplete: 'true', + autofocus: 'false', + form_field_classes: 'note-textarea js-gfm-input markdown-area' } } + = f.hidden_field :description .clearfix .error-alert diff --git a/app/views/groups/packages/index.html.haml b/app/views/groups/packages/index.html.haml index b6cf26c3677..6d0f24bf08c 100644 --- a/app/views/groups/packages/index.html.haml +++ b/app/views/groups/packages/index.html.haml @@ -10,4 +10,5 @@ npm_instance_url: package_registry_instance_url(:npm), project_list_url: '', settings_path: show_group_package_registry_settings(@group) ? group_settings_packages_and_registries_path(@group) : '', + can_delete_packages: can_delete_group_packages?(@group).to_s, group_list_url: group_packages_path(@group) } } diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 6ca9e1a2ad3..ebdea5786f5 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -8,7 +8,7 @@ .js-user-profile - else = gitlab_ui_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user js-edit-user js-quick-submit gl-show-field-errors js-password-prompt-form', remote: true }, authenticity_token: true do |f| - .js-search-settings-section + .settings-section.js-search-settings-section .settings-sticky-header .settings-sticky-header-inner %h4.gl-my-0 @@ -44,9 +44,8 @@ button_options: { class: 'gl-mt-3', data: { confirm: s_("Profiles|Avatar will be removed. Are you sure?") } }, method: :delete) do = s_("Profiles|Remove avatar") - .gl-pb-8 - .js-search-settings-section.gl-border-t.gl-pt-6 + .settings-section.js-search-settings-section.gl-border-t.gl-pt-6 .settings-sticky-header .settings-sticky-header-inner %h4.gl-my-0= s_("Profiles|Current status") @@ -60,18 +59,16 @@ = status_form.hidden_field :clear_status_after, value: user_clear_status_at(@user), data: { js_name: 'clearStatusAfter' } - .gl-pb-7 - .user-time-preferences.js-search-settings-section.gl-border-t.gl-pt-6 + .settings-section.user-time-preferences.js-search-settings-section.gl-border-t.gl-pt-6 .settings-sticky-header .settings-sticky-header-inner %h4.gl-my-0= s_("Profiles|Time settings") %p.gl-text-secondary= s_("Profiles|Set your local time zone.") = f.label :user_timezone, _("Time zone") .js-timezone-dropdown{ data: { timezone_data: timezone_data.to_json, initial_value: @user.timezone, name: 'user[timezone]' } } - .gl-pb-7 - .js-search-settings-section.gl-border-t.gl-pt-6 + .settings-section.js-search-settings-section.gl-border-t.gl-pt-6 .settings-sticky-header .settings-sticky-header-inner %h4.gl-my-0 @@ -168,7 +165,6 @@ = s_("Profiles|Achievements") = f.gitlab_ui_checkbox_component :achievements_enabled, s_('Profiles|Display achievements on your profile') - .gl-pb-7 .js-hide-when-nothing-matches-search.settings-sticky-footer = f.submit s_("Profiles|Update profile settings"), class: 'gl-mr-3 js-password-prompt-btn', pajamas_button: true diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index be6f9ac83dc..a1300ccd835 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -12,13 +12,16 @@ = render 'shared/milestones/form_dates', f: f .form-group = f.label :description, _('Description') - = render layout: 'shared/md_preview', locals: { url: preview_markdown_path(@project) } do - = render 'shared/zen', f: f, attr: :description, - classes: 'note-textarea', - qa_selector: 'milestone_description_field', - supports_autocomplete: true, - placeholder: _('Write milestone description...') - = render 'shared/notes/hints' + - @gfm_form = true + .js-markdown-editor{ data: { render_markdown_path: preview_markdown_path(@project), + markdown_docs_path: help_page_path('user/markdown'), + qa_selector: 'milestone_description_field', + form_field_placeholder: _('Write milestone description...'), + supports_quick_actions: 'false', + enable_autocomplete: 'true', + autofocus: 'false', + form_field_classes: 'note-textarea js-gfm-input markdown-area' } } + = f.hidden_field :description .clearfix .error-alert diff --git a/app/views/projects/packages/packages/index.html.haml b/app/views/projects/packages/packages/index.html.haml index 48aaf0884c8..5397828d48e 100644 --- a/app/views/projects/packages/packages/index.html.haml +++ b/app/views/projects/packages/packages/index.html.haml @@ -10,4 +10,5 @@ npm_instance_url: package_registry_instance_url(:npm), project_list_url: project_packages_path(@project), settings_path: show_package_registry_settings(@project) ? project_settings_packages_and_registries_path(@project) : '', + can_delete_packages: can_delete_packages?(@project).to_s, group_list_url: '' } } diff --git a/config/feature_flags/development/load_merge_request_via_links.yml b/config/feature_flags/development/load_merge_request_via_links.yml deleted file mode 100644 index a9db587a7a9..00000000000 --- a/config/feature_flags/development/load_merge_request_via_links.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: load_merge_request_via_links -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117321 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/412177 -milestone: '16.1' -type: development -group: group::threat insights -default_enabled: false diff --git a/config/routes/project.rb b/config/routes/project.rb index 787d95669d1..4469795ee2c 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -489,6 +489,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do resources :candidates, only: [:show, :destroy], controller: 'candidates', param: :iid resources :models, only: [:index], controller: 'models' end + + namespace :service_desk do + resource :custom_email, only: [:show, :create, :update, :destroy], controller: 'custom_email' + end end # End of the /-/ scope. diff --git a/db/docs/ml_models.yml b/db/docs/ml_models.yml new file mode 100644 index 00000000000..112a1f9263c --- /dev/null +++ b/db/docs/ml_models.yml @@ -0,0 +1,10 @@ +--- +table_name: ml_models +classes: + - Ml::Model +feature_categories: + - mlops +description: A machine learning model for the model registry +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125302 +milestone: '16.2' +gitlab_schema: gitlab_main diff --git a/db/migrate/20230705092150_create_ml_models.rb b/db/migrate/20230705092150_create_ml_models.rb new file mode 100644 index 00000000000..df8827a781f --- /dev/null +++ b/db/migrate/20230705092150_create_ml_models.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class CreateMlModels < Gitlab::Database::Migration[2.1] + enable_lock_retries! + + def up + create_table :ml_models do |t| + t.timestamps_with_timezone null: false + t.references :project, foreign_key: true, index: true, on_delete: :cascade, null: false + t.text :name, limit: 255, null: false + + t.index [:project_id, :name], unique: true + end + end + + def down + drop_table :ml_models + end +end diff --git a/db/migrate/20230706130217_add_column_model_id_to_ml_experiments.rb b/db/migrate/20230706130217_add_column_model_id_to_ml_experiments.rb new file mode 100644 index 00000000000..4eab027bc22 --- /dev/null +++ b/db/migrate/20230706130217_add_column_model_id_to_ml_experiments.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class AddColumnModelIdToMlExperiments < Gitlab::Database::Migration[2.1] + def change + # rubocop:disable Migration/AddReference + add_reference :ml_experiments, + :model, + index: true, + null: true, + unique: true, + foreign_key: { on_delete: :cascade, to_table: :ml_models } + # rubocop:enable Migration/AddReference + end +end diff --git a/db/schema_migrations/20230705092150 b/db/schema_migrations/20230705092150 new file mode 100644 index 00000000000..96e51e689e2 --- /dev/null +++ b/db/schema_migrations/20230705092150 @@ -0,0 +1 @@ +5954829dd244b4536beeb9b92a157539feb5207bc1309e177310895f269f54d8 \ No newline at end of file diff --git a/db/schema_migrations/20230706130217 b/db/schema_migrations/20230706130217 new file mode 100644 index 00000000000..8d2a74a3fc2 --- /dev/null +++ b/db/schema_migrations/20230706130217 @@ -0,0 +1 @@ +3a355ebb2299786d9aa5ce9bf1f07b2bbd3b6aca719c12c78c4a945c4a96cfe3 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 5efc4c0765f..7db5bfbca1e 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -18592,6 +18592,7 @@ CREATE TABLE ml_experiments ( user_id bigint, name text NOT NULL, deleted_on timestamp with time zone, + model_id bigint, CONSTRAINT check_ee07a0be2c CHECK ((char_length(name) <= 255)) ); @@ -18604,6 +18605,24 @@ CREATE SEQUENCE ml_experiments_id_seq ALTER SEQUENCE ml_experiments_id_seq OWNED BY ml_experiments.id; +CREATE TABLE ml_models ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + project_id bigint NOT NULL, + name text NOT NULL, + CONSTRAINT check_1fd2cc7d93 CHECK ((char_length(name) <= 255)) +); + +CREATE SEQUENCE ml_models_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE ml_models_id_seq OWNED BY ml_models.id; + CREATE TABLE namespace_admin_notes ( id bigint NOT NULL, created_at timestamp with time zone NOT NULL, @@ -25629,6 +25648,8 @@ ALTER TABLE ONLY ml_experiment_metadata ALTER COLUMN id SET DEFAULT nextval('ml_ ALTER TABLE ONLY ml_experiments ALTER COLUMN id SET DEFAULT nextval('ml_experiments_id_seq'::regclass); +ALTER TABLE ONLY ml_models ALTER COLUMN id SET DEFAULT nextval('ml_models_id_seq'::regclass); + ALTER TABLE ONLY namespace_admin_notes ALTER COLUMN id SET DEFAULT nextval('namespace_admin_notes_id_seq'::regclass); ALTER TABLE ONLY namespace_bans ALTER COLUMN id SET DEFAULT nextval('namespace_bans_id_seq'::regclass); @@ -27848,6 +27869,9 @@ ALTER TABLE ONLY ml_experiment_metadata ALTER TABLE ONLY ml_experiments ADD CONSTRAINT ml_experiments_pkey PRIMARY KEY (id); +ALTER TABLE ONLY ml_models + ADD CONSTRAINT ml_models_pkey PRIMARY KEY (id); + ALTER TABLE ONLY namespace_admin_notes ADD CONSTRAINT namespace_admin_notes_pkey PRIMARY KEY (id); @@ -31974,12 +31998,18 @@ CREATE INDEX index_ml_candidates_on_user_id ON ml_candidates USING btree (user_i CREATE UNIQUE INDEX index_ml_experiment_metadata_on_experiment_id_and_name ON ml_experiment_metadata USING btree (experiment_id, name); +CREATE INDEX index_ml_experiments_on_model_id ON ml_experiments USING btree (model_id); + CREATE UNIQUE INDEX index_ml_experiments_on_project_id_and_iid ON ml_experiments USING btree (project_id, iid); CREATE UNIQUE INDEX index_ml_experiments_on_project_id_and_name ON ml_experiments USING btree (project_id, name); CREATE INDEX index_ml_experiments_on_user_id ON ml_experiments USING btree (user_id); +CREATE INDEX index_ml_models_on_project_id ON ml_models USING btree (project_id); + +CREATE UNIQUE INDEX index_ml_models_on_project_id_and_name ON ml_models USING btree (project_id, name); + CREATE UNIQUE INDEX index_mr_blocks_on_blocking_and_blocked_mr_ids ON merge_request_blocks USING btree (blocking_merge_request_id, blocked_merge_request_id); CREATE INDEX index_mr_cleanup_schedules_timestamps_status ON merge_request_cleanup_schedules USING btree (scheduled_at) WHERE ((completed_at IS NULL) AND (status = 0)); @@ -36969,6 +36999,9 @@ ALTER TABLE ONLY project_repository_storage_moves ALTER TABLE ONLY ml_candidate_metadata ADD CONSTRAINT fk_rails_5117dddf22 FOREIGN KEY (candidate_id) REFERENCES ml_candidates(id) ON DELETE CASCADE; +ALTER TABLE ONLY ml_models + ADD CONSTRAINT fk_rails_51e87f7c50 FOREIGN KEY (project_id) REFERENCES projects(id); + ALTER TABLE ONLY elastic_group_index_statuses ADD CONSTRAINT fk_rails_52b9969b12 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; @@ -37428,6 +37461,9 @@ ALTER TABLE ONLY boards_epic_board_recent_visits ALTER TABLE ONLY packages_dependency_links ADD CONSTRAINT fk_rails_96ef1c00d3 FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE CASCADE; +ALTER TABLE ONLY ml_experiments + ADD CONSTRAINT fk_rails_97194a054e FOREIGN KEY (model_id) REFERENCES ml_models(id) ON DELETE CASCADE; + ALTER TABLE ONLY group_repository_storage_moves ADD CONSTRAINT fk_rails_982bb5daf1 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; diff --git a/doc/architecture/blueprints/modular_monolith/references.md b/doc/architecture/blueprints/modular_monolith/references.md index df8e021890c..2c7d3dc972d 100644 --- a/doc/architecture/blueprints/modular_monolith/references.md +++ b/doc/architecture/blueprints/modular_monolith/references.md @@ -60,8 +60,8 @@ concepts to look at as an example. App Continuum: -An illustration of how an application can evolve from a small, unstructed app, through various -stages including a modular well-structured, monolith, all the way to a microservices architecture. +An illustration of how an application can evolve from a small, unstructured app, through various +stages including a modular well-structured monolith, all the way to a microservices architecture. Includes discussion of why you might want to stop at various stages, and specifically the challenges/concerns with making the jump to microservices, and why sticking with a diff --git a/doc/ci/cloud_deployment/index.md b/doc/ci/cloud_deployment/index.md index ce03e9e3916..3e77e8c6c25 100644 --- a/doc/ci/cloud_deployment/index.md +++ b/doc/ci/cloud_deployment/index.md @@ -13,6 +13,12 @@ to AWS. You can reference these images in your CI/CD pipeline. If you're using GitLab.com and deploying to the [Amazon Elastic Container Service](https://aws.amazon.com/ecs/) (ECS), read about [deploying to ECS](ecs/deploy_to_aws_ecs.md). +NOTE: +If you are comfortable configuring a deployment yourself and just need to retrieve +AWS credentials, consider using [ID tokens and OpenID Connect](../cloud_services/aws/index.md). +ID tokens are more secure than storing credentials in CI/CD variables, but do not +work with the guidance on this page. + ## Authenticate GitLab with AWS To use GitLab CI/CD to connect to AWS, you must authenticate. diff --git a/doc/development/testing_guide/end_to_end/feature_flags.md b/doc/development/testing_guide/end_to_end/feature_flags.md index e473b158087..228f63d2354 100644 --- a/doc/development/testing_guide/end_to_end/feature_flags.md +++ b/doc/development/testing_guide/end_to_end/feature_flags.md @@ -55,18 +55,16 @@ RSpec.describe "with feature flag enabled", feature_flag: { let(:project) { Resource::Project.fabricate_via_api! } - before do + around do |example| Runtime::Feature.enable(:feature_flag_name, project: project) + example.run + Runtime::Feature.disable(:feature_flag_name, project: project) end it "feature flag test" do # Execute the test with the feature flag enabled. # It will only affect the project created in this test. end - - after do - Runtime::Feature.disable(:feature_flag_name, project: project) - end end ``` diff --git a/doc/integration/jira/troubleshooting.md b/doc/integration/jira/troubleshooting.md index d592455788d..49b5dfba566 100644 --- a/doc/integration/jira/troubleshooting.md +++ b/doc/integration/jira/troubleshooting.md @@ -108,3 +108,12 @@ Check [`production.log`](../../administration/logs/index.md#productionlog) to se ``` If that's the case, ensure the **Due date** field is visible for issues in the integrated Jira project. + +## `An error occurred while requesting data from Jira` when viewing the Jira issues list in GitLab + +You might see a `An error occurred while requesting data from Jira` message when you attempt to view the Jira issues list in GitLab. + +You can see this error when the authentication details in the Jira integration settings are incomplete or incorrect. + +To attempt to resolve this error, try [configuring the integration](configure.md#configure-the-integration) again. Verify that the +authentication details are correct, re-enter your API token or password, and save your changes. diff --git a/gems/config/rubocop.yml b/gems/config/rubocop.yml index f591cde534f..3458998e114 100644 --- a/gems/config/rubocop.yml +++ b/gems/config/rubocop.yml @@ -15,6 +15,7 @@ inherit_mode: AllCops: # Target the current Ruby version. For example, "3.0" or "3.1". TargetRubyVersion: <%= RUBY_VERSION[/^\d+\.\d+/, 0] %> + SuggestExtensions: false # This cop doesn't make sense in the context of gems CodeReuse/ActiveRecord: @@ -59,6 +60,18 @@ Naming/FileName: Exclude: - spec/**/*.rb +RSpec/ContextWording: + Prefixes: + - 'when' + - 'with' + - 'without' + - 'for' + - 'and' + - 'on' + - 'in' + - 'as' + - 'if' + # This cop doesn't make sense in the context of gems RSpec/MissingFeatureCategory: Enabled: false diff --git a/gems/ipynbdiff/.rubocop.yml b/gems/ipynbdiff/.rubocop.yml index f3a696778a4..e30c6c44434 100644 --- a/gems/ipynbdiff/.rubocop.yml +++ b/gems/ipynbdiff/.rubocop.yml @@ -4,8 +4,25 @@ inherit_from: CodeReuse/ActiveRecord: Enabled: false +Gitlab/Json: + Enabled: false + +# FIXME +Gitlab/RSpec/AvoidSetup: + Enabled: false + Naming/FileName: Exclude: - spec/**/*.rb - lib/gitlab/rspec.rb - lib/gitlab/rspec/all.rb + +Rails/Pluck: + Enabled: false + +RSpec/AvoidConditionalStatements: + Enabled: false + +RSpec/MultipleMemoizedHelpers: + Max: 6 + AllowSubject: true diff --git a/gems/ipynbdiff/Gemfile.lock b/gems/ipynbdiff/Gemfile.lock index 1c52ff8c829..9a583bdf274 100644 --- a/gems/ipynbdiff/Gemfile.lock +++ b/gems/ipynbdiff/Gemfile.lock @@ -1,26 +1,46 @@ PATH remote: . specs: - gitlab-ipynbdiff (0.4.7) + ipynbdiff (0.4.7) diffy (~> 3.4) oj (~> 3.13.16) GEM remote: https://rubygems.org/ specs: + activesupport (7.0.6) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) ast (2.4.2) benchmark-memory (0.2.0) memory_profiler (~> 1) - binding_ninja (0.2.3) + binding_of_caller (1.0.0) + debug_inspector (>= 0.0.1) coderay (1.1.3) + concurrent-ruby (1.2.2) + debug_inspector (1.1.0) diff-lcs (1.5.0) diffy (3.4.2) docile (1.4.0) + gitlab-styles (10.1.0) + rubocop (~> 1.50.2) + rubocop-graphql (~> 0.18) + rubocop-performance (~> 1.15) + rubocop-rails (~> 2.17) + rubocop-rspec (~> 2.22) + i18n (1.14.1) + concurrent-ruby (~> 1.0) + json (2.6.3) memory_profiler (1.0.0) method_source (1.0.0) + minitest (5.18.1) oj (3.13.23) - parser (3.1.2.0) + parallel (1.23.0) + parser (3.2.2.3) ast (~> 2.4.1) + racc proc_to_ast (0.1.0) coderay parser @@ -28,7 +48,12 @@ GEM pry (0.14.1) coderay (~> 1.1) method_source (~> 1.0) + racc (1.7.1) + rack (3.0.8) + rainbow (3.1.1) rake (13.0.6) + regexp_parser (2.8.1) + rexml (3.2.5) rspec (3.11.0) rspec-core (~> 3.11.0) rspec-expectations (~> 3.11.0) @@ -41,22 +66,60 @@ GEM rspec-mocks (3.11.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.11.0) - rspec-parameterized (0.5.2) - binding_ninja (>= 0.2.3) + rspec-parameterized (1.0.0) + rspec-parameterized-core (< 2) + rspec-parameterized-table_syntax (< 2) + rspec-parameterized-core (1.0.0) parser proc_to_ast rspec (>= 2.13, < 4) unparser + rspec-parameterized-table_syntax (1.0.0) + binding_of_caller + rspec-parameterized-core (< 2) rspec-support (3.11.0) + rubocop (1.50.2) + json (~> 2.3) + parallel (~> 1.10) + parser (>= 3.2.0.0) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.28.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.29.0) + parser (>= 3.2.1.0) + rubocop-capybara (2.18.0) + rubocop (~> 1.41) + rubocop-factory_bot (2.23.1) + rubocop (~> 1.33) + rubocop-graphql (0.19.0) + rubocop (>= 0.87, < 2) + rubocop-performance (1.18.0) + rubocop (>= 1.7.0, < 2.0) + rubocop-ast (>= 0.4.0) + rubocop-rails (2.20.2) + activesupport (>= 4.2.0) + rack (>= 1.1) + rubocop (>= 1.33.0, < 2.0) + rubocop-rspec (2.22.0) + rubocop (~> 1.33) + rubocop-capybara (~> 2.17) + rubocop-factory_bot (~> 2.22) + ruby-progressbar (1.13.0) simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) simplecov_json_formatter (0.1.4) - unparser (0.6.5) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (2.4.2) + unparser (0.6.8) diff-lcs (~> 1.3) - parser (>= 3.1.0) + parser (>= 3.2.0) PLATFORMS ruby @@ -64,12 +127,13 @@ PLATFORMS DEPENDENCIES benchmark-memory (~> 0.2.0) bundler (~> 2.2) - gitlab-ipynbdiff! + gitlab-styles (~> 10.1.0) + ipynbdiff! pry (~> 0.14) rake (~> 13.0) rspec (~> 3.10) - rspec-parameterized (~> 0.5.1) - simplecov + rspec-parameterized (~> 1.0) + simplecov (~> 0.22.0) BUNDLED WITH 2.3.16 diff --git a/gems/ipynbdiff/ipynbdiff.gemspec b/gems/ipynbdiff/ipynbdiff.gemspec index 1ec68557a05..8bc3e1b142d 100644 --- a/gems/ipynbdiff/ipynbdiff.gemspec +++ b/gems/ipynbdiff/ipynbdiff.gemspec @@ -24,9 +24,10 @@ Gem::Specification.new do |s| s.add_development_dependency 'benchmark-memory', '~>0.2.0' s.add_development_dependency 'bundler', '~> 2.2' + s.add_development_dependency 'gitlab-styles', '~> 10.1.0' s.add_development_dependency 'pry', '~> 0.14' s.add_development_dependency 'rake', '~> 13.0' s.add_development_dependency 'rspec', '~> 3.10' - s.add_development_dependency 'rspec-parameterized', '~> 0.5.1' - s.add_development_dependency 'simplecov', '~> 0.12.0' + s.add_development_dependency 'rspec-parameterized', '~> 1.0' + s.add_development_dependency 'simplecov', '~> 0.22.0' end diff --git a/gems/ipynbdiff/spec/ipynb_diff/symbol_map_spec.rb b/gems/ipynbdiff/spec/ipynb_diff/symbol_map_spec.rb index 66fb7af66af..94cb10772aa 100644 --- a/gems/ipynbdiff/spec/ipynb_diff/symbol_map_spec.rb +++ b/gems/ipynbdiff/spec/ipynb_diff/symbol_map_spec.rb @@ -8,7 +8,7 @@ describe IpynbDiff::SymbolMap do end describe '.parse' do - subject { described_class.parse(JSON.pretty_generate(source)) } + subject(:symbol_map) { described_class.parse(JSON.pretty_generate(source)) } context 'when object has blank key' do let(:source) { { "": { "": 5 } } } @@ -37,8 +37,8 @@ describe IpynbDiff::SymbolMap do context 'when object has inner object and number, string and array with object' do let(:source) { { obj1: { obj2: [123, 2, true], obj3: "hel\nlo", obj4: true, obj5: 123, obj6: 'a' } } } - it do - is_expected.to match_array( + specify do + expect(symbol_map).to match_array( res(['.obj1', 2], ['.obj1.obj2', 3], ['.obj1.obj2.0', 4], diff --git a/gems/ipynbdiff/spec/ipynb_diff_spec.rb b/gems/ipynbdiff/spec/ipynb_diff_spec.rb index 44fcd99f131..3ca092aaf5c 100644 --- a/gems/ipynbdiff/spec/ipynb_diff_spec.rb +++ b/gems/ipynbdiff/spec/ipynb_diff_spec.rb @@ -4,7 +4,7 @@ require_relative 'test_helper' describe IpynbDiff do def diff_signs(diff) - diff.to_s(:text).scan(/.*\n/).map { |l| l[0] }.join('') # rubocop:disable Rails/Pluck + diff.to_s(:text).scan(/.*\n/).map { |l| l[0] }.join('') end describe '.diff' do @@ -18,9 +18,7 @@ describe IpynbDiff do subject { described_class.diff(from, to, include_frontmatter: include_frontmatter, hide_images: hide_images) } context 'if preprocessing is active' do - it 'html tables are stripped' do - is_expected.not_to include('') - end + it { is_expected.not_to include('') } end context 'when to is nil' do diff --git a/lefthook.yml b/lefthook.yml index 7a3c7e5d174..f29de0a791d 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -44,7 +44,7 @@ pre-push: tags: backend style files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD glob: '*.{rb,rake}' - run: REVEAL_RUBOCOP_TODO=0 bundle exec rubocop --parallel --force-exclusion {files} + run: REVEAL_RUBOCOP_TODO=0 bundle exec rubocop --config .rubocop.yml --parallel --force-exclusion {files} sidekiq-queues: tags: backend files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD @@ -134,7 +134,7 @@ auto-fix: tags: backend style files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD glob: '*.{rb,rake}' - run: REVEAL_RUBOCOP_TODO=0 bundle exec rubocop --parallel --autocorrect --force-exclusion {files} + run: REVEAL_RUBOCOP_TODO=0 bundle exec rubocop --config .rubocop.yml --parallel --autocorrect --force-exclusion {files} gettext: tags: backend frontend view haml files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD | while read file;do git diff --unified=1 $(git merge-base origin/master HEAD)..HEAD $file | grep -Fqe '_(' && echo $file;done; true diff --git a/locale/gitlab.pot b/locale/gitlab.pot index d01da0c4ea9..ade7bb076ac 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -42370,6 +42370,9 @@ msgstr "" msgid "ServiceDesk|Cannot create custom email" msgstr "" +msgid "ServiceDesk|Cannot update custom email" +msgstr "" + msgid "ServiceDesk|Custom email address could not be verified." msgstr "" diff --git a/spec/factories/ml/models.rb b/spec/factories/ml/models.rb new file mode 100644 index 00000000000..2d1b29289a5 --- /dev/null +++ b/spec/factories/ml/models.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :ml_models, class: '::Ml::Model' do + sequence(:name) { |n| "model#{n}" } + + project + default_experiment { association :ml_experiments, project_id: project.id, name: name } + end +end diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb index 0a697eaa798..bb7cc3db452 100644 --- a/spec/features/groups/milestone_spec.rb +++ b/spec/features/groups/milestone_spec.rb @@ -27,7 +27,7 @@ RSpec.describe 'Group milestones', feature_category: :groups_and_projects do click_button("Preview") - preview = find('.js-md-preview') + preview = find('.js-vue-md-preview') expect(preview).to have_content('Nothing to preview.') diff --git a/spec/frontend/dropzone_input_spec.js b/spec/frontend/dropzone_input_spec.js index 57debf79c7b..ba4d838e44b 100644 --- a/spec/frontend/dropzone_input_spec.js +++ b/spec/frontend/dropzone_input_spec.js @@ -1,6 +1,5 @@ import MockAdapter from 'axios-mock-adapter'; import $ from 'jquery'; -import htmlNewMilestone from 'test_fixtures/milestones/new-milestone.html'; import mock from 'xhr-mock'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import waitForPromises from 'helpers/wait_for_promises'; @@ -9,6 +8,7 @@ import PasteMarkdownTable from '~/behaviors/markdown/paste_markdown_table'; import dropzoneInput from '~/dropzone_input'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status'; +import htmlNewMilestone from 'test_fixtures_static/textarea.html'; const TEST_FILE = new File([], 'somefile.jpg'); TEST_FILE.upload = {}; diff --git a/spec/frontend/fixtures/milestones.rb b/spec/frontend/fixtures/milestones.rb deleted file mode 100644 index 5e39dcf190a..00000000000 --- a/spec/frontend/fixtures/milestones.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Projects::MilestonesController, '(JavaScript fixtures)', :with_license, feature_category: :team_planning, type: :controller do - include JavaScriptFixturesHelpers - - let_it_be(:user) { create(:user, feed_token: 'feedtoken:coldfeed') } - let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures') } - let_it_be(:project) { create(:project_empty_repo, namespace: namespace, path: 'milestones-project') } - - render_views - - before do - project.add_maintainer(user) - sign_in(user) - end - - after do - remove_repository(project) - end - - it 'milestones/new-milestone.html' do - get :new, params: { - namespace_id: project.namespace.to_param, - project_id: project - } - - expect(response).to be_successful - end - - private - - def render_milestone(milestone) - get :show, params: { - namespace_id: project.namespace.to_param, - project_id: project, - id: milestone.to_param - } - - expect(response).to be_successful - end -end diff --git a/spec/frontend/fixtures/static/textarea.html b/spec/frontend/fixtures/static/textarea.html new file mode 100644 index 00000000000..68d5a0f2d4d --- /dev/null +++ b/spec/frontend/fixtures/static/textarea.html @@ -0,0 +1,27 @@ + + +Document with Textarea +
+
+
+
+ + + + + + +
+
+
+
+ + diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js index 7a96ddc7afc..523d5f855fc 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js @@ -27,11 +27,11 @@ describe('packages_list_row', () => { const defaultProvide = { isGroupPage: false, + canDeletePackages: true, }; const packageWithoutTags = { ...packageData(), project: packageProject(), ...linksData }; const packageWithTags = { ...packageWithoutTags, tags: { nodes: packageTags() } }; - const packageCannotDestroy = { ...packageData(), ...linksData, canDestroy: false }; const findPackageTags = () => wrapper.findComponent(PackageTags); const findDeleteDropdown = () => wrapper.findByTestId('action-delete'); @@ -105,7 +105,9 @@ describe('packages_list_row', () => { describe('delete button', () => { it('does not exist when package cannot be destroyed', () => { - mountComponent({ packageEntity: packageCannotDestroy }); + mountComponent({ + packageEntity: { ...packageWithoutTags, canDestroy: false }, + }); expect(findDeleteDropdown().exists()).toBe(false); }); @@ -168,7 +170,10 @@ describe('packages_list_row', () => { describe('left action template', () => { it('does not render checkbox if not permitted', () => { mountComponent({ - packageEntity: { ...packageWithoutTags, canDestroy: false }, + provide: { + ...defaultProvide, + canDeletePackages: false, + }, }); expect(findBulkDeleteAction().exists()).toBe(false); @@ -248,6 +253,7 @@ describe('packages_list_row', () => { it('if the package is published through CI show the project and author name', () => { mountComponent({ provide: { + ...defaultProvide, isGroupPage: true, }, packageEntity: { ...packageWithoutTags, pipelines: { nodes: packagePipelines() } }, @@ -261,6 +267,7 @@ describe('packages_list_row', () => { it('if the package is published manually dont show project and the author name', () => { mountComponent({ provide: { + ...defaultProvide, isGroupPage: true, }, packageEntity: { ...packageWithoutTags }, diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js index 483b7a9383d..fad8863e3d9 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js @@ -41,6 +41,10 @@ describe('packages_list', () => { groupSettings: defaultPackageGroupSettings, }; + const defaultProvide = { + canDeletePackages: true, + }; + const EmptySlotStub = { name: 'empty-slot-stub', template: '
bar
' }; const findPackagesListLoader = () => wrapper.findComponent(PackagesListLoader); @@ -52,8 +56,9 @@ describe('packages_list', () => { const showMock = jest.fn(); - const mountComponent = (props) => { + const mountComponent = ({ props = {}, provide = defaultProvide } = {}) => { wrapper = shallowMountExtended(PackagesList, { + provide, propsData: { ...defaultProps, ...props, @@ -75,7 +80,7 @@ describe('packages_list', () => { describe('when is loading', () => { beforeEach(() => { - mountComponent({ isLoading: true }); + mountComponent({ props: { isLoading: true } }); }); it('shows skeleton loader', () => { @@ -109,6 +114,7 @@ describe('packages_list', () => { title: '2 packages', items: defaultProps.list, pagination: defaultProps.pageInfo, + hiddenDelete: false, isLoading: false, }); }); @@ -137,6 +143,16 @@ describe('packages_list', () => { }); }); + describe('when the user does not have permission to destroy packages', () => { + beforeEach(() => { + mountComponent({ provide: { canDeletePackages: false } }); + }); + + it('sets the hidden delete prop of registry list to true', () => { + expect(findRegistryList().props('hiddenDelete')).toBe(true); + }); + }); + describe.each` description | finderFunction | deletePayload ${'when the user can destroy the package'} | ${findPackagesListRow} | ${firstPackage} @@ -262,7 +278,7 @@ describe('packages_list', () => { describe('when an error package is present', () => { beforeEach(() => { - mountComponent({ list: [firstPackage, errorPackage] }); + mountComponent({ props: { list: [firstPackage, errorPackage] } }); return nextTick(); }); @@ -290,7 +306,7 @@ describe('packages_list', () => { describe('when the list is empty', () => { beforeEach(() => { - mountComponent({ list: [] }); + mountComponent({ props: { list: [] } }); }); it('show the empty slot', () => { @@ -301,7 +317,7 @@ describe('packages_list', () => { describe('pagination', () => { beforeEach(() => { - mountComponent({ pageInfo: { hasPreviousPage: true } }); + mountComponent({ props: { pageInfo: { hasPreviousPage: true } } }); }); it('emits prev-page events when the prev event is fired', () => { diff --git a/spec/helpers/packages_helper_spec.rb b/spec/helpers/packages_helper_spec.rb index ae8a7f0c14c..6d6b8e4c707 100644 --- a/spec/helpers/packages_helper_spec.rb +++ b/spec/helpers/packages_helper_spec.rb @@ -132,7 +132,6 @@ RSpec.describe PackagesHelper, feature_category: :package_registry do describe '#show_container_registry_settings' do let_it_be(:project) { create(:project) } let_it_be(:user) { create(:user) } - let_it_be(:admin) { create(:admin) } before do allow(helper).to receive(:current_user) { user } @@ -252,4 +251,114 @@ RSpec.describe PackagesHelper, feature_category: :package_registry do end end end + + describe '#can_delete_packages?' do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + before do + allow(helper).to receive(:current_user) { user } + end + + subject { helper.can_delete_packages?(project) } + + context 'with package registry config enabled' do + before do + stub_config(packages: { enabled: true }) + end + + context 'when user has permission' do + before do + allow(Ability).to receive(:allowed?).with(user, :destroy_package, project).and_return(true) + end + + it { is_expected.to be(true) } + end + + context 'when user does not have permission' do + before do + allow(Ability).to receive(:allowed?).with(user, :destroy_package, project).and_return(false) + end + + it { is_expected.to be(false) } + end + end + + context 'with package registry config disabled' do + before do + stub_config(packages: { enabled: false }) + end + + context 'when user has permission' do + before do + allow(Ability).to receive(:allowed?).with(user, :destroy_package, project).and_return(true) + end + + it { is_expected.to be(false) } + end + + context 'when user does not have permission' do + before do + allow(Ability).to receive(:allowed?).with(user, :destroy_package, project).and_return(false) + end + + it { is_expected.to be(false) } + end + end + end + + describe '#can_delete_group_packages?' do + let_it_be(:group) { create(:group) } + let_it_be(:user) { create(:user) } + + before do + allow(helper).to receive(:current_user) { user } + end + + subject { helper.can_delete_group_packages?(group) } + + context 'with package registry config enabled' do + before do + stub_config(packages: { enabled: true }) + end + + context 'when user has permission' do + before do + allow(Ability).to receive(:allowed?).with(user, :destroy_package, group).and_return(true) + end + + it { is_expected.to be(true) } + end + + context 'when user does not have permission' do + before do + allow(Ability).to receive(:allowed?).with(user, :destroy_package, group).and_return(false) + end + + it { is_expected.to be(false) } + end + end + + context 'with package registry config disabled' do + before do + stub_config(packages: { enabled: false }) + end + + context 'when user has permission' do + before do + allow(Ability).to receive(:allowed?).with(user, :destroy_package, group).and_return(true) + end + + it { is_expected.to be(false) } + end + + context 'when user does not have permission' do + before do + allow(Ability).to receive(:allowed?).with(user, :destroy_package, group).and_return(false) + end + + it { is_expected.to be(false) } + end + end + end end diff --git a/spec/models/ml/experiment_spec.rb b/spec/models/ml/experiment_spec.rb index 9738a88b5b8..1ee35d6da03 100644 --- a/spec/models/ml/experiment_spec.rb +++ b/spec/models/ml/experiment_spec.rb @@ -14,6 +14,21 @@ RSpec.describe Ml::Experiment, feature_category: :mlops do it { is_expected.to belong_to(:user) } it { is_expected.to have_many(:candidates) } it { is_expected.to have_many(:metadata) } + it { is_expected.to belong_to(:model).class_name('Ml::Model') } + end + + describe '#destroy' do + it 'allow experiment without model to be destroyed' do + experiment = create(:ml_experiments, project: exp.project) + + expect { experiment.destroy! }.to change { Ml::Experiment.count }.by(-1) + end + + it 'throws error when destroying experiment with model' do + experiment = create(:ml_models, project: exp.project).default_experiment + + expect { experiment.destroy! }.to raise_error(ActiveRecord::ActiveRecordError) + end end describe '.package_name' do diff --git a/spec/models/ml/model_spec.rb b/spec/models/ml/model_spec.rb new file mode 100644 index 00000000000..61466ca3a6a --- /dev/null +++ b/spec/models/ml/model_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ml::Model, feature_category: :mlops do + describe 'associations' do + it { is_expected.to belong_to(:project) } + it { is_expected.to have_one(:default_experiment) } + end + + describe '#valid?' do + using RSpec::Parameterized::TableSyntax + + let_it_be(:project) { create(:project) } + let_it_be(:existing_model) { create(:ml_models, name: 'an_existing_model', project: project) } + let_it_be(:valid_name) { 'a_valid_name' } + let_it_be(:default_experiment) { create(:ml_experiments, name: valid_name, project: project) } + + let(:name) { valid_name } + + subject(:errors) do + m = described_class.new(name: name, project: project, default_experiment: default_experiment) + m.validate + m.errors + end + + it 'validates a valid model version' do + expect(errors).to be_empty + end + + describe 'name' do + where(:ctx, :name) do + 'name is blank' | '' + 'name is not valid package name' | '!!()()' + 'name is too large' | ('a' * 256) + 'name is not unique in the project' | 'an_existing_model' + end + with_them do + it { expect(errors).to include(:name) } + end + end + + describe 'default_experiment' do + context 'when experiment name name is different than model name' do + before do + allow(default_experiment).to receive(:name).and_return("#{name}a") + end + + it { expect(errors).to include(:default_experiment) } + end + + context 'when model version project is different than model project' do + before do + allow(default_experiment).to receive(:project_id).and_return(project.id + 1) + end + + it { expect(errors).to include(:default_experiment) } + end + end + end +end diff --git a/spec/requests/projects/service_desk/custom_email_controller_spec.rb b/spec/requests/projects/service_desk/custom_email_controller_spec.rb new file mode 100644 index 00000000000..8ce238ab99c --- /dev/null +++ b/spec/requests/projects/service_desk/custom_email_controller_spec.rb @@ -0,0 +1,380 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::ServiceDesk::CustomEmailController, feature_category: :service_desk do + let_it_be_with_reload(:project) do + create(:project, :private, service_desk_enabled: true) + end + + let_it_be(:custom_email_path) { project_service_desk_custom_email_path(project, format: :json) } + let_it_be(:user) { create(:user) } + let_it_be(:illegitimite_user) { create(:user) } + + let(:message) { instance_double(Mail::Message) } + let(:error_cannot_create_custom_email) { s_("ServiceDesk|Cannot create custom email") } + let(:error_cannot_update_custom_email) { s_("ServiceDesk|Cannot update custom email") } + let(:error_does_not_exist) { s_('ServiceDesk|Custom email does not exist') } + let(:error_custom_email_exists) { s_('ServiceDesk|Custom email already exists') } + + let(:custom_email_params) do + { + custom_email: 'user@example.com', + smtp_address: 'smtp.example.com', + smtp_port: '587', + smtp_username: 'user@example.com', + smtp_password: 'supersecret' + } + end + + let(:empty_json_response) do + { + "custom_email" => nil, + "custom_email_enabled" => false, + "custom_email_verification_state" => nil, + "custom_email_verification_error" => nil, + "custom_email_smtp_address" => nil, + "error_message" => nil + } + end + + before_all do + project.add_developer(illegitimite_user) + project.add_maintainer(user) + end + + shared_examples 'a json response with empty values' do + it 'returns json response with empty values' do + perform_request + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include(empty_json_response) + end + end + + shared_examples 'a controller that responds with status' do |status| + it "responds with #{status} for GET custom email" do + get custom_email_path + expect(response).to have_gitlab_http_status(status) + end + + it "responds with #{status} for POST custom email" do + post custom_email_path + expect(response).to have_gitlab_http_status(status) + end + + it "responds with #{status} for PUT custom email" do + put custom_email_path + expect(response).to have_gitlab_http_status(status) + end + + it "responds with #{status} for DELETE custom email" do + delete custom_email_path + expect(response).to have_gitlab_http_status(status) + end + end + + shared_examples 'a controller with disabled feature flag with status' do |status| + context 'when feature flag service_desk_custom_email is disabled' do + before do + stub_feature_flags(service_desk_custom_email: false) + end + + it_behaves_like 'a controller that responds with status', status + end + end + + shared_examples 'a deletable resource' do + describe 'DELETE custom email' do + let(:perform_request) { delete custom_email_path } + + it_behaves_like 'a json response with empty values' + end + end + + context 'with legitimate user signed in' do + before do + sign_out(illegitimite_user) + sign_in(user) + end + + # because CustomEmailController check_feature_flag_enabled responds + it_behaves_like 'a controller with disabled feature flag with status', :not_found + + describe 'GET custom email' do + let(:perform_request) { get custom_email_path } + + it_behaves_like 'a json response with empty values' + end + + describe 'POST custom email' do + before do + # We send verification email directly + allow(message).to receive(:deliver) + allow(Notify).to receive(:service_desk_custom_email_verification_email).and_return(message) + end + + it 'adds custom email and kicks of verification' do + post custom_email_path, params: custom_email_params + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include( + "custom_email" => custom_email_params[:custom_email], + "custom_email_enabled" => false, + "custom_email_verification_state" => "started", + "custom_email_verification_error" => nil, + "custom_email_smtp_address" => custom_email_params[:smtp_address], + "error_message" => nil + ) + end + + context 'when custom_email param is not valid' do + it 'does not add custom email' do + post custom_email_path, params: custom_email_params.merge(custom_email: 'useratexample.com') + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response).to include( + empty_json_response.merge("error_message" => error_cannot_create_custom_email) + ) + end + end + + context 'when smtp_password param is not valid' do + it 'does not add custom email' do + post custom_email_path, params: custom_email_params.merge(smtp_password: '2short') + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response).to include( + empty_json_response.merge("error_message" => error_cannot_create_custom_email) + ) + end + end + + context 'when the verification process fails fast' do + before do + # Could not establish connection, invalid host etc. + allow(message).to receive(:deliver).and_raise(SocketError) + end + + it 'adds custom email and kicks of verification and returns verification error state' do + post custom_email_path, params: custom_email_params + + # In terms of "custom email object creation", failing fast on the + # verification is a legit state that we don't treat as an error. + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include( + "custom_email" => custom_email_params[:custom_email], + "custom_email_enabled" => false, + "custom_email_verification_state" => "failed", + "custom_email_verification_error" => "smtp_host_issue", + "custom_email_smtp_address" => custom_email_params[:smtp_address], + "error_message" => nil + ) + end + end + end + + describe 'PUT custom email' do + let(:custom_email_params) { { custom_email_enabled: true } } + + it 'does not update records' do + put custom_email_path, params: custom_email_params + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response).to include( + empty_json_response.merge("error_message" => error_cannot_update_custom_email) + ) + end + end + + describe 'DELETE custom email' do + it 'does not touch any records' do + delete custom_email_path + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response).to include( + empty_json_response.merge("error_message" => error_does_not_exist) + ) + end + end + + context 'when custom email is set up' do + let!(:settings) { create(:service_desk_setting, project: project, custom_email: 'user@example.com') } + let!(:credential) { create(:service_desk_custom_email_credential, project: project) } + + before do + project.reset + end + + context 'and verification started' do + let!(:verification) do + create(:service_desk_custom_email_verification, project: project) + end + + it_behaves_like 'a deletable resource' + + describe 'GET custom email' do + it 'returns custom email in its current state' do + get custom_email_path + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include( + "custom_email" => "user@example.com", + "custom_email_enabled" => false, + "custom_email_verification_state" => "started", + "custom_email_verification_error" => nil, + "custom_email_smtp_address" => "smtp.example.com", + "error_message" => nil + ) + end + end + + describe 'POST custom email' do + it 'returns custom email in its current state' do + post custom_email_path, params: custom_email_params + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response).to include( + "custom_email" => custom_email_params[:custom_email], + "custom_email_enabled" => false, + "custom_email_verification_state" => "started", + "custom_email_verification_error" => nil, + "custom_email_smtp_address" => custom_email_params[:smtp_address], + "error_message" => error_custom_email_exists + ) + end + end + + describe 'PUT custom email' do + let(:custom_email_params) { { custom_email_enabled: true } } + + it 'marks custom email as enabled' do + put custom_email_path, params: custom_email_params + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response).to include( + "custom_email" => "user@example.com", + "custom_email_enabled" => false, + "custom_email_verification_state" => "started", + "custom_email_verification_error" => nil, + "custom_email_smtp_address" => "smtp.example.com", + "error_message" => error_cannot_update_custom_email + ) + end + end + end + + context 'and verification finished' do + let!(:verification) do + create(:service_desk_custom_email_verification, project: project, state: :finished, token: nil) + end + + it_behaves_like 'a deletable resource' + + describe 'GET custom email' do + it 'returns custom email in its current state' do + get custom_email_path + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include( + "custom_email" => "user@example.com", + "custom_email_enabled" => false, + "custom_email_verification_state" => "finished", + "custom_email_verification_error" => nil, + "custom_email_smtp_address" => "smtp.example.com", + "error_message" => nil + ) + end + end + + describe 'PUT custom email' do + let(:custom_email_params) { { custom_email_enabled: true } } + + it 'marks custom email as enabled' do + put custom_email_path, params: custom_email_params + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include( + "custom_email" => "user@example.com", + "custom_email_enabled" => true, + "custom_email_verification_state" => "finished", + "custom_email_verification_error" => nil, + "custom_email_smtp_address" => "smtp.example.com", + "error_message" => nil + ) + end + end + end + + context 'and verification failed' do + let!(:verification) do + create(:service_desk_custom_email_verification, + project: project, + state: :failed, + token: nil, + error: :smtp_host_issue + ) + end + + it_behaves_like 'a deletable resource' + + describe 'GET custom email' do + it 'returns custom email in its current state' do + get custom_email_path + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include( + "custom_email" => "user@example.com", + "custom_email_enabled" => false, + "custom_email_verification_state" => "failed", + "custom_email_verification_error" => "smtp_host_issue", + "custom_email_smtp_address" => "smtp.example.com", + "error_message" => nil + ) + end + end + + describe 'PUT custom email' do + let(:custom_email_params) { { custom_email_enabled: true } } + + it 'does not mark custom email as enabled' do + put custom_email_path, params: custom_email_params + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response).to include( + "custom_email" => "user@example.com", + "custom_email_enabled" => false, + "custom_email_verification_state" => "failed", + "custom_email_verification_error" => "smtp_host_issue", + "custom_email_smtp_address" => "smtp.example.com", + "error_message" => error_cannot_update_custom_email + ) + end + end + end + end + end + + context 'when user is anonymous' do + before do + sign_out(user) + sign_out(illegitimite_user) + end + + # because Projects::ApplicationController :authenticate_user! responds + # with redirect to login page + it_behaves_like 'a controller that responds with status', :found + it_behaves_like 'a controller with disabled feature flag with status', :found + end + + context 'with illegitimate user signed in' do + before do + sign_out(user) + sign_in(illegitimite_user) + end + + it_behaves_like 'a controller that responds with status', :not_found + # because CustomEmailController check_feature_flag_enabled responds + it_behaves_like 'a controller with disabled feature flag with status', :not_found + end +end diff --git a/spec/support/shared_examples/features/milestone_editing_shared_examples.rb b/spec/support/shared_examples/features/milestone_editing_shared_examples.rb index d21bf62ecfa..53498a1bb39 100644 --- a/spec/support/shared_examples/features/milestone_editing_shared_examples.rb +++ b/spec/support/shared_examples/features/milestone_editing_shared_examples.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true RSpec.shared_examples 'milestone handling version conflicts' do - it 'warns about version conflict when milestone has been updated in the background' do + it 'warns about version conflict when milestone has been updated in the background', :js do + wait_for_all_requests + # Update the milestone in the background in order to trigger a version conflict milestone.update!(title: "New title") diff --git a/spec/views/groups/packages/index.html.haml_spec.rb b/spec/views/groups/packages/index.html.haml_spec.rb index 26f6268a224..3c6305d1ed9 100644 --- a/spec/views/groups/packages/index.html.haml_spec.rb +++ b/spec/views/groups/packages/index.html.haml_spec.rb @@ -36,4 +36,22 @@ RSpec.describe 'groups/packages/index.html.haml', feature_category: :package_reg ) end end + + describe 'can_delete_packages' do + it 'without permission sets false' do + allow(view).to receive(:can_delete_group_packages?).and_return(false) + + render + + expect(rendered).to have_selector('[data-can-delete-packages="false"]') + end + + it 'with permission sets true' do + allow(view).to receive(:can_delete_group_packages?).and_return(true) + + render + + expect(rendered).to have_selector('[data-can-delete-packages="true"]') + end + end end diff --git a/spec/views/projects/packages/index.html.haml_spec.rb b/spec/views/projects/packages/index.html.haml_spec.rb index 2557ceb70b3..e59db289ad4 100644 --- a/spec/views/projects/packages/index.html.haml_spec.rb +++ b/spec/views/projects/packages/index.html.haml_spec.rb @@ -36,4 +36,22 @@ RSpec.describe 'projects/packages/packages/index.html.haml', feature_category: : ) end end + + describe 'can_delete_packages' do + it 'without permission sets empty settings path' do + allow(view).to receive(:can_delete_packages?).and_return(false) + + render + + expect(rendered).to have_selector('[data-can-delete-packages="false"]') + end + + it 'with permission sets project settings path' do + allow(view).to receive(:can_delete_packages?).and_return(true) + + render + + expect(rendered).to have_selector('[data-can-delete-packages="true"]') + end + end end diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb index d4c41829127..a716cc4d012 100644 --- a/spec/workers/every_sidekiq_worker_spec.rb +++ b/spec/workers/every_sidekiq_worker_spec.rb @@ -245,6 +245,7 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do 'Geo::RepositoryVerification::Primary::SingleWorker' => false, 'Geo::RepositoryVerification::Secondary::SingleWorker' => false, 'Geo::ReverificationBatchWorker' => 0, + 'Geo::BulkMarkPendingBatchWorker' => 0, 'Geo::Scheduler::Primary::SchedulerWorker' => false, 'Geo::Scheduler::SchedulerWorker' => false, 'Geo::Scheduler::Secondary::SchedulerWorker' => false,