diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml index 810b5e1dffe..2bd363d8bff 100644 --- a/.rubocop_todo/layout/line_length.yml +++ b/.rubocop_todo/layout/line_length.yml @@ -4153,7 +4153,6 @@ Layout/LineLength: - 'spec/features/action_cable_logging_spec.rb' - 'spec/features/admin/admin_abuse_reports_spec.rb' - 'spec/features/admin/admin_mode/login_spec.rb' - - 'spec/features/admin/admin_runners_spec.rb' - 'spec/features/admin/admin_sees_background_migrations_spec.rb' - 'spec/features/admin/admin_sees_project_statistics_spec.rb' - 'spec/features/admin/admin_settings_spec.rb' diff --git a/.rubocop_todo/layout/space_inside_parens.yml b/.rubocop_todo/layout/space_inside_parens.yml index 1ba43d3b019..ffbe68d7d24 100644 --- a/.rubocop_todo/layout/space_inside_parens.yml +++ b/.rubocop_todo/layout/space_inside_parens.yml @@ -185,7 +185,6 @@ Layout/SpaceInsideParens: - 'spec/controllers/projects/runners_controller_spec.rb' - 'spec/dependencies/omniauth_saml_spec.rb' - 'spec/factories/usage_data.rb' - - 'spec/features/admin/admin_runners_spec.rb' - 'spec/features/boards/board_filters_spec.rb' - 'spec/features/boards/user_visits_board_spec.rb' - 'spec/features/dashboard/datetime_on_tooltips_spec.rb' diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml index a51ba5e930b..724aa420d89 100644 --- a/.rubocop_todo/rspec/context_wording.yml +++ b/.rubocop_todo/rspec/context_wording.yml @@ -1397,7 +1397,6 @@ RSpec/ContextWording: - 'spec/features/admin/admin_mode/logout_spec.rb' - 'spec/features/admin/admin_mode/workers_spec.rb' - 'spec/features/admin/admin_mode_spec.rb' - - 'spec/features/admin/admin_runners_spec.rb' - 'spec/features/admin/admin_search_settings_spec.rb' - 'spec/features/admin/admin_settings_spec.rb' - 'spec/features/admin/dashboard_spec.rb' @@ -1444,7 +1443,6 @@ RSpec/ContextWording: - 'spec/features/groups/dependency_proxy_for_containers_spec.rb' - 'spec/features/groups/dependency_proxy_spec.rb' - 'spec/features/groups/empty_states_spec.rb' - - 'spec/features/groups/group_runners_spec.rb' - 'spec/features/groups/group_settings_spec.rb' - 'spec/features/groups/issues_spec.rb' - 'spec/features/groups/labels/subscription_spec.rb' diff --git a/.rubocop_todo/rspec/instance_variable.yml b/.rubocop_todo/rspec/instance_variable.yml index bbd8902dddb..df62a8b0f87 100644 --- a/.rubocop_todo/rspec/instance_variable.yml +++ b/.rubocop_todo/rspec/instance_variable.yml @@ -87,7 +87,6 @@ RSpec/InstanceVariable: - spec/controllers/profiles/avatars_controller_spec.rb - spec/controllers/projects/clusters_controller_spec.rb - spec/controllers/sessions_controller_spec.rb - - spec/features/admin/admin_runners_spec.rb - spec/features/calendar_spec.rb - spec/features/issues/user_filters_issues_spec.rb - spec/features/markdown/copy_as_gfm_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 51b2ddf4443..e9c671cb99f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 15.1.3 (2022-07-19) + +### Added (1 change) + +- [Add praefect list virtual storages subcommand documentation](gitlab-org/gitlab@95689c32e2734831c00ef30de303098485ec095a) ([merge request](gitlab-org/gitlab!92708)) + +### Fixed (1 change) + +- [Fix group access dropdown failure if no subgroups are available](gitlab-org/gitlab@518a2f55caddab0c18d0548d0a8f777afe5ae666) ([merge request](gitlab-org/gitlab!92708)) **GitLab Enterprise Edition** + ## 15.1.2 (2022-07-05) ### Fixed (3 changes) diff --git a/app/assets/javascripts/linked_resources/components/add_issuable_resource_link_form.vue b/app/assets/javascripts/linked_resources/components/add_issuable_resource_link_form.vue deleted file mode 100644 index 6a0deb41fd1..00000000000 --- a/app/assets/javascripts/linked_resources/components/add_issuable_resource_link_form.vue +++ /dev/null @@ -1,75 +0,0 @@ - - - diff --git a/app/assets/javascripts/linked_resources/components/resource_links_block.vue b/app/assets/javascripts/linked_resources/components/resource_links_block.vue deleted file mode 100644 index 46c4fc7f632..00000000000 --- a/app/assets/javascripts/linked_resources/components/resource_links_block.vue +++ /dev/null @@ -1,111 +0,0 @@ - - - diff --git a/app/assets/javascripts/linked_resources/constants.js b/app/assets/javascripts/linked_resources/constants.js deleted file mode 100644 index 1b11cfc5f88..00000000000 --- a/app/assets/javascripts/linked_resources/constants.js +++ /dev/null @@ -1,14 +0,0 @@ -import { s__ } from '~/locale'; - -export const resourceLinksI18n = Object.freeze({ - headerText: s__('LinkedResources|Linked resources'), - helpText: s__('LinkedResources|Read more about linked resources'), - addButtonText: s__('LinkedResources|Add a resource link'), -}); - -export const resourceLinksFormI18n = Object.freeze({ - linkTextLabel: s__('LinkedResources|Text (Optional)'), - linkValueLabel: s__('LinkedResources|Link'), - submitButtonText: s__('LinkedResources|Add'), - cancelButtonText: s__('LinkedResources|Cancel'), -}); diff --git a/app/assets/javascripts/linked_resources/index.js b/app/assets/javascripts/linked_resources/index.js index 4ac9ca31a84..244adca86c9 100644 --- a/app/assets/javascripts/linked_resources/index.js +++ b/app/assets/javascripts/linked_resources/index.js @@ -1,6 +1,6 @@ import Vue from 'vue'; +import ResourceLinksBlock from 'ee_component/linked_resources/components/resource_links_block.vue'; import { parseBoolean } from '~/lib/utils/common_utils'; -import ResourceLinksBlock from './components/resource_links_block.vue'; export default function initLinkedResources() { const linkedResourcesRootElement = document.querySelector('.js-linked-resources-root'); diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue index 56da8e88b7a..bfa99c01c3f 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue @@ -1,4 +1,5 @@ + + diff --git a/app/assets/javascripts/work_items/graphql/change_work_item_parent_link.mutation.graphql b/app/assets/javascripts/work_items/graphql/change_work_item_parent_link.mutation.graphql new file mode 100644 index 00000000000..dc5286174d8 --- /dev/null +++ b/app/assets/javascripts/work_items/graphql/change_work_item_parent_link.mutation.graphql @@ -0,0 +1,13 @@ +mutation changeWorkItemParentLink($id: WorkItemID!, $parentId: WorkItemID) { + workItemUpdate(input: { id: $id, hierarchyWidget: { parentId: $parentId } }) { + workItem { + id + workItemType { + id + } + title + state + } + errors + } +} diff --git a/app/controllers/projects/pipelines/tests_controller.rb b/app/controllers/projects/pipelines/tests_controller.rb index e42cb9b8422..8ac370b1bd4 100644 --- a/app/controllers/projects/pipelines/tests_controller.rb +++ b/app/controllers/projects/pipelines/tests_controller.rb @@ -51,7 +51,7 @@ module Projects def test_suite suite = builds.sum do |build| - build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new) + build.collect_test_reports!(Gitlab::Ci::Reports::TestReport.new) end Gitlab::Ci::Reports::TestFailureHistory.new(suite.failed.values, project).load! diff --git a/app/graphql/resolvers/ci/test_suite_resolver.rb b/app/graphql/resolvers/ci/test_suite_resolver.rb index 5d61d9e986b..f758e217b47 100644 --- a/app/graphql/resolvers/ci/test_suite_resolver.rb +++ b/app/graphql/resolvers/ci/test_suite_resolver.rb @@ -28,7 +28,7 @@ module Resolvers def load_test_suite_data(builds) suite = builds.sum do |build| - build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new) + build.collect_test_reports!(Gitlab::Ci::Reports::TestReport.new) end Gitlab::Ci::Reports::TestFailureHistory.new(suite.failed.values, pipeline.project).load! diff --git a/app/helpers/users/callouts_helper.rb b/app/helpers/users/callouts_helper.rb index 87c8bf5cb28..3dd6b3f4a80 100644 --- a/app/helpers/users/callouts_helper.rb +++ b/app/helpers/users/callouts_helper.rb @@ -11,6 +11,7 @@ module Users UNFINISHED_TAG_CLEANUP_CALLOUT = 'unfinished_tag_cleanup_callout' SECURITY_NEWSLETTER_CALLOUT = 'security_newsletter_callout' REGISTRATION_ENABLED_CALLOUT_ALLOWED_CONTROLLER_PATHS = [/^root/, /^dashboard\S*/, /^admin\S*/].freeze + WEB_HOOK_DISABLED = 'web_hook_disabled' def show_gke_cluster_integration_callout?(project) active_nav_link?(controller: sidebar_operations_paths) && @@ -60,12 +61,31 @@ module Users !user_dismissed?(SECURITY_NEWSLETTER_CALLOUT) end + def web_hook_disabled_dismissed?(project) + return false unless project + + last_failure = Gitlab::Redis::SharedState.with do |redis| + key = "web_hooks:last_failure:project-#{project.id}" + redis.get(key) + end + + last_failure = DateTime.parse(last_failure) if last_failure + + user_dismissed?(WEB_HOOK_DISABLED, last_failure, namespace: project.namespace) + end + private - def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil) + def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil, namespace: nil) return false unless current_user - current_user.dismissed_callout?(feature_name: feature_name, ignore_dismissal_earlier_than: ignore_dismissal_earlier_than) + query = { feature_name: feature_name, ignore_dismissal_earlier_than: ignore_dismissal_earlier_than } + + if namespace + current_user.dismissed_callout_for_namespace?(namespace: namespace, **query) + else + current_user.dismissed_callout?(**query) + end end end end diff --git a/app/helpers/web_hooks/web_hooks_helper.rb b/app/helpers/web_hooks/web_hooks_helper.rb new file mode 100644 index 00000000000..95122750c2f --- /dev/null +++ b/app/helpers/web_hooks/web_hooks_helper.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module WebHooks + module WebHooksHelper + EXPIRY_TTL = 1.hour + + def show_project_hook_failed_callout?(project:) + return false unless current_user + return false unless Feature.enabled?(:webhooks_failed_callout, project) + return false unless Feature.enabled?(:web_hooks_disable_failed, project) + return false unless Ability.allowed?(current_user, :read_web_hooks, project) + + # Assumes include of Users::CalloutsHelper + return false if web_hook_disabled_dismissed?(project) + + any_project_hook_failed?(project) # Most expensive query last + end + + private + + def any_project_hook_failed?(project) + Rails.cache.fetch("any_web_hook_failed:#{project.id}", expires_in: EXPIRY_TTL) do + ProjectHook.for_projects(project).disabled.exists? + end + end + end +end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 791bae17271..78b55680b5e 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -1090,7 +1090,7 @@ module Ci end def test_reports - Gitlab::Ci::Reports::TestReports.new.tap do |test_reports| + Gitlab::Ci::Reports::TestReport.new.tap do |test_reports| latest_test_report_builds.find_each do |build| build.collect_test_reports!(test_reports) end diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index b7ace34141e..bcbf43ee38b 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -27,6 +27,8 @@ class ProjectHook < WebHook belongs_to :project validates :project, presence: true + scope :for_projects, ->(project) { where(project: project) } + def pluralized_name _('Webhooks') end @@ -41,6 +43,19 @@ class ProjectHook < WebHook project end + override :update_last_failure + def update_last_failure + return if executable? + + key = "web_hooks:last_failure:project-#{project_id}" + time = Time.current.utc.iso8601 + + Gitlab::Redis::SharedState.with do |redis| + prev = redis.get(key) + redis.set(key, time) if !prev || prev < time + end + end + private override :web_hooks_disable_failed? diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 71e7206718e..f428d07cd7f 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -48,6 +48,11 @@ class WebHook < ApplicationRecord where('recent_failures <= ? AND (disabled_until IS NULL OR disabled_until < ?)', FAILURE_THRESHOLD, Time.current) end + # Inverse of executable + scope :disabled, -> do + where('recent_failures > ? OR disabled_until >= ?', FAILURE_THRESHOLD, Time.current) + end + def executable? !temporarily_disabled? && !permanently_disabled? end @@ -181,6 +186,10 @@ class WebHook < ApplicationRecord raise InterpolationError, "Invalid URL template. Missing key #{e.key}" end + def update_last_failure + # Overridden in child classes. + end + private def web_hooks_disable_failed? diff --git a/app/models/note.rb b/app/models/note.rb index f2ddf0efe47..986a85acac6 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -111,6 +111,7 @@ class Note < ApplicationRecord end validate :does_not_exceed_notes_limit?, on: :create, unless: [:system?, :importing?] + validate :validate_created_after # @deprecated attachments are handled by the Upload model. # @@ -748,6 +749,13 @@ class Note < ApplicationRecord errors.add(:base, _('Maximum number of comments exceeded')) if noteable.notes.count >= Noteable::MAX_NOTES_LIMIT end + def validate_created_after + return unless created_at + return if created_at >= '1970-01-01' + + errors.add(:created_at, s_('Note|The created date provided is too far in the past.')) + end + def noteable_label_url_method for_merge_request? ? :project_merge_requests_url : :project_issues_url end diff --git a/app/models/user.rb b/app/models/user.rb index dc2f36a9ddb..12f434db631 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -222,6 +222,7 @@ class User < ApplicationRecord has_many :custom_attributes, class_name: 'UserCustomAttribute' has_many :callouts, class_name: 'Users::Callout' has_many :group_callouts, class_name: 'Users::GroupCallout' + has_many :namespace_callouts, class_name: 'Users::NamespaceCallout' has_many :term_agreements belongs_to :accepted_term, class_name: 'ApplicationSetting::Term' @@ -2085,6 +2086,13 @@ class User < ApplicationRecord callout_dismissed?(callout, ignore_dismissal_earlier_than) end + def dismissed_callout_for_namespace?(feature_name:, namespace:, ignore_dismissal_earlier_than: nil) + source_feature_name = "#{feature_name}_#{namespace.id}" + callout = namespace_callouts_by_feature_name[source_feature_name] + + callout_dismissed?(callout, ignore_dismissal_earlier_than) + end + # Load the current highest access by looking directly at the user's memberships def current_highest_access_level members.non_request.maximum(:access_level) @@ -2111,6 +2119,11 @@ class User < ApplicationRecord .find_or_initialize_by(feature_name: ::Users::GroupCallout.feature_names[feature_name], group_id: group_id) end + def find_or_initialize_namespace_callout(feature_name, namespace_id) + namespace_callouts + .find_or_initialize_by(feature_name: ::Users::NamespaceCallout.feature_names[feature_name], namespace_id: namespace_id) + end + def can_trigger_notifications? confirmed? && !blocked? && !ghost? end @@ -2228,6 +2241,10 @@ class User < ApplicationRecord @group_callouts_by_feature_name ||= group_callouts.index_by(&:source_feature_name) end + def namespace_callouts_by_feature_name + @namespace_callouts_by_feature_name ||= namespace_callouts.index_by(&:source_feature_name) + end + def authorized_groups_without_shared_membership Group.from_union([ groups.select(*Namespace.cached_column_list), diff --git a/app/models/users/namespace_callout.rb b/app/models/users/namespace_callout.rb new file mode 100644 index 00000000000..a20a196a4ef --- /dev/null +++ b/app/models/users/namespace_callout.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Users + class NamespaceCallout < ApplicationRecord + include Users::Calloutable + + self.table_name = 'user_namespace_callouts' + + belongs_to :namespace + + enum feature_name: { + invite_members_banner: 1, + approaching_seat_count_threshold: 2, # EE-only + storage_enforcement_banner_first_enforcement_threshold: 3, + storage_enforcement_banner_second_enforcement_threshold: 4, + storage_enforcement_banner_third_enforcement_threshold: 5, + storage_enforcement_banner_fourth_enforcement_threshold: 6, + preview_user_over_limit_free_plan_alert: 7, # EE-only + user_reached_limit_free_plan_alert: 8, # EE-only + web_hook_disabled: 9 + } + + validates :namespace, presence: true + validates :feature_name, + presence: true, + uniqueness: { scope: [:user_id, :namespace_id] }, + inclusion: { in: NamespaceCallout.feature_names.keys } + + def source_feature_name + "#{feature_name}_#{namespace_id}" + end + end +end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index c54dbefc1ae..850f25a6089 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -492,6 +492,7 @@ class ProjectPolicy < BasePolicy enable :update_runners_registration_token enable :admin_project_google_cloud enable :admin_secure_files + enable :read_web_hooks end rule { public_project & metrics_dashboard_allowed }.policy do diff --git a/app/services/ci/build_report_result_service.rb b/app/services/ci/build_report_result_service.rb index 8bdb51320f9..f9146b3677a 100644 --- a/app/services/ci/build_report_result_service.rb +++ b/app/services/ci/build_report_result_service.rb @@ -22,7 +22,7 @@ module Ci private def generate_test_suite_report(build) - build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new) + build.collect_test_reports!(Gitlab::Ci::Reports::TestReport.new) end def tests_params(test_suite) diff --git a/app/services/ci/test_failure_history_service.rb b/app/services/ci/test_failure_history_service.rb index 7323ad417ea..2214a6a2729 100644 --- a/app/services/ci/test_failure_history_service.rb +++ b/app/services/ci/test_failure_history_service.rb @@ -81,7 +81,7 @@ module Ci def generate_test_suite!(build) # Returns an instance of Gitlab::Ci::Reports::TestSuite - build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new) + build.collect_test_reports!(Gitlab::Ci::Reports::TestReport.new) end def ci_unit_test_attrs(batch) diff --git a/app/services/web_hooks/log_execution_service.rb b/app/services/web_hooks/log_execution_service.rb index 0ee7c41469f..17dcf615830 100644 --- a/app/services/web_hooks/log_execution_service.rb +++ b/app/services/web_hooks/log_execution_service.rb @@ -44,6 +44,7 @@ module WebHooks end log_state_change + hook.update_last_failure end rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError raise if raise_lock_error? diff --git a/app/views/errors/omniauth_error.html.haml b/app/views/errors/omniauth_error.html.haml index e114e4609f8..3090c823677 100644 --- a/app/views/errors/omniauth_error.html.haml +++ b/app/views/errors/omniauth_error.html.haml @@ -2,14 +2,18 @@ .container = render partial: "shared/errors/graphic_422", formats: :svg - %h3 Sign-in using #{@provider} auth failed + %h3 + = _('Sign-in using %{provider} auth failed') % { provider: @provider } - %p.light.subtitle Sign-in failed because #{@error}. + %p.light.subtitle + = _('Sign-in failed because %{error}.') % { error: @error } - %p Try logging in using your username or email. If you have forgotten your password, try recovering it + %p + = _('Try logging in using your username or email. If you have forgotten your password, try recovering it') - = link_to "Sign in", new_session_path(:user), class: 'gl-button btn primary' - = link_to "Recover password", new_password_path(:user), class: 'gl-button btn secondary' + = link_to _('Sign in'), new_session_path(:user), class: 'gl-button btn primary' + = link_to _('Recover password'), new_password_path(:user), class: 'gl-button btn secondary' %hr - %p.light If none of the options work, try contacting a GitLab administrator. + %p.light + = _('If none of the options work, try contacting a GitLab administrator.') diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 54435f675a7..07e299d71ea 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -6,9 +6,9 @@ = preserve(markdown(commit.description, pipeline: :single_line)) .info-well - .well-segment.pipeline-info - .icon-container.gl-vertical-align-text-bottom - = sprite_icon('clock') + .well-segment.pipeline-info{ class: "gl-align-items-baseline!" } + .icon-container + = sprite_icon('clock', css_class: 'gl-top-0!') = pluralize @pipeline.total_size, "job" = @pipeline.ref_text - if @pipeline.duration @@ -20,7 +20,7 @@ - if has_pipeline_badges?(@pipeline) .well-segment.qa-pipeline-badges .icon-container - = sprite_icon('flag') + = sprite_icon('flag', css_class: 'gl-top-0!') - if @pipeline.child? - text = sprintf(s_('Pipelines|Child pipeline (%{link_start}parent%{link_end})'), { link_start: "", link_end: ""}).html_safe = gl_badge_tag text, { variant: :info, size: :sm }, { class: 'js-pipeline-child has-tooltip', title: s_("Pipelines|This is a child pipeline within the parent pipeline") } @@ -44,13 +44,13 @@ .well-segment.branch-info .icon-container.commit-icon - = custom_icon("icon_commit") + = sprite_icon('commit', css_class: 'gl-top-0!') = link_to commit.short_id, project_commit_path(@project, @pipeline.sha), class: "commit-sha" = clipboard_button(text: @pipeline.sha, title: _("Copy commit SHA")) .well-segment.related-merge-request-info .icon-container - = sprite_icon("git-merge") + = sprite_icon("git-merge", css_class: 'gl-top-0!') %span.related-merge-requests %span.js-truncated-mr-list = @pipeline.all_related_merge_request_text(limit: 1) diff --git a/config/feature_flags/development/webhooks_failed_callout.yml b/config/feature_flags/development/webhooks_failed_callout.yml new file mode 100644 index 00000000000..11de5a793f6 --- /dev/null +++ b/config/feature_flags/development/webhooks_failed_callout.yml @@ -0,0 +1,8 @@ +--- +name: webhooks_failed_callout +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91092 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/365535 +milestone: '15.2' +type: development +group: group::integrations +default_enabled: false diff --git a/db/docs/user_namespace_callouts.yml b/db/docs/user_namespace_callouts.yml new file mode 100644 index 00000000000..5038ecce3bc --- /dev/null +++ b/db/docs/user_namespace_callouts.yml @@ -0,0 +1,10 @@ + +--- +table_name: user_namespace_callouts +classes: +- Users::NamespaceCallout +feature_categories: +- navigation +description: Contains records of which users have dismissed a callout, grouped by namespace. +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91092 +milestone: '15.2' diff --git a/db/migrate/20220627122229_create_user_namespace_callouts.rb b/db/migrate/20220627122229_create_user_namespace_callouts.rb new file mode 100644 index 00000000000..fc85c02d2db --- /dev/null +++ b/db/migrate/20220627122229_create_user_namespace_callouts.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class CreateUserNamespaceCallouts < Gitlab::Database::Migration[2.0] + def up + create_table :user_namespace_callouts do |t| + t.bigint :user_id, null: false + t.bigint :namespace_id, null: false, index: true + t.datetime_with_timezone :dismissed_at + t.integer :feature_name, limit: 2, null: false + end + end + + def down + drop_table :user_namespace_callouts + end +end diff --git a/db/migrate/20220627122230_add_foreign_keys_to_user_namespace_callouts.rb b/db/migrate/20220627122230_add_foreign_keys_to_user_namespace_callouts.rb new file mode 100644 index 00000000000..f78eb978a9b --- /dev/null +++ b/db/migrate/20220627122230_add_foreign_keys_to_user_namespace_callouts.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class AddForeignKeysToUserNamespaceCallouts < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + def up + add_concurrent_foreign_key :user_namespace_callouts, :users, + column: :user_id, + on_delete: :cascade + + add_concurrent_foreign_key :user_namespace_callouts, :namespaces, + column: :namespace_id, + on_delete: :cascade + + add_concurrent_index :user_namespace_callouts, [:user_id, :feature_name, :namespace_id], + unique: true, + name: 'index_ns_user_callouts_feature' + end + + def down + remove_concurrent_index_by_name :user_namespace_callouts, 'index_ns_user_callouts_feature' + + with_lock_retries do + remove_foreign_key :user_namespace_callouts, column: :user_id + remove_foreign_key :user_namespace_callouts, column: :namespace_id + end + end +end diff --git a/db/post_migrate/20220628111752_drop_token_index_from_ci_builds.rb b/db/post_migrate/20220628111752_drop_token_index_from_ci_builds.rb new file mode 100644 index 00000000000..d551eeebeb6 --- /dev/null +++ b/db/post_migrate/20220628111752_drop_token_index_from_ci_builds.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class DropTokenIndexFromCiBuilds < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + INDEX_NAME = 'index_ci_builds_on_token_partial' + + def up + remove_concurrent_index_by_name :ci_builds, INDEX_NAME + end + + # rubocop:disable Migration/PreventIndexCreation + def down + add_concurrent_index :ci_builds, :token, unique: true, where: 'token IS NOT NULL', name: INDEX_NAME + end + # rubocop:enable Migration/PreventIndexCreation +end diff --git a/db/post_migrate/20220715163254_update_notes_in_past.rb b/db/post_migrate/20220715163254_update_notes_in_past.rb new file mode 100644 index 00000000000..1c46a3bc9dc --- /dev/null +++ b/db/post_migrate/20220715163254_update_notes_in_past.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# See https://docs.gitlab.com/ee/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class UpdateNotesInPast < Gitlab::Database::Migration[2.0] + restrict_gitlab_migration gitlab_schema: :gitlab_main + + def up + loop do + update_count = define_batchable_model('notes') + .where('created_at < ?', '1970-01-01').limit(100) + .update_all(created_at: '1970-01-01 00:00:00') + + break if update_count == 0 + end + end + + def down + # no op + end +end diff --git a/db/schema_migrations/20220627122229 b/db/schema_migrations/20220627122229 new file mode 100644 index 00000000000..040376e1aa0 --- /dev/null +++ b/db/schema_migrations/20220627122229 @@ -0,0 +1 @@ +29ab69647b53c331aefdd62e8fbcc1567df4424a8e7ae6f8eb7b1e9afa7a6911 \ No newline at end of file diff --git a/db/schema_migrations/20220627122230 b/db/schema_migrations/20220627122230 new file mode 100644 index 00000000000..82ba0d503ee --- /dev/null +++ b/db/schema_migrations/20220627122230 @@ -0,0 +1 @@ +6d65af0d20cd80cf3367f48c5447ff33046e982ac1cfd55aaf52a7cc2330e428 \ No newline at end of file diff --git a/db/schema_migrations/20220628111752 b/db/schema_migrations/20220628111752 new file mode 100644 index 00000000000..747546f1ba4 --- /dev/null +++ b/db/schema_migrations/20220628111752 @@ -0,0 +1 @@ +5a4a6355d1954735a05831e17c97e2879320f2cb313be56fb72e1cd2c20d9090 \ No newline at end of file diff --git a/db/schema_migrations/20220715163254 b/db/schema_migrations/20220715163254 new file mode 100644 index 00000000000..71461af7b68 --- /dev/null +++ b/db/schema_migrations/20220715163254 @@ -0,0 +1 @@ +ea8182741ce0b30f2de23041d1f6bafaf6e04a7a7d0f50abcd04462683637596 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index bca2b45c8bb..7080cf1936f 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -21669,6 +21669,23 @@ CREATE TABLE user_interacted_projects ( project_id integer NOT NULL ); +CREATE TABLE user_namespace_callouts ( + id bigint NOT NULL, + user_id bigint NOT NULL, + namespace_id bigint NOT NULL, + dismissed_at timestamp with time zone, + feature_name smallint NOT NULL +); + +CREATE SEQUENCE user_namespace_callouts_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE user_namespace_callouts_id_seq OWNED BY user_namespace_callouts.id; + CREATE TABLE user_permission_export_uploads ( id bigint NOT NULL, created_at timestamp with time zone NOT NULL, @@ -23602,6 +23619,8 @@ ALTER TABLE ONLY user_details ALTER COLUMN user_id SET DEFAULT nextval('user_det ALTER TABLE ONLY user_group_callouts ALTER COLUMN id SET DEFAULT nextval('user_group_callouts_id_seq'::regclass); +ALTER TABLE ONLY user_namespace_callouts ALTER COLUMN id SET DEFAULT nextval('user_namespace_callouts_id_seq'::regclass); + ALTER TABLE ONLY user_permission_export_uploads ALTER COLUMN id SET DEFAULT nextval('user_permission_export_uploads_id_seq'::regclass); ALTER TABLE ONLY user_preferences ALTER COLUMN id SET DEFAULT nextval('user_preferences_id_seq'::regclass); @@ -25879,6 +25898,9 @@ ALTER TABLE ONLY user_highest_roles ALTER TABLE ONLY user_interacted_projects ADD CONSTRAINT user_interacted_projects_pkey PRIMARY KEY (project_id, user_id); +ALTER TABLE ONLY user_namespace_callouts + ADD CONSTRAINT user_namespace_callouts_pkey PRIMARY KEY (id); + ALTER TABLE ONLY user_permission_export_uploads ADD CONSTRAINT user_permission_export_uploads_pkey PRIMARY KEY (id); @@ -27495,8 +27517,6 @@ CREATE INDEX index_ci_builds_on_status_and_type_and_runner_id ON ci_builds USING CREATE UNIQUE INDEX index_ci_builds_on_token_encrypted ON ci_builds USING btree (token_encrypted) WHERE (token_encrypted IS NOT NULL); -CREATE UNIQUE INDEX index_ci_builds_on_token_partial ON ci_builds USING btree (token) WHERE (token IS NOT NULL); - CREATE INDEX index_ci_builds_on_updated_at ON ci_builds USING btree (updated_at); CREATE INDEX index_ci_builds_on_upstream_pipeline_id ON ci_builds USING btree (upstream_pipeline_id) WHERE (upstream_pipeline_id IS NOT NULL); @@ -28911,6 +28931,8 @@ CREATE INDEX index_notification_settings_on_source_and_level_and_user ON notific CREATE UNIQUE INDEX index_notifications_on_user_id_and_source_id_and_source_type ON notification_settings USING btree (user_id, source_id, source_type); +CREATE UNIQUE INDEX index_ns_user_callouts_feature ON user_namespace_callouts USING btree (user_id, feature_name, namespace_id); + CREATE INDEX index_oauth_access_grants_on_resource_owner_id ON oauth_access_grants USING btree (resource_owner_id, application_id, created_at); CREATE UNIQUE INDEX index_oauth_access_grants_on_token ON oauth_access_grants USING btree (token); @@ -29913,6 +29935,8 @@ CREATE INDEX index_user_highest_roles_on_user_id_and_highest_access_level ON use CREATE INDEX index_user_interacted_projects_on_user_id ON user_interacted_projects USING btree (user_id); +CREATE INDEX index_user_namespace_callouts_on_namespace_id ON user_namespace_callouts USING btree (namespace_id); + CREATE INDEX index_user_permission_export_uploads_on_user_id_and_status ON user_permission_export_uploads USING btree (user_id, status); CREATE INDEX index_user_preferences_on_gitpod_enabled ON user_preferences USING btree (gitpod_enabled); @@ -31780,6 +31804,9 @@ ALTER TABLE ONLY ci_pipelines ALTER TABLE ONLY geo_event_log ADD CONSTRAINT fk_27548c6db3 FOREIGN KEY (hashed_storage_migrated_event_id) REFERENCES geo_hashed_storage_migrated_events(id) ON DELETE CASCADE; +ALTER TABLE ONLY user_namespace_callouts + ADD CONSTRAINT fk_27a69fd1bd FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; + ALTER TABLE ONLY merge_requests_compliance_violations ADD CONSTRAINT fk_290ec1ab02 FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE; @@ -31885,6 +31912,9 @@ ALTER TABLE ONLY releases ALTER TABLE ONLY geo_event_log ADD CONSTRAINT fk_4a99ebfd60 FOREIGN KEY (repositories_changed_event_id) REFERENCES geo_repositories_changed_events(id) ON DELETE CASCADE; +ALTER TABLE ONLY user_namespace_callouts + ADD CONSTRAINT fk_4b1257f385 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ALTER TABLE ONLY sbom_occurrences ADD CONSTRAINT fk_4b88e5b255 FOREIGN KEY (component_version_id) REFERENCES sbom_component_versions(id) ON DELETE CASCADE; diff --git a/doc/administration/operations/rails_console.md b/doc/administration/operations/rails_console.md index d6500bc25ae..660a99faaf8 100644 --- a/doc/administration/operations/rails_console.md +++ b/doc/administration/operations/rails_console.md @@ -43,6 +43,35 @@ The console is in the toolbox pod. Refer to our [Kubernetes cheat sheet](https:/ To exit the console, type: `quit`. +## Enable Active Record logging + +You can enable output of Active Record debug logging in the Rails console +session by running: + +```ruby +ActiveRecord::Base.logger = Logger.new($stdout) +``` + +This shows information about database queries triggered by any Ruby code +you may run in the console. To turn off logging again, run: + +```ruby +ActiveRecord::Base.logger = nil +``` + +## Disable database statement timeout + +You can disable the PostgreSQL statement timeout for the current Rails console +session by running: + +```ruby +ActiveRecord::Base.connection.execute('SET statement_timeout TO 0') +``` + +This change only affects the current Rails console session and is +not persisted in the GitLab production environment or in the next Rails +console session. + ## Output Rails console session history Enter the following command on the rails console to display diff --git a/doc/administration/troubleshooting/debug.md b/doc/administration/troubleshooting/debug.md index 4b28b578ebc..2ea79a15e6e 100644 --- a/doc/administration/troubleshooting/debug.md +++ b/doc/administration/troubleshooting/debug.md @@ -9,83 +9,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w Sometimes things don't work the way they should. Here are some tips on debugging issues out in production. -## Starting a Rails console session - -Troubleshooting and debugging your GitLab instance often requires a Rails console. - -Your type of GitLab installation determines how -[to start a rails console](../operations/rails_console.md). -See also: - -- [GitLab Rails Console Cheat Sheet](gitlab_rails_cheat_sheet.md). - -### Enabling Active Record logging - -You can enable output of Active Record debug logging in the Rails console -session by running: - -```ruby -ActiveRecord::Base.logger = Logger.new($stdout) -``` - -This shows information about database queries triggered by any Ruby code -you may run in the console. To turn off logging again, run: - -```ruby -ActiveRecord::Base.logger = nil -``` - -### Disabling database statement timeout - -You can disable the PostgreSQL statement timeout for the current Rails console -session by running: - -```ruby -ActiveRecord::Base.connection.execute('SET statement_timeout TO 0') -``` - -This change only affects the current Rails console session and is -not persisted in the GitLab production environment or in the next Rails -console session. - -### Output Rails console session history - -If you'd like to output your Rails console command history in a format that's -easy to copy and save for future reference, you can run: - -```ruby -puts Readline::HISTORY.to_a -``` - -## Using the Rails runner - -If you need to run some Ruby code in the context of your GitLab production -environment, you can do so using the [Rails runner](https://guides.rubyonrails.org/command_line.html#rails-runner). When executing a script file, the script must be accessible by the `git` user. - -**For Omnibus installations** - -```shell -sudo gitlab-rails runner "RAILS_COMMAND" - -# Example with a two-line Ruby script -sudo gitlab-rails runner "user = User.first; puts user.username" - -# Example with a ruby script file (make sure to use the full path) -sudo gitlab-rails runner /path/to/script.rb -``` - -**For installations from source** - -```shell -sudo -u git -H bundle exec rails runner -e production "RAILS_COMMAND" - -# Example with a two-line Ruby script -sudo -u git -H bundle exec rails runner -e production "user = User.first; puts user.username" - -# Example with a ruby script file (make sure to use the full path) -sudo -u git -H bundle exec rails runner -e production /path/to/script.rb -``` - ## More information - [Debugging Stuck Ruby Processes](https://newrelic.com/blog/best-practices/debugging-stuck-ruby-processes-what-to-do-before-you-kill-9) diff --git a/doc/api/index.md b/doc/api/index.md index cf14a9f405b..26447a2223d 100644 --- a/doc/api/index.md +++ b/doc/api/index.md @@ -453,12 +453,14 @@ Keyset-pagination allows for more efficient retrieval of pages and - in contrast to offset-based pagination - runtime is independent of the size of the collection. -This method is controlled by the following parameters: +This method is controlled by the following parameters. `order_by` and `sort` are both mandatory. -| Parameter | Description | -|--------------| ------------| -| `pagination` | `keyset` (to enable keyset pagination). | -| `per_page` | Number of items to list per page (default: `20`, max: `100`). | +| Parameter | Required | Description | +|--------------| ------------ | --------- | +| `pagination` | yes | `keyset` (to enable keyset pagination). | +| `per_page` | no | Number of items to list per page (default: `20`, max: `100`). | +| `order_by` | yes | Column by which to order by. | +| `sort` | yes | Sort order (`asc` or `desc`) | In the following example, we list 50 [projects](projects.md) per page, ordered by `id` ascending. diff --git a/doc/api/notes.md b/doc/api/notes.md index fbcf5e28f79..f7caae59b4d 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -146,7 +146,7 @@ Parameters: | `issue_iid` | integer | yes | The IID of an issue. | | `body` | string | yes | The content of a note. Limited to 1,000,000 characters. | | `confidential` | boolean | no | The confidential flag of a note. Default is false. | -| `created_at` | string | no | Date time string, ISO 8601 formatted. Example: `2016-03-11T03:45:40Z` (requires administrator or project/group owner rights) | +| `created_at` | string | no | Date time string, ISO 8601 formatted. It must be after 1970-01-01. Example: `2016-03-11T03:45:40Z` (requires administrator or project/group owner rights) | ```shell curl --request POST --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/5/issues/11/notes?body=note" diff --git a/doc/development/integrations/secure.md b/doc/development/integrations/secure.md index 7777afeed39..0a0c5e4d2a6 100644 --- a/doc/development/integrations/secure.md +++ b/doc/development/integrations/secure.md @@ -316,11 +316,12 @@ and [Container Scanning](../../user/application_security/container_scanning/inde You can find the schemas for these scanners here: -- [SAST](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/sast-report-format.json) -- [DAST](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/dast-report-format.json) -- [Dependency Scanning](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/dependency-scanning-report-format.json) +- [Cluster Image Scanning](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/cluster-image-scanning-report-format.json) - [Container Scanning](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/container-scanning-report-format.json) - [Coverage Fuzzing](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/coverage-fuzzing-report-format.json) +- [DAST](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/dast-report-format.json) +- [Dependency Scanning](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/dependency-scanning-report-format.json) +- [SAST](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/sast-report-format.json) - [Secret Detection](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/secret-detection-report-format.json) ### Retention period for vulnerabilities diff --git a/doc/user/application_security/terminology/index.md b/doc/user/application_security/terminology/index.md index 392bfa1dde2..d50cce3b4e8 100644 --- a/doc/user/application_security/terminology/index.md +++ b/doc/user/application_security/terminology/index.md @@ -220,11 +220,12 @@ once it's imported into the database. The type of scan. This must be one of the following: -- `container_scanning` -- `dependency_scanning` -- `dast` -- `sast` - `cluster_image_scanning` +- `container_scanning` +- `dast` +- `dependency_scanning` +- `sast` +- `secret_detection` ### Scanner diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md index af84b746280..427c412219a 100644 --- a/doc/user/profile/personal_access_tokens.md +++ b/doc/user/profile/personal_access_tokens.md @@ -145,7 +145,7 @@ To create a personal access token programmatically: ``` This code can be shortened into a single-line shell command by using the -[Rails runner](../../administration/troubleshooting/debug.md#using-the-rails-runner): +[Rails runner](../../administration/operations/rails_console.md#using-the-rails-runner): ```shell sudo gitlab-rails runner "token = User.find_by_username('automation-bot').personal_access_tokens.create(scopes: [:read_user, :read_repository], name: 'Automation token'); token.set_token('token-string-here123'); token.save!" @@ -177,7 +177,7 @@ To revoke a token programmatically: ``` This code can be shortened into a single-line shell command using the -[Rails runner](../../administration/troubleshooting/debug.md#using-the-rails-runner): +[Rails runner](../../administration/operations/rails_console.md#using-the-rails-runner): ```shell sudo gitlab-rails runner "PersonalAccessToken.find_by_token('token-string-here123').revoke!" diff --git a/doc/user/project/merge_requests/img/merge_method_ff_v15_0.png b/doc/user/project/merge_requests/img/merge_method_ff_v15_0.png deleted file mode 100644 index 323fd03ffa2..00000000000 Binary files a/doc/user/project/merge_requests/img/merge_method_ff_v15_0.png and /dev/null differ diff --git a/doc/user/project/merge_requests/img/merge_method_merge_commit_v15_0.png b/doc/user/project/merge_requests/img/merge_method_merge_commit_v15_0.png deleted file mode 100644 index b880c2c0e04..00000000000 Binary files a/doc/user/project/merge_requests/img/merge_method_merge_commit_v15_0.png and /dev/null differ diff --git a/doc/user/project/merge_requests/img/merge_method_merge_commit_with_semi_linear_history_v15_0.png b/doc/user/project/merge_requests/img/merge_method_merge_commit_with_semi_linear_history_v15_0.png deleted file mode 100644 index 9eab71e9d3c..00000000000 Binary files a/doc/user/project/merge_requests/img/merge_method_merge_commit_with_semi_linear_history_v15_0.png and /dev/null differ diff --git a/doc/user/project/merge_requests/methods/index.md b/doc/user/project/merge_requests/methods/index.md index d3221162cfd..63b464e5ff4 100644 --- a/doc/user/project/merge_requests/methods/index.md +++ b/doc/user/project/merge_requests/methods/index.md @@ -23,7 +23,26 @@ merge requests are merged into an existing branch. This setting is the default. It always creates a separate merge commit, even when using [squash](../squash_and_merge.md). An example commit graph generated using this merge method: -![Commit graph for merge commits](../img/merge_method_merge_commit_v15_0.png) +```mermaid +gitGraph + commit id: "Init" + branch mr-branch-1 + commit + checkout main + commit + branch mr-branch-2 + commit + checkout mr-branch-1 + commit + checkout main + branch squash-mr + commit id: "Squashed commits" + checkout main + merge squash-mr + merge mr-branch-1 + commit + merge mr-branch-2 +``` - For regular merges, it is equivalent to the command `git merge --no-ff `. - For squash merges, it squashes all commits in the source branch before merging it normally. It performs actions similar to: @@ -42,7 +61,25 @@ A merge commit is created for every merge, but the branch is only merged if a fast-forward merge is possible. This ensures that if the merge request build succeeded, the target branch build also succeeds after the merge. An example commit graph generated using this merge method: -![Commit graph for merge commit with semi-linear history](../img/merge_method_merge_commit_with_semi_linear_history_v15_0.png) +```mermaid +gitGraph + commit id: "Init" + branch mr-branch-1 + commit + commit + checkout main + merge mr-branch-1 + branch mr-branch-2 + commit + commit + checkout main + merge mr-branch-2 + commit + branch squash-mr + commit id: "Squashed commits" + checkout main + merge squash-mr +``` When you visit the merge request page with `Merge commit with semi-linear history` method selected, you can accept it **only if a fast-forward merge is possible**. @@ -63,7 +100,14 @@ fast-forward merge requests, you can retain a linear Git history and a way to accept merge requests without creating merge commits. An example commit graph generated using this merge method: -![Commit graph for fast-forward merge](../img/merge_method_ff_v15_0.png) +```mermaid +gitGraph + commit id: "Init" + commit id: "Merge mr-branch-1" + commit id: "Merge mr-branch-2" + commit id: "Commit on main" + commit id: "Merge squash-mr" +``` This method is equivalent to `git merge --ff ` for regular merges, and to `git merge -squash ` for squash merges. diff --git a/lib/gitlab/ci/reports/test_reports.rb b/lib/gitlab/ci/reports/test_report.rb similarity index 98% rename from lib/gitlab/ci/reports/test_reports.rb rename to lib/gitlab/ci/reports/test_report.rb index a5a630642e5..4fc10dd736e 100644 --- a/lib/gitlab/ci/reports/test_reports.rb +++ b/lib/gitlab/ci/reports/test_report.rb @@ -3,7 +3,7 @@ module Gitlab module Ci module Reports - class TestReports + class TestReport attr_reader :test_suites def initialize diff --git a/lib/gitlab/ci/reports/test_reports_comparer.rb b/lib/gitlab/ci/reports/test_reports_comparer.rb index c6f17f0764f..497831ae5a7 100644 --- a/lib/gitlab/ci/reports/test_reports_comparer.rb +++ b/lib/gitlab/ci/reports/test_reports_comparer.rb @@ -9,7 +9,7 @@ module Gitlab attr_reader :base_reports, :head_reports def initialize(base_reports, head_reports) - @base_reports = base_reports || TestReports.new + @base_reports = base_reports || TestReport.new @head_reports = head_reports end diff --git a/lib/gitlab/database/gitlab_schemas.yml b/lib/gitlab/database/gitlab_schemas.yml index e2ea66fdbc9..4a467d18f0a 100644 --- a/lib/gitlab/database/gitlab_schemas.yml +++ b/lib/gitlab/database/gitlab_schemas.yml @@ -578,3 +578,4 @@ zentao_tracker_data: :gitlab_main dingtalk_tracker_data: :gitlab_main zoom_meetings: :gitlab_main batched_background_migration_job_transition_logs: :gitlab_shared +user_namespace_callouts: :gitlab_main diff --git a/lib/gitlab/database/migrations/test_batched_background_runner.rb b/lib/gitlab/database/migrations/test_batched_background_runner.rb index 0c6a8d3d856..f38d847b0e8 100644 --- a/lib/gitlab/database/migrations/test_batched_background_runner.rb +++ b/lib/gitlab/database/migrations/test_batched_background_runner.rb @@ -14,7 +14,7 @@ module Gitlab def jobs_by_migration_name Gitlab::Database::BackgroundMigration::BatchedMigration .executable - .created_after(2.days.ago) # Simple way to exclude migrations already running before migration testing + .created_after(3.hours.ago) # Simple way to exclude migrations already running before migration testing .to_h do |migration| batching_strategy = migration.batch_class.new(connection: connection) diff --git a/lib/gitlab/dependency_linker/base_linker.rb b/lib/gitlab/dependency_linker/base_linker.rb index 36a840372c5..76855f2950d 100644 --- a/lib/gitlab/dependency_linker/base_linker.rb +++ b/lib/gitlab/dependency_linker/base_linker.rb @@ -6,9 +6,6 @@ module Gitlab URL_REGEX = %r{https?://[^'" ]+}.freeze GIT_INVALID_URL_REGEX = /^git\+#{URL_REGEX}/.freeze REPO_REGEX = %r{[^/'" ]+/[^/'" ]+}.freeze - VALID_LINK_ATTRIBUTES = %w[href rel target].freeze - - include ActionView::Helpers::SanitizeHelper class_attribute :file_type @@ -65,10 +62,9 @@ module Gitlab end def link_tag(name, url) - sanitize( - %{#{ERB::Util.html_escape_once(name)}}, - attributes: VALID_LINK_ATTRIBUTES - ) + href_attribute = %{href="#{ERB::Util.html_escape_once(url)}" } if Gitlab::UrlSanitizer.valid_web?(url) + + %{#{ERB::Util.html_escape_once(name)}}.html_safe end # Links package names based on regex. diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb index f72217dedde..bb5bbeeb27e 100644 --- a/lib/gitlab/git/blob.rb +++ b/lib/gitlab/git/blob.rb @@ -128,6 +128,9 @@ module Gitlab @loaded_size = @data.bytesize if @data @loaded_all_data = @loaded_size == size + # Recalculate binary status if we loaded all data + @binary = nil if @loaded_all_data + record_metric_blob_size record_metric_truncated(truncated?) end diff --git a/lib/gitlab/pagination/keyset/cursor_based_request_context.rb b/lib/gitlab/pagination/keyset/cursor_based_request_context.rb index e06d7e48ca3..41b90846345 100644 --- a/lib/gitlab/pagination/keyset/cursor_based_request_context.rb +++ b/lib/gitlab/pagination/keyset/cursor_based_request_context.rb @@ -5,6 +5,8 @@ module Gitlab module Keyset class CursorBasedRequestContext DEFAULT_SORT_DIRECTION = :desc + DEFAULT_SORT_COLUMN = :id + attr_reader :request_context delegate :params, to: :request_context @@ -28,7 +30,7 @@ module Gitlab end def order_by - { params[:order_by].to_sym => params[:sort]&.to_sym || DEFAULT_SORT_DIRECTION } + { (params[:order_by]&.to_sym || DEFAULT_SORT_COLUMN) => (params[:sort]&.to_sym || DEFAULT_SORT_DIRECTION) } end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e2aaf645b0a..ece81a08f5a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -19590,6 +19590,9 @@ msgstr "" msgid "If no options are selected, only administrators can register runners." msgstr "" +msgid "If none of the options work, try contacting a GitLab administrator." +msgstr "" + msgid "If the number of active users exceeds the user limit, you will be charged for the number of %{users_over_license_link} at your next license reconciliation." msgstr "" @@ -26491,6 +26494,9 @@ msgstr "" msgid "Notes|You're only seeing %{boldStart}other activity%{boldEnd} in the feed. To add a comment, switch to one of the following options." msgstr "" +msgid "Note|The created date provided is too far in the past." +msgstr "" + msgid "Nothing to preview." msgstr "" @@ -31990,6 +31996,9 @@ msgstr "" msgid "Reconfigure" msgstr "" +msgid "Recover password" +msgstr "" + msgid "Recovery Codes" msgstr "" @@ -36255,6 +36264,9 @@ msgstr "" msgid "Sign-in count:" msgstr "" +msgid "Sign-in failed because %{error}." +msgstr "" + msgid "Sign-in page" msgstr "" @@ -36264,6 +36276,9 @@ msgstr "" msgid "Sign-in text" msgstr "" +msgid "Sign-in using %{provider} auth failed" +msgstr "" + msgid "Sign-out page URL" msgstr "" @@ -41103,6 +41118,9 @@ msgstr "" msgid "Try grouping with different labels" msgstr "" +msgid "Try logging in using your username or email. If you have forgotten your password, try recovering it" +msgstr "" + msgid "Try out GitLab Pipelines" msgstr "" @@ -44021,6 +44039,9 @@ msgstr "" msgid "WorkItem|Child items" msgstr "" +msgid "WorkItem|Child removed" +msgstr "" + msgid "WorkItem|Closed" msgstr "" @@ -44051,6 +44072,9 @@ msgstr "" msgid "WorkItem|Open" msgstr "" +msgid "WorkItem|Remove" +msgstr "" + msgid "WorkItem|Select type" msgstr "" @@ -44084,6 +44108,9 @@ msgstr "" msgid "WorkItem|Type" msgstr "" +msgid "WorkItem|Undo" +msgstr "" + msgid "WorkItem|Work Items" msgstr "" diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb index 0f6845dc5ee..dbb5c357acb 100644 --- a/spec/factories/project_hooks.rb +++ b/spec/factories/project_hooks.rb @@ -29,5 +29,9 @@ FactoryBot.define do trait :with_push_branch_filter do push_events_branch_filter { 'my-branch-*' } end + + trait :permanently_disabled do + recent_failures { WebHook::FAILURE_THRESHOLD + 1 } + end end end diff --git a/spec/factories/users/namespace_user_callouts.rb b/spec/factories/users/namespace_user_callouts.rb new file mode 100644 index 00000000000..fded63d0cce --- /dev/null +++ b/spec/factories/users/namespace_user_callouts.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :namespace_callout, class: 'Users::NamespaceCallout' do + feature_name { :invite_members_banner } + + user + namespace + end +end diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index d312965f6cf..44fd21e510a 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -21,7 +21,7 @@ RSpec.describe "Admin Runners" do let_it_be(:namespace) { create(:namespace) } let_it_be(:project) { create(:project, namespace: namespace, creator: user) } - context "runners registration" do + describe "runners registration" do before do visit admin_runners_path end @@ -164,7 +164,9 @@ RSpec.describe "Admin Runners" do end describe 'filter by status' do - let!(:never_contacted) { create(:ci_runner, :instance, description: 'runner-never-contacted', contacted_at: nil) } + let!(:never_contacted) do + create(:ci_runner, :instance, description: 'runner-never-contacted', contacted_at: nil) + end before do create(:ci_runner, :instance, description: 'runner-1', contacted_at: Time.zone.now) @@ -326,13 +328,15 @@ RSpec.describe "Admin Runners" do visit admin_runners_path page.within('[data-testid="runner-type-tabs"]') do click_on 'Instance' - - expect(page).to have_link('Instance', class: 'active') end end it_behaves_like 'shows no runners found' + it 'shows active tab' do + expect(page).to have_link('Instance', class: 'active') + end + it 'shows no runner' do expect(page).not_to have_content 'runner-project' expect(page).not_to have_content 'runner-group' @@ -402,8 +406,8 @@ RSpec.describe "Admin Runners" do end it 'sorts by last contact date' do - create(:ci_runner, :instance, description: 'runner-1', created_at: '2018-07-12 15:37', contacted_at: '2018-07-12 15:37') - create(:ci_runner, :instance, description: 'runner-2', created_at: '2018-07-12 16:37', contacted_at: '2018-07-12 16:37') + create(:ci_runner, :instance, description: 'runner-1', contacted_at: '2018-07-12') + create(:ci_runner, :instance, description: 'runner-2', contacted_at: '2018-07-13') visit admin_runners_path @@ -448,13 +452,13 @@ RSpec.describe "Admin Runners" do it 'updates ACTIVE runner status to paused=false' do visit admin_runners_path('status[]': 'ACTIVE') - expect(page).to have_current_path(admin_runners_path('paused[]': 'false') ) + expect(page).to have_current_path(admin_runners_path('paused[]': 'false')) end it 'updates PAUSED runner status to paused=true' do visit admin_runners_path('status[]': 'PAUSED') - expect(page).to have_current_path(admin_runners_path('paused[]': 'true') ) + expect(page).to have_current_path(admin_runners_path('paused[]': 'true')) end end end @@ -477,7 +481,9 @@ RSpec.describe "Admin Runners" do describe 'runner show page breadcrumbs' do it 'contains the current runner id and token' do page.within '[data-testid="breadcrumb-links"]' do - expect(page.find('[data-testid="breadcrumb-current-link"]')).to have_link("##{runner.id} (#{runner.short_sha})") + expect(page.find('[data-testid="breadcrumb-current-link"]')).to have_link( + "##{runner.id} (#{runner.short_sha})" + ) end end end @@ -515,16 +521,16 @@ RSpec.describe "Admin Runners" do describe "Runner edit page" do let(:runner) { create(:ci_runner, :project) } + let!(:project1) { create(:project) } + let!(:project2) { create(:project) } before do - @project1 = create(:project) - @project2 = create(:project) visit edit_admin_runner_path(runner) wait_for_requests end - describe 'runner edit page breadcrumbs' do + describe 'breadcrumbs' do it 'contains the current runner id and token' do page.within '[data-testid="breadcrumb-links"]' do expect(page).to have_link("##{runner.id} (#{runner.short_sha})") @@ -539,7 +545,7 @@ RSpec.describe "Admin Runners" do end end - describe 'when a runner is updated', :js do + context 'when a runner is updated', :js do before do click_on _('Save changes') wait_for_requests @@ -556,21 +562,21 @@ RSpec.describe "Admin Runners" do describe 'projects' do it 'contains project names' do - expect(page).to have_content(@project1.full_name) - expect(page).to have_content(@project2.full_name) + expect(page).to have_content(project1.full_name) + expect(page).to have_content(project2.full_name) end end describe 'search' do before do search_form = find('#runner-projects-search') - search_form.fill_in 'search', with: @project1.name + search_form.fill_in 'search', with: project1.name search_form.click_button 'Search' end it 'contains name of correct project' do - expect(page).to have_content(@project1.full_name) - expect(page).not_to have_content(@project2.full_name) + expect(page).to have_content(project1.full_name) + expect(page).not_to have_content(project2.full_name) end end @@ -584,12 +590,12 @@ RSpec.describe "Admin Runners" do assigned_project = page.find('[data-testid="assigned-projects"]') expect(page).to have_content('Runner assigned to project.') - expect(assigned_project).to have_content(@project2.path) + expect(assigned_project).to have_content(project2.path) end end context 'with specific runner' do - let(:runner) { create(:ci_runner, :project, projects: [@project1]) } + let(:runner) { create(:ci_runner, :project, projects: [project1]) } before do visit edit_admin_runner_path(runner) @@ -599,7 +605,7 @@ RSpec.describe "Admin Runners" do end context 'with locked runner' do - let(:runner) { create(:ci_runner, :project, projects: [@project1], locked: true) } + let(:runner) { create(:ci_runner, :project, projects: [project1], locked: true) } before do visit edit_admin_runner_path(runner) @@ -610,7 +616,7 @@ RSpec.describe "Admin Runners" do end describe 'disable/destroy' do - let(:runner) { create(:ci_runner, :project, projects: [@project1]) } + let(:runner) { create(:ci_runner, :project, projects: [project1]) } before do visit edit_admin_runner_path(runner) @@ -624,7 +630,7 @@ RSpec.describe "Admin Runners" do new_runner_project = page.find('[data-testid="unassigned-projects"]') expect(page).to have_content('Runner unassigned from project.') - expect(new_runner_project).to have_content(@project1.path) + expect(new_runner_project).to have_content(project1.path) end end end diff --git a/spec/features/groups/group_runners_spec.rb b/spec/features/groups/group_runners_spec.rb index b6aaab207ce..a129db6cb6f 100644 --- a/spec/features/groups/group_runners_spec.rb +++ b/spec/features/groups/group_runners_spec.rb @@ -17,7 +17,7 @@ RSpec.describe "Group Runners" do describe "Group runners page", :js do let!(:group_registration_token) { group.runners_token } - context "runners registration" do + describe "runners registration" do before do visit group_runners_path(group) end @@ -128,7 +128,7 @@ RSpec.describe "Group Runners" do end end - context 'filtered search' do + describe 'filtered search' do before do visit group_runners_path(group) end diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb index 2dafd66b406..1d3effd4a2a 100644 --- a/spec/features/merge_request/user_sees_merge_widget_spec.rb +++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb @@ -591,14 +591,14 @@ RSpec.describe 'Merge request > User sees merge widget', :js do context 'when a new failures exists' do let(:base_reports) do - Gitlab::Ci::Reports::TestReports.new.tap do |reports| + Gitlab::Ci::Reports::TestReport.new.tap do |reports| reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) reports.get_suite('junit').add_test_case(create_test_case_java_success) end end let(:head_reports) do - Gitlab::Ci::Reports::TestReports.new.tap do |reports| + Gitlab::Ci::Reports::TestReport.new.tap do |reports| reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) reports.get_suite('junit').add_test_case(create_test_case_java_failed) end @@ -639,14 +639,14 @@ RSpec.describe 'Merge request > User sees merge widget', :js do context 'when an existing failure exists' do let(:base_reports) do - Gitlab::Ci::Reports::TestReports.new.tap do |reports| + Gitlab::Ci::Reports::TestReport.new.tap do |reports| reports.get_suite('rspec').add_test_case(create_test_case_rspec_failed) reports.get_suite('junit').add_test_case(create_test_case_java_success) end end let(:head_reports) do - Gitlab::Ci::Reports::TestReports.new.tap do |reports| + Gitlab::Ci::Reports::TestReport.new.tap do |reports| reports.get_suite('rspec').add_test_case(create_test_case_rspec_failed) reports.get_suite('junit').add_test_case(create_test_case_java_success) end @@ -686,14 +686,14 @@ RSpec.describe 'Merge request > User sees merge widget', :js do context 'when a resolved failure exists' do let(:base_reports) do - Gitlab::Ci::Reports::TestReports.new.tap do |reports| + Gitlab::Ci::Reports::TestReport.new.tap do |reports| reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) reports.get_suite('junit').add_test_case(create_test_case_java_failed) end end let(:head_reports) do - Gitlab::Ci::Reports::TestReports.new.tap do |reports| + Gitlab::Ci::Reports::TestReport.new.tap do |reports| reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) reports.get_suite('junit').add_test_case(create_test_case_java_success) end @@ -732,14 +732,14 @@ RSpec.describe 'Merge request > User sees merge widget', :js do context 'when a new error exists' do let(:base_reports) do - Gitlab::Ci::Reports::TestReports.new.tap do |reports| + Gitlab::Ci::Reports::TestReport.new.tap do |reports| reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) reports.get_suite('junit').add_test_case(create_test_case_java_success) end end let(:head_reports) do - Gitlab::Ci::Reports::TestReports.new.tap do |reports| + Gitlab::Ci::Reports::TestReport.new.tap do |reports| reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) reports.get_suite('junit').add_test_case(create_test_case_java_error) end @@ -779,14 +779,14 @@ RSpec.describe 'Merge request > User sees merge widget', :js do context 'when an existing error exists' do let(:base_reports) do - Gitlab::Ci::Reports::TestReports.new.tap do |reports| + Gitlab::Ci::Reports::TestReport.new.tap do |reports| reports.get_suite('rspec').add_test_case(create_test_case_rspec_error) reports.get_suite('junit').add_test_case(create_test_case_java_success) end end let(:head_reports) do - Gitlab::Ci::Reports::TestReports.new.tap do |reports| + Gitlab::Ci::Reports::TestReport.new.tap do |reports| reports.get_suite('rspec').add_test_case(create_test_case_rspec_error) reports.get_suite('junit').add_test_case(create_test_case_java_success) end @@ -825,14 +825,14 @@ RSpec.describe 'Merge request > User sees merge widget', :js do context 'when a resolved error exists' do let(:base_reports) do - Gitlab::Ci::Reports::TestReports.new.tap do |reports| + Gitlab::Ci::Reports::TestReport.new.tap do |reports| reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) reports.get_suite('junit').add_test_case(create_test_case_java_error) end end let(:head_reports) do - Gitlab::Ci::Reports::TestReports.new.tap do |reports| + Gitlab::Ci::Reports::TestReport.new.tap do |reports| reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) reports.get_suite('junit').add_test_case(create_test_case_java_success) end @@ -871,7 +871,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do context 'properly truncates the report' do let(:base_reports) do - Gitlab::Ci::Reports::TestReports.new.tap do |reports| + Gitlab::Ci::Reports::TestReport.new.tap do |reports| 10.times do |index| reports.get_suite('rspec').add_test_case( create_test_case_rspec_failed(index)) @@ -882,7 +882,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do end let(:head_reports) do - Gitlab::Ci::Reports::TestReports.new.tap do |reports| + Gitlab::Ci::Reports::TestReport.new.tap do |reports| 10.times do |index| reports.get_suite('rspec').add_test_case( create_test_case_rspec_failed(index)) diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb index e222f0d34a9..f5cafa2b2ec 100644 --- a/spec/features/projects/blobs/blob_show_spec.rb +++ b/spec/features/projects/blobs/blob_show_spec.rb @@ -464,6 +464,20 @@ RSpec.describe 'File blob', :js do end end + context 'binary file that appears to be text in the first 1024 bytes' do + before do + visit_blob('encoding/binary-1.bin', ref: 'binary-encoding') + end + + it 'displays the blob' do + expect(page).to have_link('Download (23.81 KiB)') + # does not show a viewer switcher + expect(page).not_to have_selector('.js-blob-viewer-switcher') + expect(page).not_to have_selector('.js-copy-blob-source-btn:not(.disabled)') + expect(page).not_to have_link('Open raw') + end + end + context 'empty file' do before do project.add_maintainer(project.creator) diff --git a/spec/features/projects/diffs/diff_show_spec.rb b/spec/features/projects/diffs/diff_show_spec.rb index 56506ada3ce..dcd6f1239bb 100644 --- a/spec/features/projects/diffs/diff_show_spec.rb +++ b/spec/features/projects/diffs/diff_show_spec.rb @@ -169,8 +169,8 @@ RSpec.describe 'Diff file viewer', :js, :with_clean_rails_cache do wait_for_requests end - it 'shows there is no preview' do - expect(page).to have_content('No preview for this file type') + it 'shows that file was added' do + expect(page).to have_content('File added') end end end diff --git a/spec/frontend/issuable/linked_resources/components/__snapshots__/resource_links_block_spec.js.snap b/spec/frontend/issuable/linked_resources/components/__snapshots__/resource_links_block_spec.js.snap deleted file mode 100644 index 2ccfe4f91e7..00000000000 --- a/spec/frontend/issuable/linked_resources/components/__snapshots__/resource_links_block_spec.js.snap +++ /dev/null @@ -1,215 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ResourceLinksBlock with defaults renders correct component 1`] = ` - -`; diff --git a/spec/frontend/issuable/linked_resources/components/add_issuable_resource_link_form_spec.js b/spec/frontend/issuable/linked_resources/components/add_issuable_resource_link_form_spec.js deleted file mode 100644 index b3707569848..00000000000 --- a/spec/frontend/issuable/linked_resources/components/add_issuable_resource_link_form_spec.js +++ /dev/null @@ -1,61 +0,0 @@ -import { nextTick } from 'vue'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import AddIssuableResourceLinkForm from '~/linked_resources/components/add_issuable_resource_link_form.vue'; - -describe('AddIssuableResourceLinkForm', () => { - let wrapper; - - const mountComponent = () => { - wrapper = mountExtended(AddIssuableResourceLinkForm); - }; - - afterEach(() => { - if (wrapper) { - wrapper.destroy(); - } - }); - - const findAddButton = () => wrapper.findByTestId('add-button'); - const findCancelButton = () => wrapper.findByText('Cancel'); - const findLinkTextInput = () => wrapper.findByTestId('link-text-input'); - const findLinkValueInput = () => wrapper.findByTestId('link-value-input'); - - const cancelForm = async () => { - await findCancelButton().trigger('click'); - }; - - describe('cancel form button', () => { - const closeFormEvent = { 'add-issuable-resource-link-form-cancel': [[]] }; - - beforeEach(() => { - mountComponent(); - }); - - it('should close the form on cancel', async () => { - await cancelForm(); - - expect(wrapper.emitted()).toEqual(closeFormEvent); - }); - - it('keeps the button disabled without input', () => { - expect(findAddButton().props('disabled')).toBe(true); - }); - - it('keeps the button disabled with only text input', async () => { - findLinkTextInput().setValue('link text'); - - await nextTick(); - - expect(findAddButton().props('disabled')).toBe(true); - }); - - it('enables add button when link input is provided', async () => { - findLinkTextInput().setValue('link text'); - findLinkValueInput().setValue('https://foo.example.com'); - - await nextTick(); - - expect(findAddButton().props('disabled')).toBe(false); - }); - }); -}); diff --git a/spec/frontend/issuable/linked_resources/components/resource_links_block_spec.js b/spec/frontend/issuable/linked_resources/components/resource_links_block_spec.js deleted file mode 100644 index bca63a34b2e..00000000000 --- a/spec/frontend/issuable/linked_resources/components/resource_links_block_spec.js +++ /dev/null @@ -1,90 +0,0 @@ -import { GlButton } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import ResourceLinksBlock from '~/linked_resources/components/resource_links_block.vue'; -import AddIssuableResourceLinkForm from '~/linked_resources/components/add_issuable_resource_link_form.vue'; - -describe('ResourceLinksBlock', () => { - let wrapper; - - const findResourceLinkAddButton = () => wrapper.find(GlButton); - const resourceLinkForm = () => wrapper.findComponent(AddIssuableResourceLinkForm); - const helpPath = '/help/user/project/issues/linked_resources'; - - const mountComponent = () => { - wrapper = mountExtended(ResourceLinksBlock, { - propsData: { - helpPath, - canAddResourceLinks: true, - }, - data() { - return { - isFormVisible: false, - }; - }, - }); - - afterEach(() => { - if (wrapper) { - wrapper.destroy(); - } - }); - }; - - describe('with defaults', () => { - beforeEach(() => { - mountComponent(); - }); - - it('renders correct component', () => { - expect(wrapper.element).toMatchSnapshot(); - }); - - it('should show the form when add button is clicked', async () => { - await findResourceLinkAddButton().trigger('click'); - - expect(resourceLinkForm().isVisible()).toBe(true); - }); - - it('should hide the form when the hide event is emitted', async () => { - // open the form - await findResourceLinkAddButton().trigger('click'); - - await resourceLinkForm().vm.$emit('add-issuable-resource-link-form-cancel'); - - expect(resourceLinkForm().isVisible()).toBe(false); - }); - }); - - describe('with canAddResourceLinks=false', () => { - it('does not show the add button', () => { - wrapper = shallowMount(ResourceLinksBlock, { - propsData: { - canAddResourceLinks: false, - }, - }); - - expect(findResourceLinkAddButton().exists()).toBe(false); - expect(resourceLinkForm().isVisible()).toBe(false); - }); - }); - - describe('with isFormVisible=true', () => { - it('renders the form with correct props', () => { - wrapper = shallowMount(ResourceLinksBlock, { - propsData: { - canAddResourceLinks: true, - }, - data() { - return { - isFormVisible: true, - isSubmitting: false, - }; - }, - }); - - expect(resourceLinkForm().exists()).toBe(true); - expect(resourceLinkForm().props('isSubmitting')).toBe(false); - }); - }); -}); diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js index 0581a40b6a2..a5b2b1d7cf8 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js @@ -109,5 +109,17 @@ describe('cleanup_status', () => { expect(findPopover().findComponent(GlLink).exists()).toBe(true); expect(findPopover().findComponent(GlLink).attributes('href')).toBe(cleanupPolicyHelpPage); }); + + it('id matches popover target attribute', () => { + mountComponent({ + status: UNFINISHED_STATUS, + next_run_at: '2063-04-08T01:44:03Z', + }); + + const id = findExtraInfoIcon().attributes('id'); + + expect(id).toMatch(/status-info-[0-9]+/); + expect(findPopover().props('target')).toEqual(id); + }); }); }); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js new file mode 100644 index 00000000000..f8471b7f167 --- /dev/null +++ b/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js @@ -0,0 +1,141 @@ +import Vue from 'vue'; +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { cloneDeep } from 'lodash'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import WorkItemLinksMenu from '~/work_items/components/work_item_links/work_item_links_menu.vue'; +import changeWorkItemParentMutation from '~/work_items/graphql/change_work_item_parent_link.mutation.graphql'; +import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql'; +import { WIDGET_TYPE_HIERARCHY } from '~/work_items/constants'; +import { workItemHierarchyResponse, changeWorkItemParentMutationResponse } from '../../mock_data'; + +Vue.use(VueApollo); + +const PARENT_ID = 'gid://gitlab/WorkItem/1'; +const WORK_ITEM_ID = 'gid://gitlab/WorkItem/3'; + +describe('WorkItemLinksMenu', () => { + let wrapper; + let mockApollo; + + const $toast = { + show: jest.fn(), + }; + + const createComponent = async ({ + data = {}, + mutationHandler = jest.fn().mockResolvedValue(changeWorkItemParentMutationResponse), + } = {}) => { + mockApollo = createMockApollo([ + [getWorkItemLinksQuery, jest.fn().mockResolvedValue(workItemHierarchyResponse)], + [changeWorkItemParentMutation, mutationHandler], + ]); + + mockApollo.clients.defaultClient.cache.writeQuery({ + query: getWorkItemLinksQuery, + variables: { + id: PARENT_ID, + }, + data: workItemHierarchyResponse.data, + }); + + wrapper = shallowMountExtended(WorkItemLinksMenu, { + data() { + return { + ...data, + }; + }, + propsData: { + workItemId: WORK_ITEM_ID, + parentWorkItemId: PARENT_ID, + }, + apolloProvider: mockApollo, + mocks: { + $toast, + }, + }); + + await waitForPromises(); + }; + + const findDropdown = () => wrapper.find(GlDropdown); + const findRemoveDropdownItem = () => wrapper.find(GlDropdownItem); + + beforeEach(async () => { + await createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + mockApollo = null; + }); + + it('renders dropdown and dropdown items', () => { + expect(findDropdown().exists()).toBe(true); + expect(findRemoveDropdownItem().exists()).toBe(true); + }); + + it('calls correct mutation with correct variables', async () => { + const mutationHandler = jest.fn().mockResolvedValue(changeWorkItemParentMutationResponse); + + createComponent({ mutationHandler }); + + findRemoveDropdownItem().vm.$emit('click'); + + await waitForPromises(); + + expect(mutationHandler).toHaveBeenCalledWith({ + id: WORK_ITEM_ID, + parentId: null, + }); + }); + + it('shows toast when mutation succeeds', async () => { + const mutationHandler = jest.fn().mockResolvedValue(changeWorkItemParentMutationResponse); + + createComponent({ mutationHandler }); + + findRemoveDropdownItem().vm.$emit('click'); + + await waitForPromises(); + + expect($toast.show).toHaveBeenCalledWith('Child removed', { + action: { onClick: expect.anything(), text: 'Undo' }, + }); + }); + + it('updates the cache when mutation succeeds', async () => { + const mutationHandler = jest.fn().mockResolvedValue(changeWorkItemParentMutationResponse); + + createComponent({ mutationHandler }); + + mockApollo.clients.defaultClient.cache.readQuery = jest.fn( + () => workItemHierarchyResponse.data, + ); + + mockApollo.clients.defaultClient.cache.writeQuery = jest.fn(); + + findRemoveDropdownItem().vm.$emit('click'); + + await waitForPromises(); + + // Remove the work item from parent's children + const resp = cloneDeep(workItemHierarchyResponse); + const index = resp.data.workItem.widgets + .find((widget) => widget.type === WIDGET_TYPE_HIERARCHY) + .children.nodes.findIndex((child) => child.id === WORK_ITEM_ID); + resp.data.workItem.widgets + .find((widget) => widget.type === WIDGET_TYPE_HIERARCHY) + .children.nodes.splice(index, 1); + + expect(mockApollo.clients.defaultClient.cache.writeQuery).toHaveBeenCalledWith( + expect.objectContaining({ + query: expect.anything(), + variables: { id: PARENT_ID }, + data: resp.data, + }), + ); + }); +}); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index f33d884144b..0359caf7116 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -392,6 +392,25 @@ export const workItemHierarchyResponse = { }, }; +export const changeWorkItemParentMutationResponse = { + data: { + workItemUpdate: { + workItem: { + id: 'gid://gitlab/WorkItem/2', + workItemType: { + id: 'gid://gitlab/WorkItems::Type/5', + __typename: 'WorkItemType', + }, + title: 'Foo', + state: 'OPEN', + __typename: 'WorkItem', + }, + errors: [], + __typename: 'WorkItemUpdatePayload', + }, + }, +}; + export const availableWorkItemsResponse = { data: { workspace: { diff --git a/spec/helpers/users/callouts_helper_spec.rb b/spec/helpers/users/callouts_helper_spec.rb index 71a8d340b30..2c148aabead 100644 --- a/spec/helpers/users/callouts_helper_spec.rb +++ b/spec/helpers/users/callouts_helper_spec.rb @@ -222,4 +222,60 @@ RSpec.describe Users::CalloutsHelper do it { is_expected.to be true } end end + + describe '#web_hook_disabled_dismissed?' do + context 'without a project' do + it 'is false' do + expect(helper).not_to be_web_hook_disabled_dismissed(nil) + end + end + + context 'with a project' do + let_it_be(:project) { create(:project) } + + context 'the web-hook failure callout has never been dismissed' do + it 'is false' do + expect(helper).not_to be_web_hook_disabled_dismissed(project) + end + end + + context 'the web-hook failure callout has been dismissed', :freeze_time do + before do + create(:namespace_callout, + feature_name: described_class::WEB_HOOK_DISABLED, + user: user, + namespace: project.namespace, + dismissed_at: 1.week.ago) + end + + it 'is true' do + expect(helper).to be_web_hook_disabled_dismissed(project) + end + + context 'when there was an older failure', :clean_gitlab_redis_shared_state do + let(:key) { "web_hooks:last_failure:project-#{project.id}" } + + before do + Gitlab::Redis::SharedState.with { |r| r.set(key, 1.month.ago.iso8601) } + end + + it 'is true' do + expect(helper).to be_web_hook_disabled_dismissed(project) + end + end + + context 'when there has been a more recent failure', :clean_gitlab_redis_shared_state do + let(:key) { "web_hooks:last_failure:project-#{project.id}" } + + before do + Gitlab::Redis::SharedState.with { |r| r.set(key, 1.day.ago.iso8601) } + end + + it 'is false' do + expect(helper).not_to be_web_hook_disabled_dismissed(project) + end + end + end + end + end end diff --git a/spec/helpers/web_hooks/web_hooks_helper_spec.rb b/spec/helpers/web_hooks/web_hooks_helper_spec.rb new file mode 100644 index 00000000000..473f33a982f --- /dev/null +++ b/spec/helpers/web_hooks/web_hooks_helper_spec.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WebHooks::WebHooksHelper do + let_it_be_with_reload(:project) { create(:project) } + + let(:current_user) { nil } + let(:callout_dismissed) { false } + let(:web_hooks_disable_failed) { false } + let(:webhooks_failed_callout) { false } + + before do + allow(helper).to receive(:current_user).and_return(current_user) + allow(helper).to receive(:web_hook_disabled_dismissed?).with(project).and_return(callout_dismissed) + + stub_feature_flags( + webhooks_failed_callout: webhooks_failed_callout, + web_hooks_disable_failed: web_hooks_disable_failed + ) + end + + shared_context 'user is logged in' do + let(:current_user) { create(:user) } + end + + shared_context 'webhooks_failed_callout is enabled' do + let(:webhooks_failed_callout) { true } + end + + shared_context 'webhooks_failed_callout is enabled for this project' do + let(:webhooks_failed_callout) { project } + end + + shared_context 'web_hooks_disable_failed is enabled' do + let(:web_hooks_disable_failed) { true } + end + + shared_context 'web_hooks_disable_failed is enabled for this project' do + let(:web_hooks_disable_failed) { project } + end + + shared_context 'the user has permission' do + before do + project.add_maintainer(current_user) + end + end + + shared_context 'the user dismissed the callout' do + let(:callout_dismissed) { true } + end + + shared_context 'a hook has failed' do + before do + create(:project_hook, :permanently_disabled, project: project) + end + end + + describe '#show_project_hook_failed_callout?' do + context 'all conditions are met' do + include_context 'user is logged in' + include_context 'webhooks_failed_callout is enabled' + include_context 'web_hooks_disable_failed is enabled' + include_context 'the user has permission' + include_context 'a hook has failed' + + it 'is true' do + expect(helper).to be_show_project_hook_failed_callout(project: project) + end + + it 'caches the DB calls until the TTL', :use_clean_rails_memory_store_caching, :request_store do + helper.show_project_hook_failed_callout?(project: project) + + travel_to((described_class::EXPIRY_TTL - 1.second).from_now) do + expect do + helper.show_project_hook_failed_callout?(project: project) + end.not_to exceed_query_limit(0) + end + + travel_to((described_class::EXPIRY_TTL + 1.second).from_now) do + expect do + helper.show_project_hook_failed_callout?(project: project) + end.to exceed_query_limit(0) + end + end + end + + context 'all conditions are met, project scoped flags' do + include_context 'user is logged in' + include_context 'webhooks_failed_callout is enabled for this project' + include_context 'web_hooks_disable_failed is enabled for this project' + include_context 'the user has permission' + include_context 'a hook has failed' + + it 'is true' do + expect(helper).to be_show_project_hook_failed_callout(project: project) + end + end + + context 'one condition is not met' do + contexts = [ + 'user is logged in', + 'webhooks_failed_callout is enabled', + 'web_hooks_disable_failed is enabled', + 'the user has permission', + 'a hook has failed' + ] + + contexts.each do |name| + context "namely #{name}" do + contexts.each { |ctx| include_context(ctx) unless ctx == name } + + it 'is false' do + expect(helper).not_to be_show_project_hook_failed_callout(project: project) + end + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/reports/test_reports_spec.rb b/spec/lib/gitlab/ci/reports/test_report_spec.rb similarity index 99% rename from spec/lib/gitlab/ci/reports/test_reports_spec.rb rename to spec/lib/gitlab/ci/reports/test_report_spec.rb index 24c00de3731..539510bca9e 100644 --- a/spec/lib/gitlab/ci/reports/test_reports_spec.rb +++ b/spec/lib/gitlab/ci/reports/test_report_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Reports::TestReports do +RSpec.describe Gitlab::Ci::Reports::TestReport do include TestReportsHelper let(:test_reports) { described_class.new } diff --git a/spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb index 3483dddca3a..ac64e4699fe 100644 --- a/spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb +++ b/spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb @@ -6,8 +6,8 @@ RSpec.describe Gitlab::Ci::Reports::TestReportsComparer do include TestReportsHelper let(:comparer) { described_class.new(base_reports, head_reports) } - let(:base_reports) { Gitlab::Ci::Reports::TestReports.new } - let(:head_reports) { Gitlab::Ci::Reports::TestReports.new } + let(:base_reports) { Gitlab::Ci::Reports::TestReport.new } + let(:head_reports) { Gitlab::Ci::Reports::TestReport.new } describe '#suite_comparers' do subject { comparer.suite_comparers } diff --git a/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb b/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb index 2f3d44f6f8f..f1f72d71e1a 100644 --- a/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb +++ b/spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb @@ -68,10 +68,10 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez end context 'with multiple jobs to run' do - it 'runs all jobs created within the last 48 hours' do + it 'runs all jobs created within the last 3 hours' do old_migration = define_background_migration(migration_name) - travel 3.days + travel 4.hours new_migration = define_background_migration('NewMigration') { travel 1.second } migration.queue_batched_background_migration('NewMigration', table_name, :id, diff --git a/spec/lib/gitlab/dependency_linker/base_linker_spec.rb b/spec/lib/gitlab/dependency_linker/base_linker_spec.rb index 678d4a90e8d..2811bc859da 100644 --- a/spec/lib/gitlab/dependency_linker/base_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/base_linker_spec.rb @@ -33,7 +33,7 @@ RSpec.describe Gitlab::DependencyLinker::BaseLinker do it 'only converts valid links' do expect(subject).to eq( <<~CONTENT - #{link('http://')}#{link('\n', url: '%5Cn')}#{link('javascript:alert(1)', url: nil)} + #{link('http://', url: nil)}#{link('\n', url: nil)}#{link('javascript:alert(1)', url: nil)} #{link('https://gitlab.com/gitlab-org/gitlab')} CONTENT ) diff --git a/spec/migrations/20220715163254_update_notes_in_past_spec.rb b/spec/migrations/20220715163254_update_notes_in_past_spec.rb new file mode 100644 index 00000000000..58e6cabc129 --- /dev/null +++ b/spec/migrations/20220715163254_update_notes_in_past_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_migration! + +RSpec.describe UpdateNotesInPast, :migration do + let(:notes) { table(:notes) } + + it 'updates created_at when it is too much in the past' do + notes.create!(id: 10, note: 'note', created_at: '2009-06-01') + notes.create!(id: 11, note: 'note', created_at: '1970-01-01') + notes.create!(id: 12, note: 'note', created_at: '1600-06-01') + + migrate! + + expect(notes.all).to contain_exactly( + an_object_having_attributes(id: 10, created_at: DateTime.parse('2009-06-01')), + an_object_having_attributes(id: 11, created_at: DateTime.parse('1970-01-01')), + an_object_having_attributes(id: 12, created_at: DateTime.parse('1970-01-01')) + ) + end +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 6d04fb86d22..e0166ba64a4 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -4281,7 +4281,7 @@ RSpec.describe Ci::Build do describe '#collect_test_reports!' do subject { build.collect_test_reports!(test_reports) } - let(:test_reports) { Gitlab::Ci::Reports::TestReports.new } + let(:test_reports) { Gitlab::Ci::Reports::TestReport.new } it { expect(test_reports.get_suite(build.name).total_count).to eq(0) } @@ -4332,7 +4332,7 @@ RSpec.describe Ci::Build do context 'when build is part of parallel build' do let(:build_1) { create(:ci_build, name: 'build 1/2') } - let(:test_report) { Gitlab::Ci::Reports::TestReports.new } + let(:test_report) { Gitlab::Ci::Reports::TestReport.new } before do build_1.collect_test_reports!(test_report) @@ -4356,7 +4356,7 @@ RSpec.describe Ci::Build do end context 'when build is part of matrix build' do - let(:test_report) { Gitlab::Ci::Reports::TestReports.new } + let(:test_report) { Gitlab::Ci::Reports::TestReport.new } let(:matrix_build_1) { create(:ci_build, :matrix) } before do diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb index 4253686b843..923a6f92424 100644 --- a/spec/models/hooks/project_hook_spec.rb +++ b/spec/models/hooks/project_hook_spec.rb @@ -15,6 +15,19 @@ RSpec.describe ProjectHook do subject { build(:project_hook, project: create(:project)) } end + describe '.for_projects' do + it 'finds related project hooks' do + hook_a = create(:project_hook) + hook_b = create(:project_hook) + hook_c = create(:project_hook) + + expect(described_class.for_projects([hook_a.project, hook_b.project])) + .to contain_exactly(hook_a, hook_b) + expect(described_class.for_projects(hook_c.project)) + .to contain_exactly(hook_c) + end + end + describe '.push_hooks' do it 'returns hooks for push events only' do hook = create(:project_hook, push_events: true) @@ -50,4 +63,62 @@ RSpec.describe ProjectHook do ) end end + + describe '#update_last_failure', :clean_gitlab_redis_shared_state do + let_it_be(:hook) { create(:project_hook) } + + it 'is a method of this class' do + expect { hook.update_last_failure }.not_to raise_error + end + + context 'when the hook is executable' do + it 'does not update the state' do + expect(Gitlab::Redis::SharedState).not_to receive(:with) + + hook.update_last_failure + end + end + + context 'when the hook is failed' do + before do + allow(hook).to receive(:executable?).and_return(false) + end + + def last_failure + Gitlab::Redis::SharedState.with do |redis| + redis.get("web_hooks:last_failure:project-#{hook.project.id}") + end + end + + context 'there is no prior value', :freeze_time do + it 'updates the state' do + expect { hook.update_last_failure }.to change { last_failure }.to(Time.current) + end + end + + context 'there is a prior value, from before now' do + it 'updates the state' do + the_future = 1.minute.from_now + + hook.update_last_failure + + travel_to(the_future) do + expect { hook.update_last_failure }.to change { last_failure }.to(the_future.iso8601) + end + end + end + + context 'there is a prior value, from after now' do + it 'does not update the state' do + the_past = 1.minute.ago + + hook.update_last_failure + + travel_to(the_past) do + expect { hook.update_last_failure }.not_to change { last_failure } + end + end + end + end + end end diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb index fb3968777bf..9faa5e1567c 100644 --- a/spec/models/hooks/web_hook_spec.rb +++ b/spec/models/hooks/web_hook_spec.rb @@ -187,8 +187,8 @@ RSpec.describe WebHook do end end - describe '.executable' do - let(:not_executable) do + describe '.executable/.disabled' do + let!(:not_executable) do [ [0, Time.current], [0, 1.minute.from_now], @@ -202,7 +202,7 @@ RSpec.describe WebHook do end end - let(:executables) do + let!(:executables) do [ [0, nil], [0, 1.day.ago], @@ -217,6 +217,7 @@ RSpec.describe WebHook do it 'finds the correct set of project hooks' do expect(described_class.where(project_id: project.id).executable).to match_array executables + expect(described_class.where(project_id: project.id).disabled).to match_array not_executable end context 'when the feature flag is not enabled' do @@ -224,7 +225,7 @@ RSpec.describe WebHook do stub_feature_flags(web_hooks_disable_failed: false) end - it 'is the same as all' do + specify 'enabled is the same as all' do expect(described_class.where(project_id: project.id).executable).to match_array(executables + not_executable) end end @@ -635,4 +636,10 @@ RSpec.describe WebHook do end end end + + describe '#update_last_failure' do + it 'is a method of this class' do + expect { described_class.new.update_last_failure }.not_to raise_error + end + end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 4b262c1f3a9..fc6f7832c2c 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -106,6 +106,22 @@ RSpec.describe Note do end end + describe 'created_at in the past' do + let_it_be(:noteable) { create(:issue) } + + context 'when creating a note not too much in the past' do + subject { build(:note, project: noteable.project, noteable: noteable, created_at: '1990-05-06') } + + it { is_expected.to be_valid } + end + + context 'when creating a note too much in the past' do + subject { build(:note, project: noteable.project, noteable: noteable, created_at: '1600-05-06') } + + it { is_expected.not_to be_valid } + end + end + describe 'confidentiality' do context 'for existing public note' do let_it_be(:existing_note) { create(:note) } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index d3b229c2094..6d2ba66d5f4 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -136,6 +136,7 @@ RSpec.describe User do it { is_expected.to have_many(:timelogs) } it { is_expected.to have_many(:callouts).class_name('Users::Callout') } it { is_expected.to have_many(:group_callouts).class_name('Users::GroupCallout') } + it { is_expected.to have_many(:namespace_callouts).class_name('Users::NamespaceCallout') } describe '#user_detail' do it 'does not persist `user_detail` by default' do @@ -6415,6 +6416,96 @@ RSpec.describe User do end end + describe 'Users::NamespaceCallout' do + describe '#dismissed_callout_for_namespace?' do + let_it_be(:user, refind: true) { create(:user) } + let_it_be(:namespace) { create(:namespace) } + let_it_be(:feature_name) { Users::NamespaceCallout.feature_names.each_key.first } + + let(:query) do + { feature_name: feature_name, namespace: namespace } + end + + def have_dismissed_callout + be_dismissed_callout_for_namespace(**query) + end + + context 'when no callout dismissal record exists' do + it 'returns false when no ignore_dismissal_earlier_than provided' do + expect(user).not_to have_dismissed_callout + end + end + + context 'when dismissed callout exists' do + before_all do + create(:namespace_callout, + user: user, + namespace_id: namespace.id, + feature_name: feature_name, + dismissed_at: 4.months.ago) + end + + it 'returns true when no ignore_dismissal_earlier_than provided' do + expect(user).to have_dismissed_callout + end + + it 'returns true when ignore_dismissal_earlier_than is earlier than dismissed_at' do + query[:ignore_dismissal_earlier_than] = 6.months.ago + + expect(user).to have_dismissed_callout + end + + it 'returns false when ignore_dismissal_earlier_than is later than dismissed_at' do + query[:ignore_dismissal_earlier_than] = 2.months.ago + + expect(user).not_to have_dismissed_callout + end + end + end + + describe '#find_or_initialize_namespace_callout' do + let_it_be(:user, refind: true) { create(:user) } + let_it_be(:namespace) { create(:namespace) } + let_it_be(:feature_name) { Users::NamespaceCallout.feature_names.each_key.first } + + subject(:callout_with_source) do + user.find_or_initialize_namespace_callout(feature_name, namespace.id) + end + + context 'when callout exists' do + let!(:callout) do + create(:namespace_callout, user: user, feature_name: feature_name, namespace_id: namespace.id) + end + + it 'returns existing callout' do + expect(callout_with_source).to eq(callout) + end + end + + context 'when callout does not exist' do + context 'when feature name is valid' do + it 'initializes a new callout' do + expect(callout_with_source) + .to be_a_new(Users::NamespaceCallout) + .and be_valid + end + end + + context 'when feature name is not valid' do + let(:feature_name) { 'notvalid' } + + it 'initializes a new callout' do + expect(callout_with_source).to be_a_new(Users::NamespaceCallout) + end + + it 'is not valid' do + expect(callout_with_source).not_to be_valid + end + end + end + end + end + describe '#dismissed_callout_for_group?' do let_it_be(:user, refind: true) { create(:user) } let_it_be(:group) { create(:group) } diff --git a/spec/models/users/namespace_callout_spec.rb b/spec/models/users/namespace_callout_spec.rb new file mode 100644 index 00000000000..f8207f2abc8 --- /dev/null +++ b/spec/models/users/namespace_callout_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Users::NamespaceCallout do + let_it_be(:user) { create_default(:user) } + let_it_be(:namespace) { create_default(:namespace) } + let_it_be(:callout) { create(:namespace_callout) } + + it_behaves_like 'having unique enum values' + + describe 'relationships' do + it { is_expected.to belong_to(:namespace) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:namespace) } + it { is_expected.to validate_presence_of(:user) } + it { is_expected.to validate_presence_of(:feature_name) } + + specify do + is_expected.to validate_uniqueness_of(:feature_name) + .scoped_to(:user_id, :namespace_id) + .ignoring_case_sensitivity + end + + it { is_expected.to allow_value(:web_hook_disabled).for(:feature_name) } + + it 'rejects invalid feature names' do + expect { callout.feature_name = :non_existent_feature }.to raise_error(ArgumentError) + end + end + + describe '#source_feature_name' do + it 'provides string based off source and feature' do + expect(callout.source_feature_name).to eq "#{callout.feature_name}_#{callout.namespace_id}" + end + end +end diff --git a/spec/serializers/test_reports_comparer_entity_spec.rb b/spec/serializers/test_reports_comparer_entity_spec.rb index 3f88438ccde..78aa64edae0 100644 --- a/spec/serializers/test_reports_comparer_entity_spec.rb +++ b/spec/serializers/test_reports_comparer_entity_spec.rb @@ -7,8 +7,8 @@ RSpec.describe TestReportsComparerEntity do let(:entity) { described_class.new(comparer) } let(:comparer) { Gitlab::Ci::Reports::TestReportsComparer.new(base_reports, head_reports) } - let(:base_reports) { Gitlab::Ci::Reports::TestReports.new } - let(:head_reports) { Gitlab::Ci::Reports::TestReports.new } + let(:base_reports) { Gitlab::Ci::Reports::TestReport.new } + let(:head_reports) { Gitlab::Ci::Reports::TestReport.new } describe '#as_json' do subject { entity.as_json } diff --git a/spec/serializers/test_reports_comparer_serializer_spec.rb b/spec/serializers/test_reports_comparer_serializer_spec.rb index f9c37f49039..d19d9681e07 100644 --- a/spec/serializers/test_reports_comparer_serializer_spec.rb +++ b/spec/serializers/test_reports_comparer_serializer_spec.rb @@ -8,8 +8,8 @@ RSpec.describe TestReportsComparerSerializer do let(:project) { double(:project) } let(:serializer) { described_class.new(project: project).represent(comparer) } let(:comparer) { Gitlab::Ci::Reports::TestReportsComparer.new(base_reports, head_reports) } - let(:base_reports) { Gitlab::Ci::Reports::TestReports.new } - let(:head_reports) { Gitlab::Ci::Reports::TestReports.new } + let(:base_reports) { Gitlab::Ci::Reports::TestReport.new } + let(:head_reports) { Gitlab::Ci::Reports::TestReport.new } describe '#to_json' do subject { serializer.to_json } diff --git a/spec/services/web_hooks/log_execution_service_spec.rb b/spec/services/web_hooks/log_execution_service_spec.rb index 0ba0372b99d..873f6adc8dc 100644 --- a/spec/services/web_hooks/log_execution_service_spec.rb +++ b/spec/services/web_hooks/log_execution_service_spec.rb @@ -35,6 +35,12 @@ RSpec.describe WebHooks::LogExecutionService do expect(WebHookLog.recent.first).to have_attributes(data) end + it 'updates the last failure' do + expect(project_hook).to receive(:update_last_failure) + + service.execute + end + context 'obtaining an exclusive lease' do let(:lease_key) { "web_hooks:update_hook_failure_state:#{project_hook.id}" } diff --git a/spec/views/errors/omniauth_error.html.haml_spec.rb b/spec/views/errors/omniauth_error.html.haml_spec.rb new file mode 100644 index 00000000000..e99cb536bd8 --- /dev/null +++ b/spec/views/errors/omniauth_error.html.haml_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'errors/omniauth_error' do + let(:provider) { FFaker::Product.brand } + let(:error) { FFaker::Lorem.sentence } + + before do + assign(:provider, provider) + assign(:error, error) + end + + it 'renders template' do + render + + expect(rendered).to have_content(provider) + expect(rendered).to have_content(_('Sign-in failed because %{error}.') % { error: error }) + expect(rendered).to have_link('Sign in') + expect(rendered).to have_content(_('If none of the options work, try contacting a GitLab administrator.')) + end +end