diff options
59 files changed, 785 insertions, 171 deletions
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue b/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue index 13e5d7c3019..71e8cf4f634 100644 --- a/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue +++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue @@ -1,8 +1,16 @@ <script> -import { GlAlert, GlFormGroup, GlFormInputGroup, GlSkeletonLoader, GlSprintf } from '@gitlab/ui'; +import { + GlAlert, + GlFormGroup, + GlFormInputGroup, + GlSkeletonLoader, + GlSprintf, + GlEmptyState, +} from '@gitlab/ui'; import { s__ } from '~/locale'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import TitleArea from '~/vue_shared/components/registry/title_area.vue'; +import ManifestsList from '~/packages_and_registries/dependency_proxy/components/manifests_list.vue'; import { DEPENDENCY_PROXY_SETTINGS_DESCRIPTION, DEPENDENCY_PROXY_DOCS_PATH, @@ -13,15 +21,17 @@ import getDependencyProxyDetailsQuery from '~/packages_and_registries/dependency export default { components: { - GlFormGroup, GlAlert, + GlEmptyState, + GlFormGroup, GlFormInputGroup, + GlSkeletonLoader, GlSprintf, ClipboardButton, TitleArea, - GlSkeletonLoader, + ManifestsList, }, - inject: ['groupPath', 'dependencyProxyAvailable'], + inject: ['groupPath', 'dependencyProxyAvailable', 'noManifestsIllustration'], i18n: { proxyNotAvailableText: s__( 'DependencyProxy|Dependency Proxy feature is limited to public groups for now.', @@ -33,6 +43,7 @@ export default { copyImagePrefixText: s__('DependencyProxy|Copy prefix'), blobCountAndSize: s__('DependencyProxy|Contains %{count} blobs of images (%{size})'), pageTitle: s__('DependencyProxy|Dependency Proxy'), + noManifestTitle: s__('DependencyProxy|There are no images in the cache'), }, data() { return { @@ -46,7 +57,7 @@ export default { return !this.dependencyProxyAvailable; }, variables() { - return { fullPath: this.groupPath, first: GRAPHQL_PAGE_SIZE }; + return this.queryVariables; }, }, }, @@ -62,6 +73,38 @@ export default { dependencyProxyEnabled() { return this.group?.dependencyProxySetting?.enabled; }, + queryVariables() { + return { fullPath: this.groupPath, first: GRAPHQL_PAGE_SIZE }; + }, + pageInfo() { + return this.group.dependencyProxyManifests.pageInfo; + }, + manifests() { + return this.group.dependencyProxyManifests.nodes; + }, + }, + methods: { + fetchNextPage() { + this.fetchMore({ + first: GRAPHQL_PAGE_SIZE, + after: this.pageInfo?.endCursor, + }); + }, + fetchPreviousPage() { + this.fetchMore({ + first: null, + last: GRAPHQL_PAGE_SIZE, + before: this.pageInfo?.startCursor, + }); + }, + fetchMore(variables) { + this.$apollo.queries.group.fetchMore({ + variables: { ...this.queryVariables, ...variables }, + updateQuery(_, { fetchMoreResult }) { + return fetchMoreResult; + }, + }); + }, }, }; </script> @@ -103,6 +146,20 @@ export default { </span> </template> </gl-form-group> + + <manifests-list + v-if="manifests && manifests.length" + :manifests="manifests" + :pagination="pageInfo" + @prev-page="fetchPreviousPage" + @next-page="fetchNextPage" + /> + + <gl-empty-state + v-else + :svg-path="noManifestsIllustration" + :title="$options.i18n.noManifestTitle" + /> </div> <gl-alert v-else :dismissible="false" data-testid="proxy-disabled"> {{ $options.i18n.proxyDisabledText }} diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifests_list.vue b/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifests_list.vue index f3ac017268b..005c8feea3a 100644 --- a/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifests_list.vue +++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifests_list.vue @@ -21,13 +21,18 @@ export default { }, }, i18n: { - listTitle: s__('DependencyProxy|Manifest list'), + listTitle: s__('DependencyProxy|Image list'), + }, + computed: { + showPagination() { + return this.pagination.hasNextPage || this.pagination.hasPreviousPage; + }, }, }; </script> <template> - <div class="gl-mt-5"> + <div class="gl-mt-6"> <h3 class="gl-font-base">{{ $options.i18n.listTitle }}</h3> <div class="gl-border-t-1 gl-border-gray-100 gl-border-t-solid gl-display-flex gl-flex-direction-column" @@ -36,6 +41,7 @@ export default { </div> <div class="gl-display-flex gl-justify-content-center"> <gl-keyset-pagination + v-if="showPagination" v-bind="pagination" class="gl-mt-3" @prev="$emit('prev-page')" diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index b350db0c838..8d71a3dab68 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -43,6 +43,8 @@ const onProjectPathChange = ($projectNameInput, $projectPathInput, hasExistingPr }; const setProjectNamePathHandlers = ($projectNameInput, $projectPathInput) => { + const specialRepo = document.querySelector('.js-user-readme-repo'); + // eslint-disable-next-line @gitlab/no-global-event-off $projectNameInput.off('keyup change').on('keyup change', () => { onProjectNameChange($projectNameInput, $projectPathInput); @@ -54,6 +56,11 @@ const setProjectNamePathHandlers = ($projectNameInput, $projectPathInput) => { $projectPathInput.off('keyup change').on('keyup change', () => { onProjectPathChange($projectNameInput, $projectPathInput, hasUserDefinedProjectName); hasUserDefinedProjectPath = $projectPathInput.val().trim().length > 0; + + specialRepo.classList.toggle( + 'gl-display-none', + $projectPathInput.val() !== $projectPathInput.data('username'), + ); }); }; diff --git a/app/assets/javascripts/security_configuration/components/constants.js b/app/assets/javascripts/security_configuration/components/constants.js index d0a7b0d0b89..9c80506549e 100644 --- a/app/assets/javascripts/security_configuration/components/constants.js +++ b/app/assets/javascripts/security_configuration/components/constants.js @@ -37,10 +37,13 @@ export const SAST_IAC_SHORT_NAME = s__('ciReport|IaC Scanning'); export const SAST_IAC_DESCRIPTION = __( 'Analyze your infrastructure as code configuration files for known vulnerabilities.', ); -export const SAST_IAC_HELP_PATH = helpPagePath('user/application_security/sast/index'); -export const SAST_IAC_CONFIG_HELP_PATH = helpPagePath('user/application_security/sast/index', { - anchor: 'configuration', -}); +export const SAST_IAC_HELP_PATH = helpPagePath('user/application_security/iac_scanning/index'); +export const SAST_IAC_CONFIG_HELP_PATH = helpPagePath( + 'user/application_security/iac_scanning/index', + { + anchor: 'configuration', + }, +); export const DAST_NAME = __('Dynamic Application Security Testing (DAST)'); export const DAST_SHORT_NAME = s__('ciReport|DAST'); diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 8039fac02ec..8644d95b96c 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -98,7 +98,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController # Specs are in spec/requests/self_monitoring_project_spec.rb def create_self_monitoring_project - job_id = SelfMonitoringProjectCreateWorker.perform_async # rubocop:disable CodeReuse/Worker + job_id = SelfMonitoringProjectCreateWorker.with_status.perform_async # rubocop:disable CodeReuse/Worker render status: :accepted, json: { job_id: job_id, @@ -137,7 +137,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController # Specs are in spec/requests/self_monitoring_project_spec.rb def delete_self_monitoring_project - job_id = SelfMonitoringProjectDeleteWorker.perform_async # rubocop:disable CodeReuse/Worker + job_id = SelfMonitoringProjectDeleteWorker.with_status.perform_async # rubocop:disable CodeReuse/Worker render status: :accepted, json: { job_id: job_id, diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb index d1046b7b668..e53e35baac3 100644 --- a/app/helpers/tab_helper.rb +++ b/app/helpers/tab_helper.rb @@ -33,7 +33,6 @@ module TabHelper # :item_active - Overrides the default state focing the "active" css classes (optional). # def gl_tab_link_to(name = nil, options = {}, html_options = {}, &block) - tab_class = 'nav-item' link_classes = %w[nav-link gl-tab-nav-item] active_link_classes = %w[active gl-tab-nav-item-active gl-tab-nav-item-active-indigo] @@ -52,6 +51,8 @@ module TabHelper end html_options = html_options.except(:item_active) + extra_tab_classes = html_options.delete(:tab_class) + tab_class = %w[nav-item].push(*extra_tab_classes) content_tag(:li, class: tab_class, role: 'presentation') do if block_given? @@ -215,6 +216,7 @@ def gl_tab_counter_badge(count, html_options = {}) badge_classes = %w[badge badge-muted badge-pill gl-badge sm gl-tab-counter-badge] content_tag(:span, count, - class: [*html_options[:class], badge_classes].join(' ') + class: [*html_options[:class], badge_classes].join(' '), + data: html_options[:data] ) end diff --git a/app/models/concerns/strip_attribute.rb b/app/models/concerns/strip_attribute.rb index 1c433a3275e..817a4465f91 100644 --- a/app/models/concerns/strip_attribute.rb +++ b/app/models/concerns/strip_attribute.rb @@ -2,7 +2,8 @@ # == Strip Attribute module # -# Contains functionality to clean attributes before validation +# Contains functionality to remove leading and trailing +# whitespace from the attribute before validation # # Usage: # diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index f2f9b527cc0..d220f4b837a 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -662,7 +662,7 @@ class MergeRequest < ApplicationRecord # updates `merge_jid` with the MergeWorker#jid. # This helps tracking enqueued and ongoing merge jobs. def merge_async(user_id, params) - jid = MergeWorker.perform_async(id, user_id, params.to_h) + jid = MergeWorker.with_status.perform_async(id, user_id, params.to_h) update_column(:merge_jid, jid) # merge_ongoing? depends on merge_jid @@ -681,7 +681,7 @@ class MergeRequest < ApplicationRecord # attribute is set *and* that the sidekiq job is still running. So a JID # for a completed RebaseWorker is equivalent to a nil JID. jid = Sidekiq::Worker.skipping_transaction_check do - RebaseWorker.perform_async(id, user_id, skip_ci) + RebaseWorker.with_status.perform_async(id, user_id, skip_ci) end update_column(:rebase_jid, jid) diff --git a/app/models/user.rb b/app/models/user.rb index 2ca7909ebcb..394c64db3bf 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1992,6 +1992,18 @@ class User < ApplicationRecord saved end + def user_project + strong_memoize(:user_project) do + personal_projects.find_by(path: username, visibility_level: Gitlab::VisibilityLevel::PUBLIC) + end + end + + def user_readme + strong_memoize(:user_readme) do + user_project&.repository&.readme + end + end + protected # override, from Devise::Validatable diff --git a/app/models/users_statistics.rb b/app/models/users_statistics.rb index a903541f69a..a314ae8920b 100644 --- a/app/models/users_statistics.rb +++ b/app/models/users_statistics.rb @@ -3,12 +3,6 @@ class UsersStatistics < ApplicationRecord scope :order_created_at_desc, -> { order(created_at: :desc) } - class << self - def latest - order_created_at_desc.first - end - end - def active [ without_groups_and_projects, @@ -26,30 +20,26 @@ class UsersStatistics < ApplicationRecord end class << self - def create_current_stats! - stats_by_role = highest_role_stats + def latest + order_created_at_desc.first + end - create!( - without_groups_and_projects: without_groups_and_projects_stats, - with_highest_role_guest: stats_by_role[:guest], - with_highest_role_reporter: stats_by_role[:reporter], - with_highest_role_developer: stats_by_role[:developer], - with_highest_role_maintainer: stats_by_role[:maintainer], - with_highest_role_owner: stats_by_role[:owner], - bots: bot_stats, - blocked: blocked_stats - ) + def create_current_stats! + create!(highest_role_stats) end private def highest_role_stats { - owner: batch_count_for_access_level(Gitlab::Access::OWNER), - maintainer: batch_count_for_access_level(Gitlab::Access::MAINTAINER), - developer: batch_count_for_access_level(Gitlab::Access::DEVELOPER), - reporter: batch_count_for_access_level(Gitlab::Access::REPORTER), - guest: batch_count_for_access_level(Gitlab::Access::GUEST) + without_groups_and_projects: without_groups_and_projects_stats, + with_highest_role_guest: batch_count_for_access_level(Gitlab::Access::GUEST), + with_highest_role_reporter: batch_count_for_access_level(Gitlab::Access::REPORTER), + with_highest_role_developer: batch_count_for_access_level(Gitlab::Access::DEVELOPER), + with_highest_role_maintainer: batch_count_for_access_level(Gitlab::Access::MAINTAINER), + with_highest_role_owner: batch_count_for_access_level(Gitlab::Access::OWNER), + bots: bot_stats, + blocked: blocked_stats } end diff --git a/app/services/groups/import_export/import_service.rb b/app/services/groups/import_export/import_service.rb index f9db552f743..c8c2124078d 100644 --- a/app/services/groups/import_export/import_service.rb +++ b/app/services/groups/import_export/import_service.rb @@ -14,7 +14,7 @@ module Groups def async_execute group_import_state = GroupImportState.safe_find_or_create_by!(group: group, user: current_user) - jid = GroupImportWorker.perform_async(current_user.id, group.id) + jid = GroupImportWorker.with_status.perform_async(current_user.id, group.id) if jid.present? group_import_state.update!(jid: jid) diff --git a/app/views/admin/dashboard/stats.html.haml b/app/views/admin/dashboard/stats.html.haml index b98d11b734b..e0701812ba3 100644 --- a/app/views/admin/dashboard/stats.html.haml +++ b/app/views/admin/dashboard/stats.html.haml @@ -1,74 +1,75 @@ - page_title s_('AdminArea|Users statistics') -%h3.my-4 +%h3.gl-my-6 = s_('AdminArea|Users statistics') %table.table.gl-text-gray-500 %tr - %td.p-3 + %td.gl-p-5! = s_('AdminArea|Users without a Group and Project') = render_if_exists 'admin/dashboard/included_free_in_license_tooltip' - %td.p-3.text-right - = @users_statistics&.without_groups_and_projects.to_i + %td.gl-text-right{ class: 'gl-p-5!' } + = @users_statistics&.without_groups_and_projects + = render_if_exists 'admin/dashboard/minimal_access_stats_row', users_statistics: @users_statistics %tr - %td.p-3 + %td.gl-p-5! = s_('AdminArea|Users with highest role') %strong = s_('AdminArea|Guest') = render_if_exists 'admin/dashboard/included_free_in_license_tooltip' - %td.p-3.text-right - = @users_statistics&.with_highest_role_guest.to_i + %td.gl-text-right{ class: 'gl-p-5!' } + = @users_statistics&.with_highest_role_guest %tr - %td.p-3 + %td.gl-p-5! = s_('AdminArea|Users with highest role') %strong = s_('AdminArea|Reporter') - %td.p-3.text-right - = @users_statistics&.with_highest_role_reporter.to_i + %td.gl-text-right{ class: 'gl-p-5!' } + = @users_statistics&.with_highest_role_reporter %tr - %td.p-3 + %td.gl-p-5! = s_('AdminArea|Users with highest role') %strong = s_('AdminArea|Developer') - %td.p-3.text-right - = @users_statistics&.with_highest_role_developer.to_i + %td.gl-text-right{ class: 'gl-p-5!' } + = @users_statistics&.with_highest_role_developer %tr - %td.p-3 + %td.gl-p-5! = s_('AdminArea|Users with highest role') %strong = s_('AdminArea|Maintainer') - %td.p-3.text-right - = @users_statistics&.with_highest_role_maintainer.to_i + %td.gl-text-right{ class: 'gl-p-5!' } + = @users_statistics&.with_highest_role_maintainer %tr - %td.p-3 + %td.gl-p-5! = s_('AdminArea|Users with highest role') %strong = s_('AdminArea|Owner') - %td.p-3.text-right - = @users_statistics&.with_highest_role_owner.to_i + %td.gl-text-right{ class: 'gl-p-5!' } + = @users_statistics&.with_highest_role_owner %tr - %td.p-3 + %td.gl-p-5! = s_('AdminArea|Bots') - %td.p-3.text-right - = @users_statistics&.bots.to_i + %td.gl-text-right{ class: 'gl-p-5!' } + = @users_statistics&.bots = render_if_exists 'admin/dashboard/billable_users_row' %tr.bg-gray-light.gl-text-gray-900 - %td.p-3 + %td.gl-p-5! %strong = s_('AdminArea|Active users') - %td.p-3.text-right + %td.gl-text-right{ class: 'gl-p-5!' } %strong - = @users_statistics&.active.to_i + = @users_statistics&.active %tr.bg-gray-light.gl-text-gray-900 - %td.p-3 + %td.gl-p-5! %strong = s_('AdminArea|Blocked users') - %td.p-3.text-right + %td.gl-text-right{ class: 'gl-p-5!' } %strong - = @users_statistics&.blocked.to_i + = @users_statistics&.blocked %tr.bg-gray-light.gl-text-gray-900 - %td.p-3 + %td.gl-p-5! %strong = s_('AdminArea|Total users') - %td.p-3.text-right + %td.gl-text-right{ class: 'gl-p-5!' } %strong - = @users_statistics&.total.to_i + = @users_statistics&.total diff --git a/app/views/groups/dependency_proxies/show.html.haml b/app/views/groups/dependency_proxies/show.html.haml index 83ab97f8e4f..47caec717af 100644 --- a/app/views/groups/dependency_proxies/show.html.haml +++ b/app/views/groups/dependency_proxies/show.html.haml @@ -3,4 +3,5 @@ - dependency_proxy_available = Feature.enabled?(:dependency_proxy_for_private_groups, default_enabled: true) || @group.public? #js-dependency-proxy{ data: { group_path: @group.full_path, - dependency_proxy_available: dependency_proxy_available.to_s } } + dependency_proxy_available: dependency_proxy_available.to_s, + no_manifests_illustration: image_path('illustrations/docker-empty-state.svg') } } diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml index 5e20e21e314..c21240b340c 100644 --- a/app/views/projects/_new_project_fields.html.haml +++ b/app/views/projects/_new_project_fields.html.haml @@ -40,12 +40,17 @@ .form-group.project-path.col-sm-6 = f.label :path, class: 'label-bold' do %span= _("Project slug") - = f.text_field :path, placeholder: "my-awesome-project", class: "form-control gl-form-input", required: true, aria: { required: true } + = f.text_field :path, placeholder: "my-awesome-project", class: "form-control gl-form-input", required: true, aria: { required: true }, data: { username: current_user.username } - if current_user.can_create_group? .form-text.text-muted - link_start_group_path = '<a href="%{path}">' % { path: new_group_path } - project_tip = s_('ProjectsNew|Want to house several dependent projects under the same namespace? %{link_start}Create a group.%{link_end}') % { link_start: link_start_group_path, link_end: '</a>' } = project_tip.html_safe +.gl-alert.gl-alert-success.gl-mb-4.gl-display-none.js-user-readme-repo + = sprite_icon('check-circle', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title') + .gl-alert-body + - help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/profile/index', anchor: 'user-profile-readme') } + = html_escape(_('%{project_path} is a project that you can use to add a README to your GitLab profile. Create a public project and initialize the repository with a README to get started. %{help_link_start}Learn more.%{help_link_end}')) % { project_path: "<strong>#{current_user.username} / #{current_user.username}</strong>".html_safe, help_link_start: help_link_start, help_link_end: '</a>'.html_safe } .form-group = f.label :description, class: 'label-bold' do diff --git a/app/views/users/_overview.html.haml b/app/views/users/_overview.html.haml index 8d33628da41..eb9caa796b5 100644 --- a/app/views/users/_overview.html.haml +++ b/app/views/users/_overview.html.haml @@ -9,6 +9,22 @@ %a.js-retry-load{ href: '#' } = s_('UserProfile|Retry') .user-calendar-activities +- if @user.user_readme + .row + .col-12.gl-my-6 + .gl-border-gray-100.gl-border-1.gl-border-solid.gl-rounded-small.gl-py-4.gl-px-6 + .gl-display-flex + %ol.breadcrumb.gl-breadcrumb-list.gl-mb-4 + %li.breadcrumb-item.gl-breadcrumb-item + = link_to @user.username, project_path(@user.user_project) + %span.gl-breadcrumb-separator + = sprite_icon("chevron-right", size: 16) + %li.breadcrumb-item.gl-breadcrumb-item + = link_to @user.user_readme.path, @user.user_project.readme_url + - if current_user == @user + .gl-ml-auto + = link_to _('Edit'), edit_blob_path(@user.user_project, @user.user_project.default_branch, @user.user_readme.path) + = render 'projects/blob/viewer', viewer: @user.user_readme.rich_viewer, load_async: false .row %div{ class: activity_pane_class } - if can?(current_user, :read_cross_project) diff --git a/app/workers/concerns/application_worker.rb b/app/workers/concerns/application_worker.rb index 1d55cd45840..03a0b5fae00 100644 --- a/app/workers/concerns/application_worker.rb +++ b/app/workers/concerns/application_worker.rb @@ -55,6 +55,12 @@ module ApplicationWorker subclass.after_set_class_attribute { subclass.set_queue } end + def with_status + status_from_class = self.sidekiq_options_hash['status_expiration'] + + set(status_expiration: status_from_class || Gitlab::SidekiqStatus::DEFAULT_EXPIRATION) + end + def generated_queue_name Gitlab::SidekiqConfig::WorkerRouter.queue_name_from_worker_name(self) end diff --git a/app/workers/concerns/limited_capacity/worker.rb b/app/workers/concerns/limited_capacity/worker.rb index b4cdfda680f..bcedb4efcc0 100644 --- a/app/workers/concerns/limited_capacity/worker.rb +++ b/app/workers/concerns/limited_capacity/worker.rb @@ -47,7 +47,7 @@ module LimitedCapacity # would be occupied by a job that will be performed in the distant future. # We let the cron worker enqueue new jobs, this could be seen as our retry and # back off mechanism because the job might fail again if executed immediately. - sidekiq_options retry: 0 + sidekiq_options retry: 0, status_expiration: Gitlab::SidekiqStatus::DEFAULT_EXPIRATION deduplicate :none end diff --git a/config/feature_flags/development/configure_iac_scanning_via_mr.yml b/config/feature_flags/development/configure_iac_scanning_via_mr.yml index 0e3e921058c..cef22644b8f 100644 --- a/config/feature_flags/development/configure_iac_scanning_via_mr.yml +++ b/config/feature_flags/development/configure_iac_scanning_via_mr.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/343966 milestone: '14.5' type: development group: group::static analysis -default_enabled: false +default_enabled: true diff --git a/db/migrate/20211103062728_add_with_highest_role_minimal_access_to_users_statistics.rb b/db/migrate/20211103062728_add_with_highest_role_minimal_access_to_users_statistics.rb new file mode 100644 index 00000000000..43cd7afbf06 --- /dev/null +++ b/db/migrate/20211103062728_add_with_highest_role_minimal_access_to_users_statistics.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddWithHighestRoleMinimalAccessToUsersStatistics < Gitlab::Database::Migration[1.0] + def change + add_column :users_statistics, :with_highest_role_minimal_access, :integer, null: false, default: 0 + end +end diff --git a/db/schema_migrations/20211103062728 b/db/schema_migrations/20211103062728 new file mode 100644 index 00000000000..45bb2fcda65 --- /dev/null +++ b/db/schema_migrations/20211103062728 @@ -0,0 +1 @@ +a22322122144f28306b3b38dbe50b3465ad623c389f8bfe6fa97a0f71b1c7c21
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index f74fa143972..c2bf310d3c6 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -20289,7 +20289,8 @@ CREATE TABLE users_statistics ( with_highest_role_maintainer integer DEFAULT 0 NOT NULL, with_highest_role_owner integer DEFAULT 0 NOT NULL, bots integer DEFAULT 0 NOT NULL, - blocked integer DEFAULT 0 NOT NULL + blocked integer DEFAULT 0 NOT NULL, + with_highest_role_minimal_access integer DEFAULT 0 NOT NULL ); CREATE SEQUENCE users_statistics_id_seq diff --git a/doc/api/dependencies.md b/doc/api/dependencies.md index b421a32b88a..1ecb78aa26d 100644 --- a/doc/api/dependencies.md +++ b/doc/api/dependencies.md @@ -34,7 +34,7 @@ GET /projects/:id/dependencies?package_manager=yarn,bundler | Attribute | Type | Required | Description | | ------------- | -------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding). | -| `package_manager` | string array | no | Returns dependencies belonging to specified package manager. Valid values: `bundler`, `composer`, `conan`, `go`, `maven`, `npm`, `nuget`, `pip`, `yarn`, or `sbt`. | +| `package_manager` | string array | no | Returns dependencies belonging to specified package manager. Valid values: `bundler`, `composer`, `conan`, `go`, `gradle`, `maven`, `npm`, `nuget`, `pip`, `pipenv`, `yarn`, `sbt`, or `setuptools`. | ```shell curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/4/dependencies" diff --git a/doc/ci/yaml/includes.md b/doc/ci/yaml/includes.md index a69a4b31324..1a0afc94eb3 100644 --- a/doc/ci/yaml/includes.md +++ b/doc/ci/yaml/includes.md @@ -269,10 +269,11 @@ see this [CI/CD variable demo](https://youtu.be/4XR8gw3Pkos). > - [Enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/337507) GitLab 14.3. > - [Feature flag `ci_include_rules` removed](https://gitlab.com/gitlab-org/gitlab/-/issues/337507) in GitLab 14.4. > - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/337507) in GitLab 14.4. +> - [Support for `exists` keyword added](https://gitlab.com/gitlab-org/gitlab/-/issues/341511) in GitLab 14.5. You can use [`rules`](index.md#rules) with `include` to conditionally include other configuration files. -You can only use [`if` rules](index.md#rulesif) in `include`, and only with [certain variables](#use-variables-with-include). -`rules` keywords such as `changes` and `exists` are not supported. +You can only use [`if` rules](index.md#rulesif) and [`exists` rules](index.md#rulesexists) in `include`, and only with +[certain variables](#use-variables-with-include). `rules` keyword `changes` is not supported. ```yaml include: diff --git a/doc/user/application_security/iac_scanning/index.md b/doc/user/application_security/iac_scanning/index.md new file mode 100644 index 00000000000..a58a00a869b --- /dev/null +++ b/doc/user/application_security/iac_scanning/index.md @@ -0,0 +1,98 @@ +--- +stage: Secure +group: Static Analysis +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +--- + +# Infrastructure as Code (IaC) Scanning + +> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/6655) in GitLab 14.5. + +Infrastructure as Code (IaC) Scanning scans your IaC configuration files for known vulnerabilities. + +Currently, IaC scanning supports configuration files for Terraform, Ansible, AWS CloudFormation, and Kubernetes. + +## Requirements + +To run IaC scanning jobs, by default, you need GitLab Runner with the +[`docker`](https://docs.gitlab.com/runner/executors/docker.html) or +[`kubernetes`](https://docs.gitlab.com/runner/install/kubernetes.html) executor. +If you're using the shared runners on GitLab.com, this is enabled by default. + +WARNING: +Our IaC scanning jobs require a Linux container type. Windows containers are not yet supported. + +WARNING: +If you use your own runners, make sure the Docker version installed +is **not** `19.03.0`. See [troubleshooting information](../sast/index.md#error-response-from-daemon-error-processing-tar-file-docker-tar-relocation-error) for details. + +## Supported languages and frameworks + +GitLab IaC scanning supports a variety of IaC configuration files. Our IaC security scanners also feature automatic language detection which works even for mixed-language projects. If any supported configuration files are detected in project source code we automatically run the appropriate IaC analyzers. + +| Configuration File Type | Scan tool | Introduced in GitLab Version | +|------------------------------------------|----------------------------------|-------------------------------| +| Ansible | [kics](https://kics.io/) | 14.5 | +| AWS CloudFormation | [kics](https://kics.io/) | 14.5 | +| Kubernetes | [kics](https://kics.io/) | 14.5 | +| Terraform | [kics](https://kics.io/) | 14.5 | + +### Making IaC analyzers available to all GitLab tiers + +All open source (OSS) analyzers are availibile with the GitLab Free tier. Future propietary analyzers may be restricted to higher tiers. + +#### Summary of features per tier + +Different features are available in different [GitLab tiers](https://about.gitlab.com/pricing/), +as shown in the following table: + +| Capability | In Free | In Ultimate | +|:---------------------------------------------------------------------------------------|:--------------------|:-------------------| +| [Configure IaC Scanners](#configuration) v | **{check-circle}** | **{check-circle}** | +| View [JSON Report](#reports-json-format) | **{check-circle}** | **{check-circle}** | +| Presentation of JSON Report in Merge Request | **{dotted-circle}** | **{check-circle}** | +| [Address vulnerabilities](../../application_security/vulnerabilities/index.md) | **{dotted-circle}** | **{check-circle}** | +| [Access to Security Dashboard](../../application_security/security_dashboard/index.md) | **{dotted-circle}** | **{check-circle}** | + +## Contribute your scanner + +The [Security Scanner Integration](../../../development/integrations/secure.md) documentation explains how to integrate other security scanners into GitLab. + +## Configuration + +To configure IaC Scanning for a project you can: + +- [Configure IaC Scanning manually](#configure-iac-scanning-manually) +- [Enable IaC Scanning via an automatic merge request](#enable-iac-scanning-via-an-automatic-merge-request) + +### Configure IaC Scanning manually + +To enable IaC Scanning you must [include](../../../ci/yaml/index.md#includetemplate) the +[`SAST-IaC.latest.gitlab-ci.yml template`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/SAST-IaC.latest.gitlab-ci.yml) provided as part of your GitLab installation. + +The included template creates IaC scanning jobs in your CI/CD pipeline and scans +your project's configuration files for possible vulnerabilities. + +The results are saved as a +[SAST report artifact](../../../ci/yaml/index.md#artifactsreportssast) +that you can download and analyze. + +### Enable IaC Scanning via an automatic merge request + +To enable IaC Scanning in a project, you can create a merge request +from the Security Configuration page: + +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Security & Compliance > Configuration**. +1. In the **Infrastructure as Code (IaC) Scanning** row, select **Configure via Merge Request**. + +This automatically creates a merge request with the changes necessary to enable IaC Scanning +that you can review and merge to complete the configuration. + +## Reports JSON format + +The IaC tool emits a JSON report file in the existing SAST report format. For more information, see the +[schema for this report](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/sast-report-format.json). + +The JSON report file can be downloaded from the CI pipelines page, or the +pipelines tab on merge requests by [setting `artifacts: paths`](../../../ci/yaml/index.md#artifactspaths) to `gl-sast-report.json`. For more information see [Downloading artifacts](../../../ci/pipelines/job_artifacts.md). diff --git a/doc/user/application_security/index.md b/doc/user/application_security/index.md index 35c027af670..91cfeae0c71 100644 --- a/doc/user/application_security/index.md +++ b/doc/user/application_security/index.md @@ -31,19 +31,20 @@ For an overview of GitLab application security, see [Shifting Security Left](htt GitLab uses the following tools to scan and report known vulnerabilities found in your project. -| Secure scanning tool | Description | -|:-----------------------------------------------------------------------------|:-----------------------------------------------------------------------| -| [Container Scanning](container_scanning/index.md) | Scan Docker containers for known vulnerabilities. | -| [Dependency List](dependency_list/index.md) | View your project's dependencies and their known vulnerabilities. | -| [Dependency Scanning](dependency_scanning/index.md) | Analyze your dependencies for known vulnerabilities. | -| [Dynamic Application Security Testing (DAST)](dast/index.md) | Analyze running web applications for known vulnerabilities. | -| [DAST API](dast_api/index.md) | Analyze running web APIs for known vulnerabilities. | -| [API fuzzing](api_fuzzing/index.md) | Find unknown bugs and vulnerabilities in web APIs with fuzzing. | -| [Secret Detection](secret_detection/index.md) | Analyze Git history for leaked secrets. | -| [Security Dashboard](security_dashboard/index.md) | View vulnerabilities in all your projects and groups. | -| [Static Application Security Testing (SAST)](sast/index.md) | Analyze source code for known vulnerabilities. | -| [Coverage fuzzing](coverage_fuzzing/index.md) | Find unknown bugs and vulnerabilities with coverage-guided fuzzing. | -| [Cluster Image Scanning](cluster_image_scanning/index.md) | Scan Kubernetes clusters for known vulnerabilities. | +| Secure scanning tool | Description | +| :------------------------------------------------------------- | :------------------------------------------------------------------ | +| [Container Scanning](container_scanning/index.md) | Scan Docker containers for known vulnerabilities. | +| [Dependency List](dependency_list/index.md) | View your project's dependencies and their known vulnerabilities. | +| [Dependency Scanning](dependency_scanning/index.md) | Analyze your dependencies for known vulnerabilities. | +| [Dynamic Application Security Testing (DAST)](dast/index.md) | Analyze running web applications for known vulnerabilities. | +| [DAST API](dast_api/index.md) | Analyze running web APIs for known vulnerabilities. | +| [API fuzzing](api_fuzzing/index.md) | Find unknown bugs and vulnerabilities in web APIs with fuzzing. | +| [Secret Detection](secret_detection/index.md) | Analyze Git history for leaked secrets. | +| [Security Dashboard](security_dashboard/index.md) | View vulnerabilities in all your projects and groups. | +| [Static Application Security Testing (SAST)](sast/index.md) | Analyze source code for known vulnerabilities. | +| [Infrastructure as Code (IaC) Scanning](iac_scanning/index.md) | Analyze your IaC coniguration files for known vulnerabilities. | +| [Coverage fuzzing](coverage_fuzzing/index.md) | Find unknown bugs and vulnerabilities with coverage-guided fuzzing. | +| [Cluster Image Scanning](cluster_image_scanning/index.md) | Scan Kubernetes clusters for known vulnerabilities. | ## Security scanning with Auto DevOps diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md index 94953a48ba0..47fcf7577cd 100644 --- a/doc/user/profile/index.md +++ b/doc/user/profile/index.md @@ -100,6 +100,18 @@ When visiting the public page of a user, you can only see the projects which you If the [public level is restricted](../admin_area/settings/visibility_and_access_controls.md#restrict-visibility-levels), user profiles are only visible to signed-in users. +## User profile README + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/232157) in GitLab 14.5. + +You can add a README section to your profile that can include more information and formatting than +your profile's bio. + +To add a README to your profile: + +1. Create a new public project with the same name as your GitLab username. +1. Create a README file inside this project. The file can be any valid [README or index file](../project/repository/index.md#readme-and-index-files). + ## Add external accounts to your user profile page You can add links to certain other external accounts you might have, like Skype and Twitter. diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md index b2b656ac243..e2ce8c12185 100644 --- a/doc/user/project/settings/index.md +++ b/doc/user/project/settings/index.md @@ -177,7 +177,9 @@ audit trail: include: # Execute individual project's configuration (if project contains .gitlab-ci.yml) project: '$CI_PROJECT_PATH' file: '$CI_CONFIG_PATH' - ref: '$CI_COMMIT_REF_NAME' # Must be defined or MR pipelines always use the use default branch. + ref: '$CI_COMMIT_REF_NAME' # Must be defined or MR pipelines always use the use default branch + rules: + - exists: '$CI_CONFIG_PATH' ``` ##### Ensure compliance jobs are always run diff --git a/lib/api/terraform/modules/v1/packages.rb b/lib/api/terraform/modules/v1/packages.rb index aa59b6a4fee..ad5a4ae7ea6 100644 --- a/lib/api/terraform/modules/v1/packages.rb +++ b/lib/api/terraform/modules/v1/packages.rb @@ -46,7 +46,8 @@ module API def finder_params { package_type: :terraform_module, - package_name: "#{params[:module_name]}/#{params[:module_system]}" + package_name: "#{params[:module_name]}/#{params[:module_system]}", + exact_name: true }.tap do |finder_params| finder_params[:package_version] = params[:module_version] if params.has_key?(:module_version) end diff --git a/lib/gitlab/ci/build/context/base.rb b/lib/gitlab/ci/build/context/base.rb index 02b97ea76e9..c7ea7c78e2f 100644 --- a/lib/gitlab/ci/build/context/base.rb +++ b/lib/gitlab/ci/build/context/base.rb @@ -5,6 +5,8 @@ module Gitlab module Build module Context class Base + include Gitlab::Utils::StrongMemoize + attr_reader :pipeline def initialize(pipeline) @@ -15,6 +17,26 @@ module Gitlab raise NotImplementedError end + def project + pipeline.project + end + + def sha + pipeline.sha + end + + def top_level_worktree_paths + strong_memoize(:top_level_worktree_paths) do + project.repository.tree(sha).blobs.map(&:path) + end + end + + def all_worktree_paths + strong_memoize(:all_worktree_paths) do + project.repository.ls_files(sha) + end + end + protected def pipeline_attributes diff --git a/lib/gitlab/ci/build/rules/rule/clause/exists.rb b/lib/gitlab/ci/build/rules/rule/clause/exists.rb index 85e77438f51..e2b54797dc8 100644 --- a/lib/gitlab/ci/build/rules/rule/clause/exists.rb +++ b/lib/gitlab/ci/build/rules/rule/clause/exists.rb @@ -15,19 +15,21 @@ module Gitlab @exact_globs, @pattern_globs = globs.partition(&method(:exact_glob?)) end - def satisfied_by?(pipeline, context) - paths = worktree_paths(pipeline) + def satisfied_by?(_pipeline, context) + paths = worktree_paths(context) exact_matches?(paths) || pattern_matches?(paths) end private - def worktree_paths(pipeline) + def worktree_paths(context) + return unless context.project + if @top_level_only - pipeline.top_level_worktree_paths + context.top_level_worktree_paths else - pipeline.all_worktree_paths + context.all_worktree_paths end end diff --git a/lib/gitlab/ci/config/entry/include/rules/rule.rb b/lib/gitlab/ci/config/entry/include/rules/rule.rb index d3d0f098814..fa99a7204d6 100644 --- a/lib/gitlab/ci/config/entry/include/rules/rule.rb +++ b/lib/gitlab/ci/config/entry/include/rules/rule.rb @@ -9,9 +9,9 @@ module Gitlab include ::Gitlab::Config::Entry::Validatable include ::Gitlab::Config::Entry::Attributable - ALLOWED_KEYS = %i[if].freeze + ALLOWED_KEYS = %i[if exists].freeze - attributes :if + attributes :if, :exists validations do validates :config, presence: true diff --git a/lib/gitlab/ci/config/external/context.rb b/lib/gitlab/ci/config/external/context.rb index e0adb1b19c2..51624dc30ea 100644 --- a/lib/gitlab/ci/config/external/context.rb +++ b/lib/gitlab/ci/config/external/context.rb @@ -5,6 +5,8 @@ module Gitlab class Config module External class Context + include Gitlab::Utils::StrongMemoize + TimeoutError = Class.new(StandardError) attr_reader :project, :sha, :user, :parent_pipeline, :variables @@ -22,6 +24,18 @@ module Gitlab yield self if block_given? end + def top_level_worktree_paths + strong_memoize(:top_level_worktree_paths) do + project.repository.tree(sha).blobs.map(&:path) + end + end + + def all_worktree_paths + strong_memoize(:all_worktree_paths) do + project.repository.ls_files(sha) + end + end + def mutate(attrs = {}) self.class.new(**attrs) do |ctx| ctx.expandset = expandset diff --git a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml index aa7b394a13c..197ce2438e6 100644 --- a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml @@ -74,6 +74,9 @@ gemnasium-maven-dependency_scanning: # override the analyzer image with a custom value. This may be subject to change or # breakage across GitLab releases. DS_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/gemnasium-maven:$DS_MAJOR_VERSION" + # Stop reporting Gradle as "maven". + # See https://gitlab.com/gitlab-org/gitlab/-/issues/338252 + DS_REPORT_PACKAGE_MANAGER_MAVEN_WHEN_JAVA: "false" rules: - if: $DEPENDENCY_SCANNING_DISABLED when: never @@ -97,6 +100,9 @@ gemnasium-python-dependency_scanning: # override the analyzer image with a custom value. This may be subject to change or # breakage across GitLab releases. DS_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/gemnasium-python:$DS_MAJOR_VERSION" + # Stop reporting Pipenv and Setuptools as "pip". + # See https://gitlab.com/gitlab-org/gitlab/-/issues/338252 + DS_REPORT_PACKAGE_MANAGER_PIP_WHEN_PYTHON: "false" rules: - if: $DEPENDENCY_SCANNING_DISABLED when: never diff --git a/lib/gitlab/sidekiq_status.rb b/lib/gitlab/sidekiq_status.rb index 623fdd89456..fbf2718d718 100644 --- a/lib/gitlab/sidekiq_status.rb +++ b/lib/gitlab/sidekiq_status.rb @@ -7,12 +7,16 @@ module Gitlab # To check if a job has been completed, simply pass the job ID to the # `completed?` method: # - # job_id = SomeWorker.perform_async(...) + # job_id = SomeWorker.with_status.perform_async(...) # # if Gitlab::SidekiqStatus.completed?(job_id) # ... # end # + # If you do not use `with_status`, and the worker class does not declare + # `status_expiration` in its `sidekiq_options`, then this status will not be + # stored. + # # For each job ID registered a separate key is stored in Redis, making lookups # much faster than using Sidekiq's built-in job finding/status API. These keys # expire after a certain period of time to prevent storing too many keys in diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 25673097f38..9fa1f64f025 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -815,6 +815,9 @@ msgstr "" msgid "%{primary} (%{secondary})" msgstr "" +msgid "%{project_path} is a project that you can use to add a README to your GitLab profile. Create a public project and initialize the repository with a README to get started. %{help_link_start}Learn more.%{help_link_end}" +msgstr "" + msgid "%{ref} cannot be added: %{error}" msgstr "" @@ -2313,6 +2316,9 @@ msgstr "" msgid "AdminArea|Maintainer" msgstr "" +msgid "AdminArea|Minimal access" +msgstr "" + msgid "AdminArea|New group" msgstr "" @@ -11337,7 +11343,10 @@ msgstr "" msgid "DependencyProxy|Enable Proxy" msgstr "" -msgid "DependencyProxy|Manifest list" +msgid "DependencyProxy|Image list" +msgstr "" + +msgid "DependencyProxy|There are no images in the cache" msgstr "" msgid "Depends on %d merge request being merged" @@ -18547,12 +18556,18 @@ msgstr "" msgid "Integrations|Default settings are inherited from the instance level." msgstr "" +msgid "Integrations|Edit project alias" +msgstr "" + msgid "Integrations|Enable GitLab.com slash commands in a Slack workspace." msgstr "" msgid "Integrations|Enable comments" msgstr "" +msgid "Integrations|Enter your alias" +msgstr "" + msgid "Integrations|Failed to link namespace. Please try again." msgstr "" @@ -18670,6 +18685,9 @@ msgstr "" msgid "Integrations|You can now close this window and return to the GitLab for Jira application." msgstr "" +msgid "Integrations|You can use this alias in your Slack commands" +msgstr "" + msgid "Integrations|You haven't activated any integrations yet." msgstr "" diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index e2fc867db44..46b332a8938 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -10,7 +10,7 @@ RSpec.describe Projects::MergeRequestsController do let_it_be_with_reload(:project_public_with_private_builds) { create(:project, :repository, :public, :builds_private) } let(:user) { project.owner } - let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: merge_request_source_project, allow_maintainer_to_push: false) } + let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: merge_request_source_project, allow_collaboration: false) } let(:merge_request_source_project) { project } before do @@ -507,6 +507,7 @@ RSpec.describe Projects::MergeRequestsController do end it 'starts the merge immediately with permitted params' do + allow(MergeWorker).to receive(:with_status).and_return(MergeWorker) expect(MergeWorker).to receive(:perform_async).with(merge_request.id, anything, { 'sha' => merge_request.diff_head_sha }) merge_with_sha @@ -2078,6 +2079,10 @@ RSpec.describe Projects::MergeRequestsController do post :rebase, params: { namespace_id: project.namespace, project_id: project, id: merge_request } end + before do + allow(RebaseWorker).to receive(:with_status).and_return(RebaseWorker) + end + def expect_rebase_worker_for(user) expect(RebaseWorker).to receive(:perform_async).with(merge_request.id, user.id, false) end diff --git a/spec/factories/user_highest_roles.rb b/spec/factories/user_highest_roles.rb index 761a8b6c583..ee5794b55fb 100644 --- a/spec/factories/user_highest_roles.rb +++ b/spec/factories/user_highest_roles.rb @@ -5,10 +5,10 @@ FactoryBot.define do highest_access_level { nil } user - trait(:guest) { highest_access_level { GroupMember::GUEST } } - trait(:reporter) { highest_access_level { GroupMember::REPORTER } } - trait(:developer) { highest_access_level { GroupMember::DEVELOPER } } - trait(:maintainer) { highest_access_level { GroupMember::MAINTAINER } } - trait(:owner) { highest_access_level { GroupMember::OWNER } } + trait(:guest) { highest_access_level { GroupMember::GUEST } } + trait(:reporter) { highest_access_level { GroupMember::REPORTER } } + trait(:developer) { highest_access_level { GroupMember::DEVELOPER } } + trait(:maintainer) { highest_access_level { GroupMember::MAINTAINER } } + trait(:owner) { highest_access_level { GroupMember::OWNER } } end end diff --git a/spec/features/profiles/user_visits_profile_spec.rb b/spec/features/profiles/user_visits_profile_spec.rb index 475fda5e7a1..273d52996d3 100644 --- a/spec/features/profiles/user_visits_profile_spec.rb +++ b/spec/features/profiles/user_visits_profile_spec.rb @@ -21,6 +21,14 @@ RSpec.describe 'User visits their profile' do expect(page).to have_content "This information will appear on your profile" end + it 'shows user readme' do + create(:project, :repository, :public, path: user.username, namespace: user.namespace) + + visit(user_path(user)) + + expect(find('.file-content')).to have_content('testme') + end + context 'when user has groups' do let(:group) do create :group do |group| diff --git a/spec/frontend/members/mock_data.js b/spec/frontend/members/mock_data.js index f42ee295511..218db0b587a 100644 --- a/spec/frontend/members/mock_data.js +++ b/spec/frontend/members/mock_data.js @@ -39,7 +39,7 @@ export const member = { Developer: 30, Maintainer: 40, Owner: 50, - 'Minimal Access': 5, + 'Minimal access': 5, }, }; diff --git a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js index 1f0252965b0..625f00a8666 100644 --- a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js +++ b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js @@ -1,32 +1,40 @@ -import { GlFormInputGroup, GlFormGroup, GlSkeletonLoader, GlSprintf } from '@gitlab/ui'; +import { + GlFormInputGroup, + GlFormGroup, + GlSkeletonLoader, + GlSprintf, + GlEmptyState, +} from '@gitlab/ui'; import { createLocalVue } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { stripTypenames } from 'helpers/graphql_helpers'; import waitForPromises from 'helpers/wait_for_promises'; +import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/dependency_proxy/constants'; import DependencyProxyApp from '~/packages_and_registries/dependency_proxy/app.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import ManifestsList from '~/packages_and_registries/dependency_proxy/components/manifests_list.vue'; import getDependencyProxyDetailsQuery from '~/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql'; -import { proxyDetailsQuery, proxyData } from './mock_data'; +import { proxyDetailsQuery, proxyData, pagination, proxyManifests } from './mock_data'; const localVue = createLocalVue(); describe('DependencyProxyApp', () => { let wrapper; let apolloProvider; + let resolver; const provideDefaults = { groupPath: 'gitlab-org', dependencyProxyAvailable: true, + noManifestsIllustration: 'noManifestsIllustration', }; - function createComponent({ - provide = provideDefaults, - resolver = jest.fn().mockResolvedValue(proxyDetailsQuery()), - } = {}) { + function createComponent({ provide = provideDefaults } = {}) { localVue.use(VueApollo); const requestHandlers = [[getDependencyProxyDetailsQuery, resolver]]; @@ -53,6 +61,12 @@ describe('DependencyProxyApp', () => { const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); const findMainArea = () => wrapper.findByTestId('main-area'); const findProxyCountText = () => wrapper.findByTestId('proxy-count'); + const findManifestList = () => wrapper.findComponent(ManifestsList); + const findEmptyState = () => wrapper.findComponent(GlEmptyState); + + beforeEach(() => { + resolver = jest.fn().mockResolvedValue(proxyDetailsQuery()); + }); afterEach(() => { wrapper.destroy(); @@ -78,8 +92,8 @@ describe('DependencyProxyApp', () => { }); it('does not call the graphql endpoint', async () => { - const resolver = jest.fn().mockResolvedValue(proxyDetailsQuery()); - createComponent({ ...createComponentArguments, resolver }); + resolver = jest.fn().mockResolvedValue(proxyDetailsQuery()); + createComponent({ ...createComponentArguments }); await waitForPromises(); @@ -145,14 +159,73 @@ describe('DependencyProxyApp', () => { it('from group has a description with proxy count', () => { expect(findProxyCountText().text()).toBe('Contains 2 blobs of images (1024 Bytes)'); }); + + describe('manifest lists', () => { + describe('when there are no manifests', () => { + beforeEach(() => { + resolver = jest.fn().mockResolvedValue( + proxyDetailsQuery({ + extend: { dependencyProxyManifests: { nodes: [], pageInfo: pagination() } }, + }), + ); + createComponent(); + return waitForPromises(); + }); + + it('shows the empty state message', () => { + expect(findEmptyState().props()).toMatchObject({ + svgPath: provideDefaults.noManifestsIllustration, + title: DependencyProxyApp.i18n.noManifestTitle, + }); + }); + + it('hides the list', () => { + expect(findManifestList().exists()).toBe(false); + }); + }); + + describe('when there are manifests', () => { + it('hides the empty state message', () => { + expect(findEmptyState().exists()).toBe(false); + }); + + it('shows list', () => { + expect(findManifestList().props()).toMatchObject({ + manifests: proxyManifests(), + pagination: stripTypenames(pagination()), + }); + }); + + it('prev-page event on list fetches the previous page', () => { + findManifestList().vm.$emit('prev-page'); + + expect(resolver).toHaveBeenCalledWith({ + before: pagination().startCursor, + first: null, + fullPath: provideDefaults.groupPath, + last: GRAPHQL_PAGE_SIZE, + }); + }); + + it('next-page event on list fetches the next page', () => { + findManifestList().vm.$emit('next-page'); + + expect(resolver).toHaveBeenCalledWith({ + after: pagination().endCursor, + first: GRAPHQL_PAGE_SIZE, + fullPath: provideDefaults.groupPath, + }); + }); + }); + }); }); + describe('when the dependency proxy is disabled', () => { beforeEach(() => { - createComponent({ - resolver: jest - .fn() - .mockResolvedValue(proxyDetailsQuery({ extendSettings: { enabled: false } })), - }); + resolver = jest + .fn() + .mockResolvedValue(proxyDetailsQuery({ extendSettings: { enabled: false } })); + createComponent(); return waitForPromises(); }); diff --git a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js index 2c3b4e96098..9e4c747a1bd 100644 --- a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js +++ b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js @@ -26,42 +26,56 @@ describe('Manifests List', () => { const findRows = () => wrapper.findAllComponents(ManifestRow); const findPagination = () => wrapper.findComponent(GlKeysetPagination); - beforeEach(() => { - createComponent(); - }); - afterEach(() => { wrapper.destroy(); }); it('has the correct title', () => { + createComponent(); + expect(wrapper.text()).toContain(Component.i18n.listTitle); }); it('shows a row for every manifest', () => { + createComponent(); + expect(findRows().length).toBe(defaultProps.manifests.length); }); it('binds a manifest to each row', () => { + createComponent(); + expect(findRows().at(0).props()).toMatchObject({ manifest: defaultProps.manifests[0], }); }); describe('pagination', () => { + it('is hidden when there is no next or prev pages', () => { + createComponent({ ...defaultProps, pagination: {} }); + + expect(findPagination().exists()).toBe(false); + }); + it('has the correct props', () => { + createComponent(); + expect(findPagination().props()).toMatchObject({ ...defaultProps.pagination, }); }); it('emits the next-page event', () => { + createComponent(); + findPagination().vm.$emit('next'); expect(wrapper.emitted('next-page')).toEqual([[]]); }); it('emits the prev-page event', () => { + createComponent(); + findPagination().vm.$emit('prev'); expect(wrapper.emitted('prev-page')).toEqual([[]]); diff --git a/spec/frontend/packages_and_registries/dependency_proxy/mock_data.js b/spec/frontend/packages_and_registries/dependency_proxy/mock_data.js index 119ec995462..8bad22b5287 100644 --- a/spec/frontend/packages_and_registries/dependency_proxy/mock_data.js +++ b/spec/frontend/packages_and_registries/dependency_proxy/mock_data.js @@ -21,7 +21,7 @@ export const pagination = (extend) => ({ ...extend, }); -export const proxyDetailsQuery = ({ extendSettings = {} } = {}) => ({ +export const proxyDetailsQuery = ({ extendSettings = {}, extend } = {}) => ({ data: { group: { ...proxyData(), @@ -34,6 +34,7 @@ export const proxyDetailsQuery = ({ extendSettings = {} } = {}) => ({ nodes: proxyManifests(), pageInfo: pagination(), }, + ...extend, }, }, }); diff --git a/spec/helpers/tab_helper_spec.rb b/spec/helpers/tab_helper_spec.rb index 7286aa288cf..e5e88466946 100644 --- a/spec/helpers/tab_helper_spec.rb +++ b/spec/helpers/tab_helper_spec.rb @@ -36,7 +36,15 @@ RSpec.describe TabHelper do expect(gl_tab_link_to('/url') { 'block content' }).to match(/block content/) end - it 'creates a tab with custom classes' do + it 'creates a tab with custom classes for enclosing list item without content block provided' do + expect(gl_tab_link_to('Link', '/url', { tab_class: 'my-class' })).to match(/<li class=".*my-class.*"/) + end + + it 'creates a tab with custom classes for enclosing list item with content block provided' do + expect(gl_tab_link_to('/url', { tab_class: 'my-class' }) { 'Link' }).to match(/<li class=".*my-class.*"/) + end + + it 'creates a tab with custom classes for anchor element' do expect(gl_tab_link_to('Link', '/url', { class: 'my-class' })).to match(/<a class=".*my-class.*"/) end @@ -161,5 +169,11 @@ RSpec.describe TabHelper do expect(gl_tab_counter_badge(1, { class: 'js-test' })).to eq('<span class="js-test badge badge-muted badge-pill gl-badge sm gl-tab-counter-badge">1</span>') end end + + context 'with data attributes' do + it 'creates a tab counter badge with the data attributes' do + expect(gl_tab_counter_badge(1, { data: { some_attribute: 'foo' } })).to eq('<span class="badge badge-muted badge-pill gl-badge sm gl-tab-counter-badge" data-some-attribute="foo">1</span>') + end + end end end diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb index 86dd5569a96..f192862c1c4 100644 --- a/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb +++ b/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb @@ -3,10 +3,8 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists do - describe '#satisfied_by?' do - let(:pipeline) { build(:ci_pipeline, project: project, sha: project.repository.head_commit.sha) } - - subject { described_class.new(globs).satisfied_by?(pipeline, nil) } + shared_examples 'an exists rule with a context' do + subject { described_class.new(globs).satisfied_by?(pipeline, context) } it_behaves_like 'a glob matching rule' do let(:project) { create(:project, :custom_repo, files: files) } @@ -24,4 +22,26 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists do it { is_expected.to be_truthy } end end + + describe '#satisfied_by?' do + let(:pipeline) { build(:ci_pipeline, project: project, sha: project.repository.head_commit.sha) } + + context 'when context is Build::Context::Build' do + it_behaves_like 'an exists rule with a context' do + let(:context) { Gitlab::Ci::Build::Context::Build.new(pipeline, sha: 'abc1234') } + end + end + + context 'when context is Build::Context::Global' do + it_behaves_like 'an exists rule with a context' do + let(:context) { Gitlab::Ci::Build::Context::Global.new(pipeline, yaml_variables: {}) } + end + end + + context 'when context is Config::External::Context' do + it_behaves_like 'an exists rule with a context' do + let(:context) { Gitlab::Ci::Config::External::Context.new(project: project, sha: project.repository.tree.sha) } + end + end + end end diff --git a/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb b/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb index b99048e2c18..0505b17ea91 100644 --- a/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb @@ -5,7 +5,7 @@ require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules::Rule do let(:factory) do Gitlab::Config::Entry::Factory.new(described_class) - .value(config) + .value(config) end subject(:entry) { factory.create! } @@ -25,6 +25,12 @@ RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules::Rule do it { is_expected.to be_valid } end + context 'when specifying an exists: clause' do + let(:config) { { exists: './this.md' } } + + it { is_expected.to be_valid } + end + context 'using a list of multiple expressions' do let(:config) { { if: ['$MY_VAR == "this"', '$YOUR_VAR == "that"'] } } @@ -86,5 +92,13 @@ RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules::Rule do expect(subject).to eq(if: '$THIS || $THAT') end end + + context 'when specifying an exists: clause' do + let(:config) { { exists: './test.md' } } + + it 'returns the config' do + expect(subject).to eq(exists: './test.md') + end + end end end diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb index c2f28253f54..2e9e6f95071 100644 --- a/spec/lib/gitlab/ci/config/external/processor_spec.rb +++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb @@ -406,7 +406,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do context 'when rules defined' do context 'when a rule is invalid' do let(:values) do - { include: [{ local: 'builds.yml', rules: [{ exists: ['$MY_VAR'] }] }] } + { include: [{ local: 'builds.yml', rules: [{ changes: ['$MY_VAR'] }] }] } end it 'raises IncludeError' do diff --git a/spec/lib/gitlab/ci/config/external/rules_spec.rb b/spec/lib/gitlab/ci/config/external/rules_spec.rb index 9a5c29befa2..1e42cb30ae7 100644 --- a/spec/lib/gitlab/ci/config/external/rules_spec.rb +++ b/spec/lib/gitlab/ci/config/external/rules_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'fast_spec_helper' +require 'spec_helper' RSpec.describe Gitlab::Ci::Config::External::Rules do let(:rule_hashes) {} @@ -32,6 +32,26 @@ RSpec.describe Gitlab::Ci::Config::External::Rules do end end + context 'when there is a rule with exists' do + let(:project) { create(:project, :repository) } + let(:context) { double(project: project, sha: project.repository.tree.sha, top_level_worktree_paths: ['test.md']) } + let(:rule_hashes) { [{ exists: 'Dockerfile' }] } + + context 'when the file does not exist' do + it { is_expected.to eq(false) } + end + + context 'when the file exists' do + let(:context) { double(project: project, sha: project.repository.tree.sha, top_level_worktree_paths: ['Dockerfile']) } + + before do + project.repository.create_file(project.owner, 'Dockerfile', "commit", message: 'test', branch_name: "master") + end + + it { is_expected.to eq(true) } + end + end + context 'when there is a rule with if and when' do let(:rule_hashes) { [{ if: '$MY_VAR == "hello"', when: 'on_success' }] } @@ -41,12 +61,12 @@ RSpec.describe Gitlab::Ci::Config::External::Rules do end end - context 'when there is a rule with exists' do - let(:rule_hashes) { [{ exists: ['$MY_VAR'] }] } + context 'when there is a rule with changes' do + let(:rule_hashes) { [{ changes: ['$MY_VAR'] }] } it 'raises an error' do expect { result }.to raise_error(described_class::InvalidIncludeRulesError, - 'invalid include rule: {:exists=>["$MY_VAR"]}') + 'invalid include rule: {:changes=>["$MY_VAR"]}') end end end diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb index ebfa2388152..1b3e8a2ce4a 100644 --- a/spec/lib/gitlab/ci/config_spec.rb +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -597,7 +597,7 @@ RSpec.describe Gitlab::Ci::Config do job1: { script: ["echo 'hello from main file'"], variables: { - VARIABLE_DEFINED_IN_MAIN_FILE: 'some value' + VARIABLE_DEFINED_IN_MAIN_FILE: 'some value' } } }) @@ -727,30 +727,70 @@ RSpec.describe Gitlab::Ci::Config do end end - context "when an 'include' has rules with a project variable" do - let(:gitlab_ci_yml) do - <<~HEREDOC - include: - - local: #{local_location} - rules: - - if: $CI_PROJECT_ID == "#{project_id}" - image: ruby:2.7 - HEREDOC - end + context "when an 'include' has rules" do + context "when the rule is an if" do + let(:gitlab_ci_yml) do + <<~HEREDOC + include: + - local: #{local_location} + rules: + - if: $CI_PROJECT_ID == "#{project_id}" + image: ruby:2.7 + HEREDOC + end - context 'when the rules condition is satisfied' do - let(:project_id) { project.id } + context 'when the rules condition is satisfied' do + let(:project_id) { project.id } - it 'includes the file' do - expect(config.to_hash).to include(local_location_hash) + it 'includes the file' do + expect(config.to_hash).to include(local_location_hash) + end + end + + context 'when the rules condition is satisfied' do + let(:project_id) { non_existing_record_id } + + it 'does not include the file' do + expect(config.to_hash).not_to include(local_location_hash) + end end end - context 'when the rules condition is satisfied' do - let(:project_id) { non_existing_record_id } + context "when the rule is an exists" do + let(:gitlab_ci_yml) do + <<~HEREDOC + include: + - local: #{local_location} + rules: + - exists: "#{filename}" + image: ruby:2.7 + HEREDOC + end - it 'does not include the file' do - expect(config.to_hash).not_to include(local_location_hash) + before do + project.repository.create_file( + project.creator, + 'my_builds.yml', + local_file_content, + message: 'Add my_builds.yml', + branch_name: '12345' + ) + end + + context 'when the exists file does not exist' do + let(:filename) { 'not_a_real_file.md' } + + it 'does not include the file' do + expect(config.to_hash).not_to include(local_location_hash) + end + end + + context 'when the exists file does exist' do + let(:filename) { 'my_builds.yml' } + + it 'does include the file' do + expect(config.to_hash).to include(local_location_hash) + end end end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 22d54177f23..5618fb06157 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -2920,6 +2920,8 @@ RSpec.describe MergeRequest, factory_default: :keep do params = {} merge_jid = 'hash-123' + allow(MergeWorker).to receive(:with_status).and_return(MergeWorker) + expect(merge_request).to receive(:expire_etag_cache) expect(MergeWorker).to receive(:perform_async).with(merge_request.id, user_id, params) do merge_jid @@ -2938,6 +2940,10 @@ RSpec.describe MergeRequest, factory_default: :keep do subject(:execute) { merge_request.rebase_async(user_id) } + before do + allow(RebaseWorker).to receive(:with_status).and_return(RebaseWorker) + end + it 'atomically enqueues a RebaseWorker job and updates rebase_jid' do expect(RebaseWorker) .to receive(:perform_async) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 2921d94c343..eee5525a3bd 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -6225,4 +6225,31 @@ RSpec.describe User do expect(described_class.get_ids_by_username([user_name])).to match_array([user_id]) end end + + describe 'user_project' do + it 'returns users project matched by username and public visibility' do + user = create(:user) + public_project = create(:project, :public, path: user.username, namespace: user.namespace) + create(:project, namespace: user.namespace) + + expect(user.user_project).to eq(public_project) + end + end + + describe 'user_readme' do + it 'returns readme from user project' do + user = create(:user) + create(:project, :repository, :public, path: user.username, namespace: user.namespace) + + expect(user.user_readme.name).to eq('README.md') + expect(user.user_readme.data).to include('testme') + end + + it 'returns nil if project is private' do + user = create(:user) + create(:project, :repository, :private, path: user.username, namespace: user.namespace) + + expect(user.user_readme).to be(nil) + end + end end diff --git a/spec/models/users_statistics_spec.rb b/spec/models/users_statistics_spec.rb index b4b7ddb7c63..8553d0bfdb0 100644 --- a/spec/models/users_statistics_spec.rb +++ b/spec/models/users_statistics_spec.rb @@ -34,11 +34,11 @@ RSpec.describe UsersStatistics do describe '.create_current_stats!' do before do - create_list(:user_highest_role, 4) + create_list(:user_highest_role, 1) create_list(:user_highest_role, 2, :guest) - create_list(:user_highest_role, 3, :reporter) - create_list(:user_highest_role, 4, :developer) - create_list(:user_highest_role, 3, :maintainer) + create_list(:user_highest_role, 2, :reporter) + create_list(:user_highest_role, 2, :developer) + create_list(:user_highest_role, 2, :maintainer) create_list(:user_highest_role, 2, :owner) create_list(:user, 2, :bot) create_list(:user, 1, :blocked) @@ -49,11 +49,11 @@ RSpec.describe UsersStatistics do context 'when successful' do it 'creates an entry with the current statistics values' do expect(described_class.create_current_stats!).to have_attributes( - without_groups_and_projects: 4, + without_groups_and_projects: 1, with_highest_role_guest: 2, - with_highest_role_reporter: 3, - with_highest_role_developer: 4, - with_highest_role_maintainer: 3, + with_highest_role_reporter: 2, + with_highest_role_developer: 2, + with_highest_role_maintainer: 2, with_highest_role_owner: 2, bots: 2, blocked: 1 diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index bdbc73a59d8..7c147419354 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -3278,6 +3278,8 @@ RSpec.describe API::MergeRequests do context 'when skip_ci parameter is set' do it 'enqueues a rebase of the merge request with skip_ci flag set' do + allow(RebaseWorker).to receive(:with_status).and_return(RebaseWorker) + expect(RebaseWorker).to receive(:perform_async).with(merge_request.id, user.id, true).and_call_original Sidekiq::Testing.fake! do diff --git a/spec/requests/api/terraform/modules/v1/packages_spec.rb b/spec/requests/api/terraform/modules/v1/packages_spec.rb index b04f5ad9a94..b17bc11a451 100644 --- a/spec/requests/api/terraform/modules/v1/packages_spec.rb +++ b/spec/requests/api/terraform/modules/v1/packages_spec.rb @@ -28,10 +28,25 @@ RSpec.describe API::Terraform::Modules::V1::Packages do describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/versions' do let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/versions") } - let(:headers) { {} } + let(:headers) { { 'Authorization' => "Bearer #{tokens[:job_token]}" } } subject { get(url, headers: headers) } + context 'with a conflicting package name' do + let!(:conflicting_package) { create(:terraform_module_package, project: project, name: "conflict-#{package.name}", version: '2.0.0') } + + before do + group.add_developer(user) + end + + it 'returns only one version' do + subject + + expect(json_response['modules'][0]['versions'].size).to eq(1) + expect(json_response['modules'][0]['versions'][0]['version']).to eq('1.0.0') + end + end + context 'with valid namespace' do where(:visibility, :user_role, :member, :token_type, :valid_token, :shared_examples_name, :expected_status) do :public | :developer | true | :personal_access_token | true | 'returns terraform module packages' | :success diff --git a/spec/requests/import/gitlab_groups_controller_spec.rb b/spec/requests/import/gitlab_groups_controller_spec.rb index 1f6487986a3..4abf99cf994 100644 --- a/spec/requests/import/gitlab_groups_controller_spec.rb +++ b/spec/requests/import/gitlab_groups_controller_spec.rb @@ -60,6 +60,7 @@ RSpec.describe Import::GitlabGroupsController do end it 'imports the group data', :sidekiq_inline do + allow(GroupImportWorker).to receive(:with_status).and_return(GroupImportWorker) allow(GroupImportWorker).to receive(:perform_async).and_call_original import_request @@ -67,7 +68,6 @@ RSpec.describe Import::GitlabGroupsController do group = Group.find_by(name: 'test-group-import') expect(GroupImportWorker).to have_received(:perform_async).with(user.id, group.id) - expect(group.description).to eq 'A voluptate non sequi temporibus quam at.' expect(group.visibility_level).to eq Gitlab::VisibilityLevel::PRIVATE end diff --git a/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb index eaa5f723bec..6f28f892f00 100644 --- a/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb +++ b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb @@ -24,6 +24,10 @@ RSpec.describe AutoMerge::MergeWhenPipelineSucceedsService do project.add_maintainer(user) end + before do + allow(MergeWorker).to receive(:with_status).and_return(MergeWorker) + end + describe "#available_for?" do subject { service.available_for?(mr_merge_if_green_enabled) } diff --git a/spec/services/groups/import_export/import_service_spec.rb b/spec/services/groups/import_export/import_service_spec.rb index ad5c4364deb..292f2e2b86b 100644 --- a/spec/services/groups/import_export/import_service_spec.rb +++ b/spec/services/groups/import_export/import_service_spec.rb @@ -7,6 +7,10 @@ RSpec.describe Groups::ImportExport::ImportService do let_it_be(:user) { create(:user) } let_it_be(:group) { create(:group) } + before do + allow(GroupImportWorker).to receive(:with_status).and_return(GroupImportWorker) + end + context 'when the job can be successfully scheduled' do subject(:import_service) { described_class.new(group: group, user: user) } @@ -20,6 +24,8 @@ RSpec.describe Groups::ImportExport::ImportService do end it 'enqueues an import job' do + allow(GroupImportWorker).to receive(:with_status).and_return(GroupImportWorker) + expect(GroupImportWorker).to receive(:perform_async).with(user.id, group.id) import_service.async_execute diff --git a/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb b/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb index ff87fc5d8df..f8a752a5673 100644 --- a/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb +++ b/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb @@ -39,6 +39,10 @@ end # let(:status_api) { status_create_self_monitoring_project_admin_application_settings_path } # subject { post create_self_monitoring_project_admin_application_settings_path } RSpec.shared_examples 'triggers async worker, returns sidekiq job_id with response accepted' do + before do + allow(worker_class).to receive(:with_status).and_return(worker_class) + end + it 'returns sidekiq job_id of expected length' do subject diff --git a/spec/support/shared_examples/workers/self_monitoring_shared_examples.rb b/spec/support/shared_examples/workers/self_monitoring_shared_examples.rb index 89c0841fbd6..e6da96e12ec 100644 --- a/spec/support/shared_examples/workers/self_monitoring_shared_examples.rb +++ b/spec/support/shared_examples/workers/self_monitoring_shared_examples.rb @@ -17,7 +17,7 @@ end RSpec.shared_examples 'returns in_progress based on Sidekiq::Status' do it 'returns true when job is enqueued' do - jid = described_class.perform_async + jid = described_class.with_status.perform_async expect(described_class.in_progress?(jid)).to eq(true) end diff --git a/spec/workers/concerns/application_worker_spec.rb b/spec/workers/concerns/application_worker_spec.rb index f08c5336506..fbf39b3c7cd 100644 --- a/spec/workers/concerns/application_worker_spec.rb +++ b/spec/workers/concerns/application_worker_spec.rb @@ -598,4 +598,48 @@ RSpec.describe ApplicationWorker do end end end + + describe '.with_status' do + around do |example| + Sidekiq::Testing.fake!(&example) + end + + context 'when the worker does have status_expiration set' do + let(:status_expiration_worker) do + Class.new(worker) do + sidekiq_options status_expiration: 3 + end + end + + it 'uses status_expiration from the worker' do + status_expiration_worker.with_status.perform_async + + expect(Sidekiq::Queues[status_expiration_worker.queue].first).to include('status_expiration' => 3) + expect(Sidekiq::Queues[status_expiration_worker.queue].length).to eq(1) + end + + it 'uses status_expiration from the worker without with_status' do + status_expiration_worker.perform_async + + expect(Sidekiq::Queues[status_expiration_worker.queue].first).to include('status_expiration' => 3) + expect(Sidekiq::Queues[status_expiration_worker.queue].length).to eq(1) + end + end + + context 'when the worker does not have status_expiration set' do + it 'uses the default status_expiration' do + worker.with_status.perform_async + + expect(Sidekiq::Queues[worker.queue].first).to include('status_expiration' => Gitlab::SidekiqStatus::DEFAULT_EXPIRATION) + expect(Sidekiq::Queues[worker.queue].length).to eq(1) + end + + it 'does not set status_expiration without with_status' do + worker.perform_async + + expect(Sidekiq::Queues[worker.queue].first).not_to include('status_expiration') + expect(Sidekiq::Queues[worker.queue].length).to eq(1) + end + end + end end |