From f26a600a69b3a13d3102d801f5d92d3235ea56d4 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 8 Sep 2022 21:13:09 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .gitlab/ci/rules.gitlab-ci.yml | 2 + .rubocop_todo/gitlab/namespaced_class.yml | 1 - .rubocop_todo/layout/line_length.yml | 1 - .../layout/space_in_lambda_literal.yml | 1 - .rubocop_todo/rails/where_exists.yml | 1 - .rubocop_todo/rspec/context_wording.yml | 1 - .rubocop_todo/style/lambda.yml | 2 +- .../components/content_editor.vue | 70 +++++- .../components/content_editor_alert.vue | 17 +- .../components/editor_state_observer.vue | 14 +- .../components/loading_indicator.vue | 34 +-- .../content_editor/services/content_editor.js | 41 ++-- .../projects/merge_requests/edit/index.js | 67 ++++++ .../shared/wikis/components/wiki_form.vue | 73 +------ app/graphql/resolvers/members_resolver.rb | 4 + app/graphql/types/member_sort_enum.rb | 13 ++ app/helpers/learn_gitlab_helper.rb | 6 +- app/models/ci/pipeline.rb | 32 +-- app/models/ci/runner.rb | 8 - .../concerns/integrations/has_web_hook.rb | 1 + app/models/concerns/sortable.rb | 27 +++ app/models/environment.rb | 2 +- app/models/environment_status.rb | 2 +- app/models/integrations/buildkite.rb | 8 - app/models/issue.rb | 26 --- app/models/member.rb | 87 +++++++- app/models/merge_request.rb | 2 +- app/models/namespace.rb | 2 +- app/models/onboarding/progress.rb | 118 ++++++++++ app/models/onboarding_progress.rb | 114 ---------- app/models/project.rb | 4 +- .../ci/create_downstream_pipeline_service.rb | 2 +- .../ci/expire_pipeline_cache_service.rb | 2 +- .../ci/generate_coverage_reports_service.rb | 2 +- .../ci/downstream_pipeline_helpers.rb | 2 +- app/services/groups/create_service.rb | 2 +- .../in_product_marketing_emails_service.rb | 2 +- app/services/onboarding_progress_service.rb | 4 +- .../issuable/form/_branch_chooser.html.haml | 12 +- .../coverage_report_worker.rb | 2 +- ...based-integration-with-kubernetes-saas.yml | 2 +- db/docs/onboarding_progresses.yml | 2 +- ...ts_project_name_lower_pattern_ops_index.rb | 15 ++ db/schema_migrations/20220901131828 | 1 + db/structure.sql | 2 + doc/api/graphql/reference/index.md | 21 ++ doc/update/deprecations.md | 2 +- .../ci/reports/coverage_report_generator.rb | 2 +- lib/learn_gitlab/onboarding.rb | 4 +- locale/gitlab.pot | 12 +- .../project/project_owner_permissions_spec.rb | 6 +- .../integration_hook_logs_controller_spec.rb | 3 +- .../repositories/git_http_controller_spec.rb | 4 +- .../progresses.rb} | 2 +- .../user_edits_merge_request_spec.rb | 11 +- .../components/content_editor_alert_spec.js | 25 +++ .../components/content_editor_spec.js | 206 ++++++++++++------ .../components/editor_state_observer_spec.js | 26 +-- .../components/loading_indicator_spec.js | 46 +--- .../services/content_editor_spec.js | 115 ++++++---- .../shared/wikis/components/wiki_form_spec.js | 48 +--- .../content_editor_integration_spec.js | 71 +++--- .../resolvers/group_members_resolver_spec.rb | 4 +- .../project_members_resolver_spec.rb | 4 +- spec/helpers/learn_gitlab_helper_spec.rb | 6 +- .../lib/gitlab/web_hooks/rate_limiter_spec.rb | 2 +- spec/lib/learn_gitlab/onboarding_spec.rb | 4 +- spec/models/ci/pipeline_spec.rb | 36 +-- spec/models/ci/runner_spec.rb | 48 ---- spec/models/integrations/drone_ci_spec.rb | 1 - .../progress_spec.rb} | 52 +++-- .../environments/stop_service_spec.rb | 10 +- spec/services/groups/create_service_spec.rb | 4 +- spec/services/members/create_service_spec.rb | 26 +-- .../onboarding_progress_service_spec.rb | 14 +- .../graphql/members_shared_examples.rb | 9 + .../has_web_hook_shared_examples.rb | 24 ++ .../onboarding_issue_created_worker_spec.rb | 4 +- 78 files changed, 955 insertions(+), 730 deletions(-) create mode 100644 app/graphql/types/member_sort_enum.rb create mode 100644 app/models/onboarding/progress.rb delete mode 100644 app/models/onboarding_progress.rb create mode 100644 db/migrate/20220901131828_add_environments_project_name_lower_pattern_ops_index.rb create mode 100644 db/schema_migrations/20220901131828 rename spec/factories/{onboarding_progresses.rb => onboarding/progresses.rb} (53%) rename spec/models/{onboarding_progress_spec.rb => onboarding/progress_spec.rb} (82%) diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index b2ab53f42c1..c5214b9e10b 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -312,6 +312,8 @@ - "{,ee/,jh/}{,spec/}lib/{,ee/,jh/}gitlab/background_migration{,_spec}.rb" - "{,ee/,jh/}spec/support/helpers/database/**/*" - "lib/gitlab/markdown_cache/active_record/**/*" + - "lib/api/admin/batched_background_migrations.rb" + - "spec/requests/api/admin/batched_background_migrations_spec.rb" - "config/prometheus/common_metrics.yml" # Used by Gitlab::DatabaseImporters::CommonMetrics::Importer - "{,ee/,jh/}app/models/project_statistics.rb" # Used to calculate sizes in migration specs # Gitaly has interactions with background migrations: https://gitlab.com/gitlab-org/gitlab/-/issues/336538 diff --git a/.rubocop_todo/gitlab/namespaced_class.yml b/.rubocop_todo/gitlab/namespaced_class.yml index b79402ce5bf..79212670488 100644 --- a/.rubocop_todo/gitlab/namespaced_class.yml +++ b/.rubocop_todo/gitlab/namespaced_class.yml @@ -252,7 +252,6 @@ Gitlab/NamespacedClass: - 'app/models/notification_setting.rb' - 'app/models/oauth_access_grant.rb' - 'app/models/oauth_access_token.rb' - - 'app/models/onboarding_progress.rb' - 'app/models/out_of_context_discussion.rb' - 'app/models/pages_deployment.rb' - 'app/models/pages_domain.rb' diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml index 5fcc55bb0b0..6d2ea48e48c 100644 --- a/.rubocop_todo/layout/line_length.yml +++ b/.rubocop_todo/layout/line_length.yml @@ -5394,7 +5394,6 @@ Layout/LineLength: - 'spec/models/namespace_statistics_spec.rb' - 'spec/models/note_spec.rb' - 'spec/models/notification_setting_spec.rb' - - 'spec/models/onboarding_progress_spec.rb' - 'spec/models/packages/composer/cache_file_spec.rb' - 'spec/models/packages/composer/metadatum_spec.rb' - 'spec/models/packages/conan/metadatum_spec.rb' diff --git a/.rubocop_todo/layout/space_in_lambda_literal.yml b/.rubocop_todo/layout/space_in_lambda_literal.yml index c88fdf5a3b0..9359939514b 100644 --- a/.rubocop_todo/layout/space_in_lambda_literal.yml +++ b/.rubocop_todo/layout/space_in_lambda_literal.yml @@ -96,7 +96,6 @@ Layout/SpaceInLambdaLiteral: - 'app/models/namespace_statistics.rb' - 'app/models/note.rb' - 'app/models/note_diff_file.rb' - - 'app/models/onboarding_progress.rb' - 'app/models/operations/feature_flags/user_list.rb' - 'app/models/packages/build_info.rb' - 'app/models/packages/maven/metadatum.rb' diff --git a/.rubocop_todo/rails/where_exists.yml b/.rubocop_todo/rails/where_exists.yml index 00ff82d137e..77722549722 100644 --- a/.rubocop_todo/rails/where_exists.yml +++ b/.rubocop_todo/rails/where_exists.yml @@ -19,7 +19,6 @@ Rails/WhereExists: - 'app/models/lfs_object.rb' - 'app/models/merge_request_diff.rb' - 'app/models/namespace.rb' - - 'app/models/onboarding_progress.rb' - 'app/models/project.rb' - 'app/models/protected_branch/push_access_level.rb' - 'app/services/projects/transfer_service.rb' diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml index 98fd3c52fd9..97480a0e0ba 100644 --- a/.rubocop_todo/rspec/context_wording.yml +++ b/.rubocop_todo/rspec/context_wording.yml @@ -2466,7 +2466,6 @@ RSpec/ContextWording: - 'spec/models/note_spec.rb' - 'spec/models/notification_recipient_spec.rb' - 'spec/models/notification_setting_spec.rb' - - 'spec/models/onboarding_progress_spec.rb' - 'spec/models/operations/feature_flag_spec.rb' - 'spec/models/packages/conan/file_metadatum_spec.rb' - 'spec/models/packages/debian/file_metadatum_spec.rb' diff --git a/.rubocop_todo/style/lambda.yml b/.rubocop_todo/style/lambda.yml index 525e2c31797..f733af601ec 100644 --- a/.rubocop_todo/style/lambda.yml +++ b/.rubocop_todo/style/lambda.yml @@ -77,7 +77,7 @@ Style/Lambda: - 'app/models/note.rb' - 'app/models/note_diff_file.rb' - 'app/models/notification_setting.rb' - - 'app/models/onboarding_progress.rb' + - 'app/models/onboarding/progress.rb' - 'app/models/operations/feature_flags/user_list.rb' - 'app/models/packages/package.rb' - 'app/models/packages/package_file.rb' diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue index 05d6f468d23..659c447e861 100644 --- a/app/assets/javascripts/content_editor/components/content_editor.vue +++ b/app/assets/javascripts/content_editor/components/content_editor.vue @@ -1,6 +1,9 @@ - nil) .distinct('ci_builds_metadata.expanded_environment_name') .limit(100) @@ -977,17 +977,17 @@ module Ci end # With multi-project and parent-child pipelines - def all_pipelines_in_hierarchy + def upstream_and_all_downstreams object_hierarchy.all_objects end # With only parent-child pipelines - def self_and_ancestors + def self_and_project_ancestors object_hierarchy(project_condition: :same).base_and_ancestors end # With only parent-child pipelines - def self_and_descendants + def self_and_project_descendants object_hierarchy(project_condition: :same).base_and_descendants end @@ -996,8 +996,8 @@ module Ci object_hierarchy(project_condition: :same).descendants end - def self_and_descendants_complete? - self_and_descendants.all?(&:complete?) + def self_and_project_descendants_complete? + self_and_project_descendants.all?(&:complete?) end # Follow the parent-child relationships and return the top-level parent @@ -1061,8 +1061,8 @@ module Ci latest_report_builds(Ci::JobArtifact.of_report_type(:test)).preload(:project, :metadata) end - def latest_report_builds_in_self_and_descendants(reports_scope = ::Ci::JobArtifact.all_reports) - builds_in_self_and_descendants.with_artifacts(reports_scope) + def latest_report_builds_in_self_and_project_descendants(reports_scope = ::Ci::JobArtifact.all_reports) + builds_in_self_and_project_descendants.with_artifacts(reports_scope) end def builds_with_coverage diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 6c3754d84d0..3cbc4c50444 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -359,14 +359,6 @@ module Ci runner_projects.limit(2).count(:all) > 1 end - def assigned_to_group? - runner_namespaces.any? - end - - def assigned_to_project? - runner_projects.any? - end - def match_build_if_online?(build) active? && online? && matches_build?(build) end diff --git a/app/models/concerns/integrations/has_web_hook.rb b/app/models/concerns/integrations/has_web_hook.rb index e9fcb0d151d..e6ca6cc7938 100644 --- a/app/models/concerns/integrations/has_web_hook.rb +++ b/app/models/concerns/integrations/has_web_hook.rb @@ -5,6 +5,7 @@ module Integrations extend ActiveSupport::Concern included do + after_save :update_web_hook!, if: :activated? has_one :service_hook, inverse_of: :integration, foreign_key: :service_id end diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index 65fb62a814f..eccb004b503 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -43,6 +43,33 @@ module Sortable } end + def build_keyset_order_on_joined_column(scope:, attribute_name:, column:, direction:, nullable:) + reversed_direction = direction == :asc ? :desc : :asc + + # rubocop: disable GitlabSecurity/PublicSend + order = ::Gitlab::Pagination::Keyset::Order.build( + [ + ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: attribute_name, + column_expression: column, + order_expression: column.send(direction).send(nullable), + reversed_order_expression: column.send(reversed_direction).send(nullable), + order_direction: direction, + distinct: false, + add_to_projections: true, + nullable: nullable + ), + ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'id', + order_expression: arel_table['id'].desc + ) + ] + ) + # rubocop: enable GitlabSecurity/PublicSend + + order.apply_cursor_conditions(scope).reorder(order) + end + private def highest_label_priority(target_type_column: nil, target_type: nil, target_column:, project_column:, excluded_labels: []) diff --git a/app/models/environment.rb b/app/models/environment.rb index bc1b2daf3db..4aab4ab8170 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -84,7 +84,7 @@ class Environment < ApplicationRecord # Search environments which have names like the given query. # Do not set a large limit unless you've confirmed that it works on gitlab.com scale. scope :for_name_like, -> (query, limit: 5) do - where(arel_table[:name].matches("#{sanitize_sql_like query}%")).limit(limit) + where('LOWER(environments.name) LIKE LOWER(?) || \'%\'', sanitize_sql_like(query)).limit(limit) end scope :for_project, -> (project) { where(project_id: project) } diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb index 43b2c7899a1..d06d0a99948 100644 --- a/app/models/environment_status.rb +++ b/app/models/environment_status.rb @@ -100,7 +100,7 @@ class EnvironmentStatus def self.build_environments_status(mr, user, pipeline) return [] unless pipeline - pipeline.environments_in_self_and_descendants.includes(:project).available.map do |environment| + pipeline.environments_in_self_and_project_descendants.includes(:project).available.map do |environment| next unless Ability.allowed?(user, :read_environment, environment) EnvironmentStatus.new(pipeline.project, environment, mr, pipeline.sha) diff --git a/app/models/integrations/buildkite.rb b/app/models/integrations/buildkite.rb index c19e06f055e..7a48e71b934 100644 --- a/app/models/integrations/buildkite.rb +++ b/app/models/integrations/buildkite.rb @@ -8,8 +8,6 @@ module Integrations include ReactivelyCached extend Gitlab::Utils::Override - after_save :ensure_ssl_verification - ENDPOINT = "https://buildkite.com" field :project_url, @@ -50,12 +48,6 @@ module Integrations self.properties = properties.except('enable_ssl_verification') # Remove unused key end - def ensure_ssl_verification - return unless service_hook - - update_web_hook! - end - override :hook_url def hook_url "#{buildkite_endpoint('webhook')}/deliver/#{webhook_token}" diff --git a/app/models/issue.rb b/app/models/issue.rb index 1de2075b456..153747c75df 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -254,32 +254,6 @@ class Issue < ApplicationRecord alias_method :with_state, :with_state_id alias_method :with_states, :with_state_ids - def build_keyset_order_on_joined_column(scope:, attribute_name:, column:, direction:, nullable:) - reversed_direction = direction == :asc ? :desc : :asc - - # rubocop: disable GitlabSecurity/PublicSend - order = ::Gitlab::Pagination::Keyset::Order.build( - [ - ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( - attribute_name: attribute_name, - column_expression: column, - order_expression: column.send(direction).send(nullable), - reversed_order_expression: column.send(reversed_direction).send(nullable), - order_direction: direction, - distinct: false, - add_to_projections: true, - nullable: nullable - ), - ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( - attribute_name: 'id', - order_expression: arel_table['id'].desc - ) - ]) - # rubocop: enable GitlabSecurity/PublicSend - - order.apply_cursor_conditions(scope).order(order) - end - override :order_upvotes_desc def order_upvotes_desc reorder(upvotes_count: :desc) diff --git a/app/models/member.rb b/app/models/member.rb index 186fcd8759f..c5351d5447b 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -184,14 +184,85 @@ class Member < ApplicationRecord unscoped.from(distinct_members, :members) end - scope :order_name_asc, -> { left_join_users.reorder(User.arel_table[:name].asc.nulls_last) } - scope :order_name_desc, -> { left_join_users.reorder(User.arel_table[:name].desc.nulls_last) } - scope :order_recent_sign_in, -> { left_join_users.reorder(User.arel_table[:last_sign_in_at].desc.nulls_last) } - scope :order_oldest_sign_in, -> { left_join_users.reorder(User.arel_table[:last_sign_in_at].asc.nulls_last) } - scope :order_recent_last_activity, -> { left_join_users.reorder(User.arel_table[:last_activity_on].desc.nulls_last) } - scope :order_oldest_last_activity, -> { left_join_users.reorder(User.arel_table[:last_activity_on].asc.nulls_first) } - scope :order_recent_created_user, -> { left_join_users.reorder(User.arel_table[:created_at].desc.nulls_last) } - scope :order_oldest_created_user, -> { left_join_users.reorder(User.arel_table[:created_at].asc.nulls_first) } + scope :order_name_asc, -> do + build_keyset_order_on_joined_column( + scope: left_join_users, + attribute_name: 'member_user_full_name', + column: User.arel_table[:name], + direction: :asc, + nullable: :nulls_last + ) + end + + scope :order_name_desc, -> do + build_keyset_order_on_joined_column( + scope: left_join_users, + attribute_name: 'member_user_full_name', + column: User.arel_table[:name], + direction: :desc, + nullable: :nulls_last + ) + end + + scope :order_oldest_sign_in, -> do + build_keyset_order_on_joined_column( + scope: left_join_users, + attribute_name: 'member_user_last_sign_in_at', + column: User.arel_table[:last_sign_in_at], + direction: :asc, + nullable: :nulls_last + ) + end + + scope :order_recent_sign_in, -> do + build_keyset_order_on_joined_column( + scope: left_join_users, + attribute_name: 'member_user_last_sign_in_at', + column: User.arel_table[:last_sign_in_at], + direction: :desc, + nullable: :nulls_last + ) + end + + scope :order_oldest_last_activity, -> do + build_keyset_order_on_joined_column( + scope: left_join_users, + attribute_name: 'member_user_last_activity_on', + column: User.arel_table[:last_activity_on], + direction: :asc, + nullable: :nulls_first + ) + end + + scope :order_recent_last_activity, -> do + build_keyset_order_on_joined_column( + scope: left_join_users, + attribute_name: 'member_user_last_activity_on', + column: User.arel_table[:last_activity_on], + direction: :desc, + nullable: :nulls_last + ) + end + + scope :order_oldest_created_user, -> do + build_keyset_order_on_joined_column( + scope: left_join_users, + attribute_name: 'member_user_created_at', + column: User.arel_table[:created_at], + direction: :asc, + nullable: :nulls_first + ) + end + + scope :order_recent_created_user, -> do + build_keyset_order_on_joined_column( + scope: left_join_users, + attribute_name: 'member_user_created_at', + column: User.arel_table[:created_at], + direction: :desc, + nullable: :nulls_last + ) + end scope :on_project_and_ancestors, ->(project) { where(source: [project] + project.ancestors) } diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 18459805883..d155a295481 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -1466,7 +1466,7 @@ class MergeRequest < ApplicationRecord end def environments_in_head_pipeline(deployment_status: nil) - actual_head_pipeline&.environments_in_self_and_descendants(deployment_status: deployment_status) || Environment.none + actual_head_pipeline&.environments_in_self_and_project_descendants(deployment_status: deployment_status) || Environment.none end def fetch_ref! diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 4f0639a08a0..17d4c6d27e6 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -61,7 +61,7 @@ class Namespace < ApplicationRecord has_many :runner_namespaces, inverse_of: :namespace, class_name: 'Ci::RunnerNamespace' has_many :runners, through: :runner_namespaces, source: :runner, class_name: 'Ci::Runner' has_many :pending_builds, class_name: 'Ci::PendingBuild' - has_one :onboarding_progress + has_one :onboarding_progress, class_name: 'Onboarding::Progress' # This should _not_ be `inverse_of: :namespace`, because that would also set # `user.namespace` when this user creates a group with themselves as `owner`. diff --git a/app/models/onboarding/progress.rb b/app/models/onboarding/progress.rb new file mode 100644 index 00000000000..ecc78418256 --- /dev/null +++ b/app/models/onboarding/progress.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +module Onboarding + class Progress < ApplicationRecord + self.table_name = 'onboarding_progresses' + + belongs_to :namespace, optional: false + + validate :namespace_is_root_namespace + + ACTIONS = [ + :git_pull, + :git_write, + :merge_request_created, + :pipeline_created, + :user_added, + :trial_started, + :subscription_created, + :required_mr_approvals_enabled, + :code_owners_enabled, + :scoped_label_created, + :security_scan_enabled, + :issue_created, + :issue_auto_closed, + :repository_imported, + :repository_mirrored, + :secure_dependency_scanning_run, + :secure_container_scanning_run, + :secure_dast_run, + :secure_secret_detection_run, + :secure_coverage_fuzzing_run, + :secure_api_fuzzing_run, + :secure_cluster_image_scanning_run, + :license_scanning_run + ].freeze + + scope :incomplete_actions, ->(actions) do + Array.wrap(actions).inject(self) { |scope, action| scope.where(column_name(action) => nil) } + end + + scope :completed_actions, ->(actions) do + Array.wrap(actions).inject(self) { |scope, action| scope.where.not(column_name(action) => nil) } + end + + scope :completed_actions_with_latest_in_range, ->(actions, range) do + actions = Array(actions) + if actions.size == 1 + where(column_name(actions[0]) => range) + else + action_columns = actions.map { |action| arel_table[column_name(action)] } + completed_actions(actions).where(Arel::Nodes::NamedFunction.new('GREATEST', action_columns).between(range)) + end + end + + class << self + def onboard(namespace) + return unless root_namespace?(namespace) + + create(namespace: namespace) + end + + def onboarding?(namespace) + where(namespace: namespace).any? + end + + def register(namespace, actions) + actions = Array(actions) + return unless root_namespace?(namespace) && actions.difference(ACTIONS).empty? + + onboarding_progress = find_by(namespace: namespace) + return unless onboarding_progress + + now = Time.current + nil_actions = actions.select { |action| onboarding_progress[column_name(action)].nil? } + return if nil_actions.empty? + + updates = nil_actions.inject({}) { |sum, action| sum.merge!({ column_name(action) => now }) } + onboarding_progress.update!(updates) + end + + def completed?(namespace, action) + return unless root_namespace?(namespace) && ACTIONS.include?(action) + + action_column = column_name(action) + where(namespace: namespace).where.not(action_column => nil).exists? + end + + def not_completed?(namespace_id, action) + return unless ACTIONS.include?(action) + + action_column = column_name(action) + exists?(namespace_id: namespace_id, action_column => nil) + end + + def column_name(action) + :"#{action}_at" + end + + private + + def root_namespace?(namespace) + namespace&.root? + end + end + + def number_of_completed_actions + attributes.extract!(*ACTIONS.map { |action| self.class.column_name(action).to_s }).compact!.size + end + + private + + def namespace_is_root_namespace + return unless namespace + + errors.add(:namespace, _('must be a root namespace')) if namespace.has_parent? + end + end +end diff --git a/app/models/onboarding_progress.rb b/app/models/onboarding_progress.rb deleted file mode 100644 index e5851c5cfc5..00000000000 --- a/app/models/onboarding_progress.rb +++ /dev/null @@ -1,114 +0,0 @@ -# frozen_string_literal: true - -class OnboardingProgress < ApplicationRecord - belongs_to :namespace, optional: false - - validate :namespace_is_root_namespace - - ACTIONS = [ - :git_pull, - :git_write, - :merge_request_created, - :pipeline_created, - :user_added, - :trial_started, - :subscription_created, - :required_mr_approvals_enabled, - :code_owners_enabled, - :scoped_label_created, - :security_scan_enabled, - :issue_created, - :issue_auto_closed, - :repository_imported, - :repository_mirrored, - :secure_dependency_scanning_run, - :secure_container_scanning_run, - :secure_dast_run, - :secure_secret_detection_run, - :secure_coverage_fuzzing_run, - :secure_api_fuzzing_run, - :secure_cluster_image_scanning_run, - :license_scanning_run - ].freeze - - scope :incomplete_actions, -> (actions) do - Array.wrap(actions).inject(self) { |scope, action| scope.where(column_name(action) => nil) } - end - - scope :completed_actions, -> (actions) do - Array.wrap(actions).inject(self) { |scope, action| scope.where.not(column_name(action) => nil) } - end - - scope :completed_actions_with_latest_in_range, -> (actions, range) do - actions = Array(actions) - if actions.size == 1 - where(column_name(actions[0]) => range) - else - action_columns = actions.map { |action| arel_table[column_name(action)] } - completed_actions(actions).where(Arel::Nodes::NamedFunction.new('GREATEST', action_columns).between(range)) - end - end - - class << self - def onboard(namespace) - return unless root_namespace?(namespace) - - create(namespace: namespace) - end - - def onboarding?(namespace) - where(namespace: namespace).any? - end - - def register(namespace, actions) - actions = Array(actions) - return unless root_namespace?(namespace) && actions.difference(ACTIONS).empty? - - onboarding_progress = find_by(namespace: namespace) - return unless onboarding_progress - - now = Time.current - nil_actions = actions.select { |action| onboarding_progress[column_name(action)].nil? } - return if nil_actions.empty? - - updates = nil_actions.inject({}) { |sum, action| sum.merge!({ column_name(action) => now }) } - onboarding_progress.update!(updates) - end - - def completed?(namespace, action) - return unless root_namespace?(namespace) && ACTIONS.include?(action) - - action_column = column_name(action) - where(namespace: namespace).where.not(action_column => nil).exists? - end - - def not_completed?(namespace_id, action) - return unless ACTIONS.include?(action) - - action_column = column_name(action) - where(namespace_id: namespace_id).where(action_column => nil).exists? - end - - def column_name(action) - :"#{action}_at" - end - - private - - def root_namespace?(namespace) - namespace && namespace.root? - end - end - - def number_of_completed_actions - attributes.extract!(*ACTIONS.map { |action| self.class.column_name(action).to_s }).compact!.size - end - - private - - def namespace_is_root_namespace - return unless namespace - - errors.add(:namespace, _('must be a root namespace')) if namespace.has_parent? - end -end diff --git a/app/models/project.rb b/app/models/project.rb index 78ec17acc01..0be24252fa8 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1170,7 +1170,7 @@ class Project < ApplicationRecord latest_pipeline = ci_pipelines.latest_successful_for_ref(ref) return unless latest_pipeline - latest_pipeline.build_with_artifacts_in_self_and_descendants(job_name) + latest_pipeline.build_with_artifacts_in_self_and_project_descendants(job_name) end def latest_successful_build_for_sha(job_name, sha) @@ -1179,7 +1179,7 @@ class Project < ApplicationRecord latest_pipeline = ci_pipelines.latest_successful_for_sha(sha) return unless latest_pipeline - latest_pipeline.build_with_artifacts_in_self_and_descendants(job_name) + latest_pipeline.build_with_artifacts_in_self_and_project_descendants(job_name) end def latest_successful_build_for_ref!(job_name, ref = default_branch) diff --git a/app/services/ci/create_downstream_pipeline_service.rb b/app/services/ci/create_downstream_pipeline_service.rb index b38b3b93353..c68d990ae2e 100644 --- a/app/services/ci/create_downstream_pipeline_service.rb +++ b/app/services/ci/create_downstream_pipeline_service.rb @@ -137,7 +137,7 @@ module Ci return false unless @bridge.triggers_child_pipeline? # only applies to parent-child pipelines not multi-project - ancestors_of_new_child = @bridge.pipeline.self_and_ancestors + ancestors_of_new_child = @bridge.pipeline.self_and_project_ancestors ancestors_of_new_child.count > MAX_NESTED_CHILDREN end diff --git a/app/services/ci/expire_pipeline_cache_service.rb b/app/services/ci/expire_pipeline_cache_service.rb index bf2355c447a..15597eb7209 100644 --- a/app/services/ci/expire_pipeline_cache_service.rb +++ b/app/services/ci/expire_pipeline_cache_service.rb @@ -86,7 +86,7 @@ module Ci etag_paths << path end - pipeline.all_pipelines_in_hierarchy.includes(project: [:route, { namespace: :route }]).each do |relative_pipeline| # rubocop: disable CodeReuse/ActiveRecord + pipeline.upstream_and_all_downstreams.includes(project: [:route, { namespace: :route }]).each do |relative_pipeline| # rubocop: disable CodeReuse/ActiveRecord etag_paths << project_pipeline_path(relative_pipeline.project, relative_pipeline) etag_paths << graphql_pipeline_path(relative_pipeline) etag_paths << graphql_pipeline_sha_path(relative_pipeline.sha) diff --git a/app/services/ci/generate_coverage_reports_service.rb b/app/services/ci/generate_coverage_reports_service.rb index 81f26e84ef8..8beecb79fd9 100644 --- a/app/services/ci/generate_coverage_reports_service.rb +++ b/app/services/ci/generate_coverage_reports_service.rb @@ -43,7 +43,7 @@ module Ci end def last_update_timestamp(pipeline_hierarchy) - pipeline_hierarchy&.self_and_descendants&.maximum(:updated_at) + pipeline_hierarchy&.self_and_project_descendants&.maximum(:updated_at) end end end diff --git a/app/services/concerns/ci/downstream_pipeline_helpers.rb b/app/services/concerns/ci/downstream_pipeline_helpers.rb index 39c0adb6e4e..557bfe2534b 100644 --- a/app/services/concerns/ci/downstream_pipeline_helpers.rb +++ b/app/services/concerns/ci/downstream_pipeline_helpers.rb @@ -5,7 +5,7 @@ module Ci def log_downstream_pipeline_creation(downstream_pipeline) return unless downstream_pipeline&.persisted? - hierarchy_size = downstream_pipeline.all_pipelines_in_hierarchy.count + hierarchy_size = downstream_pipeline.upstream_and_all_downstreams.count root_pipeline = downstream_pipeline.upstream_root ::Gitlab::AppLogger.info( diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb index 35716f7742a..d508865ef32 100644 --- a/app/services/groups/create_service.rb +++ b/app/services/groups/create_service.rb @@ -39,7 +39,7 @@ module Groups if @group.save @group.add_owner(current_user) Integration.create_from_active_default_integrations(@group, :group_id) - OnboardingProgress.onboard(@group) + Onboarding::Progress.onboard(@group) end end diff --git a/app/services/namespaces/in_product_marketing_emails_service.rb b/app/services/namespaces/in_product_marketing_emails_service.rb index c139b2e11dd..1ce7e4cae16 100644 --- a/app/services/namespaces/in_product_marketing_emails_service.rb +++ b/app/services/namespaces/in_product_marketing_emails_service.rb @@ -89,7 +89,7 @@ module Namespaces end def groups_for_track - onboarding_progress_scope = OnboardingProgress + onboarding_progress_scope = Onboarding::Progress .completed_actions_with_latest_in_range(completed_actions, range) .incomplete_actions(incomplete_actions) diff --git a/app/services/onboarding_progress_service.rb b/app/services/onboarding_progress_service.rb index 6d44c0a61ea..7ba7fbc5b7f 100644 --- a/app/services/onboarding_progress_service.rb +++ b/app/services/onboarding_progress_service.rb @@ -9,7 +9,7 @@ class OnboardingProgressService end def execute(action:) - return unless OnboardingProgress.not_completed?(namespace_id, action) + return unless Onboarding::Progress.not_completed?(namespace_id, action) Namespaces::OnboardingProgressWorker.perform_async(namespace_id, action) end @@ -26,6 +26,6 @@ class OnboardingProgressService def execute(action:) return unless @namespace - OnboardingProgress.register(@namespace, action) + Onboarding::Progress.register(@namespace, action) end end diff --git a/app/views/shared/issuable/form/_branch_chooser.html.haml b/app/views/shared/issuable/form/_branch_chooser.html.haml index 8ab002f755f..634e927f891 100644 --- a/app/views/shared/issuable/form/_branch_chooser.html.haml +++ b/app/views/shared/issuable/form/_branch_chooser.html.haml @@ -31,10 +31,14 @@ - if issuable.merged? %code= target_title - unless issuable.new_record? || issuable.merged? - %span.dropdown.gl-ml-2.d-inline-block - = form.hidden_field(:target_branch, - { class: 'target_branch js-target-branch-select ref-name mw-xl', - data: { placeholder: _('Select branch'), endpoint: refs_project_path(@project, sort: 'updated_desc', find: 'branches') }}) + .merge-request-select.dropdown.gl-w-auto + = form.hidden_field :target_branch + = dropdown_toggle form.object.target_branch.presence || _("Select branch"), { toggle: "dropdown", 'field-name': "#{form.object_name}[target_branch]", 'refs-url': refs_project_path(@project, sort: 'updated_desc', find: 'branches'), selected: form.object.target_branch, default_text: _("Select branch") }, { toggle_class: "js-compare-dropdown js-target-branch monospace" } + .dropdown-menu.dropdown-menu-selectable.js-target-branch-dropdown.target_branch.ref-name.git-revision-dropdown + = dropdown_title(_("Select branch")) + = dropdown_filter(_("Search branches")) + = dropdown_content + = dropdown_loading - if source_level < target_level = render Pajamas::AlertComponent.new(variant: :warning, dismissible: false, alert_options: { class: 'gl-mb-4' }) do |c| diff --git a/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb b/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb index 127eb3b6f44..53bed0fa9da 100644 --- a/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb +++ b/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb @@ -20,7 +20,7 @@ module Ci return unless pipeline pipeline.root_ancestor.try do |root_ancestor_pipeline| - next unless root_ancestor_pipeline.self_and_descendants_complete? + next unless root_ancestor_pipeline.self_and_project_descendants_complete? Ci::PipelineArtifacts::CoverageReportService.new(root_ancestor_pipeline).execute end diff --git a/data/deprecations/14-5-certificate-based-integration-with-kubernetes-saas.yml b/data/deprecations/14-5-certificate-based-integration-with-kubernetes-saas.yml index c0963ea33c3..559189d759b 100644 --- a/data/deprecations/14-5-certificate-based-integration-with-kubernetes-saas.yml +++ b/data/deprecations/14-5-certificate-based-integration-with-kubernetes-saas.yml @@ -5,7 +5,7 @@ removal_date: "2023-02-22" # the date of the milestone release when this feature is planned to be removed breaking_change: true body: | - The certificate-based integration with Kubernetes will be [deprecated and removed](https://about.gitlab.com/blog/2021/11/15/deprecating-the-cert-based-kubernetes-integration/). As a GitLab SaaS customer, on new namespaces, you will no longer be able to integrate GitLab and your cluster using the certificate-based approach as of GitLab 15.0. The integration for current users will be enabled per namespace. The integrations are expected to be switched off completely on GitLab SaaS around 2022 November 22. + The certificate-based integration with Kubernetes will be [deprecated and removed](https://about.gitlab.com/blog/2021/11/15/deprecating-the-cert-based-kubernetes-integration/). As a GitLab SaaS customer, on new namespaces, you will no longer be able to integrate GitLab and your cluster using the certificate-based approach as of GitLab 15.0. The integration for current users will be enabled per namespace. For a more robust, secure, forthcoming, and reliable integration with Kubernetes, we recommend you use the [agent for Kubernetes](https://docs.gitlab.com/ee/user/clusters/agent/) to connect Kubernetes clusters with GitLab. [How do I migrate?](https://docs.gitlab.com/ee/user/infrastructure/clusters/migrate_to_gitlab_agent.html) diff --git a/db/docs/onboarding_progresses.yml b/db/docs/onboarding_progresses.yml index 4166b934570..80b70fe0b1f 100644 --- a/db/docs/onboarding_progresses.yml +++ b/db/docs/onboarding_progresses.yml @@ -1,7 +1,7 @@ --- table_name: onboarding_progresses classes: -- OnboardingProgress +- Onboarding::Progress feature_categories: - onboarding description: TODO diff --git a/db/migrate/20220901131828_add_environments_project_name_lower_pattern_ops_index.rb b/db/migrate/20220901131828_add_environments_project_name_lower_pattern_ops_index.rb new file mode 100644 index 00000000000..d8b7cd9be28 --- /dev/null +++ b/db/migrate/20220901131828_add_environments_project_name_lower_pattern_ops_index.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddEnvironmentsProjectNameLowerPatternOpsIndex < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + INDEX_NAME = 'index_environments_on_project_name_varchar_pattern_ops' + + def up + add_concurrent_index :environments, 'project_id, lower(name) varchar_pattern_ops', name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :environments, INDEX_NAME + end +end diff --git a/db/schema_migrations/20220901131828 b/db/schema_migrations/20220901131828 new file mode 100644 index 00000000000..b41fef9415b --- /dev/null +++ b/db/schema_migrations/20220901131828 @@ -0,0 +1 @@ +c32756c482bdda948f911d0405d2373673041c57ebc514cfc5f172ba6fda9185 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 37b410e7018..ea00b4049cd 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -28560,6 +28560,8 @@ CREATE INDEX index_environments_on_project_id_and_tier ON environments USING btr CREATE INDEX index_environments_on_project_id_state_environment_type ON environments USING btree (project_id, state, environment_type); +CREATE INDEX index_environments_on_project_name_varchar_pattern_ops ON environments USING btree (project_id, lower((name)::text) varchar_pattern_ops); + CREATE INDEX index_environments_on_state_and_auto_delete_at ON environments USING btree (auto_delete_at) WHERE ((auto_delete_at IS NOT NULL) AND ((state)::text = 'stopped'::text)); CREATE INDEX index_environments_on_state_and_auto_stop_at ON environments USING btree (state, auto_stop_at) WHERE ((auto_stop_at IS NOT NULL) AND ((state)::text = 'available'::text)); diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index bcffccf7140..a13a0ece465 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -12682,6 +12682,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | `accessLevels` | [`[AccessLevelEnum!]`](#accesslevelenum) | Filter members by the given access levels. | | `relations` | [`[GroupMemberRelation!]`](#groupmemberrelation) | Filter members by the given member relations. | | `search` | [`String`](#string) | Search query. | +| `sort` | [`MemberSort`](#membersort) | sort query. | ##### `Group.issues` @@ -16597,6 +16598,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | ---- | ---- | ----------- | | `relations` | [`[ProjectMemberRelation!]`](#projectmemberrelation) | Filter members by the given member relations. | | `search` | [`String`](#string) | Search query. | +| `sort` | [`MemberSort`](#membersort) | sort query. | ##### `Project.release` @@ -20318,6 +20320,25 @@ Possible identifier types for a measurement. | `PROJECTS` | Project count. | | `USERS` | User count. | +### `MemberSort` + +Values for sorting members. + +| Value | Description | +| ----- | ----------- | +| `ACCESS_LEVEL_ASC` | Access level ascending order. | +| `ACCESS_LEVEL_DESC` | Access level descending order. | +| `CREATED_ASC` | Created at ascending order. | +| `CREATED_DESC` | Created at descending order. | +| `UPDATED_ASC` | Updated at ascending order. | +| `UPDATED_DESC` | Updated at descending order. | +| `USER_FULL_NAME_ASC` | User's full name ascending order. | +| `USER_FULL_NAME_DESC` | User's full name descending order. | +| `created_asc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `CREATED_ASC`. | +| `created_desc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `CREATED_DESC`. | +| `updated_asc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `UPDATED_ASC`. | +| `updated_desc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `UPDATED_DESC`. | + ### `MergeRequestNewState` New state to apply to a merge request. diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md index ad565fbc3e7..8c39f39132e 100644 --- a/doc/update/deprecations.md +++ b/doc/update/deprecations.md @@ -1783,7 +1783,7 @@ WARNING: This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/). Review the details carefully before upgrading. -The certificate-based integration with Kubernetes will be [deprecated and removed](https://about.gitlab.com/blog/2021/11/15/deprecating-the-cert-based-kubernetes-integration/). As a GitLab SaaS customer, on new namespaces, you will no longer be able to integrate GitLab and your cluster using the certificate-based approach as of GitLab 15.0. The integration for current users will be enabled per namespace. The integrations are expected to be switched off completely on GitLab SaaS around 2022 November 22. +The certificate-based integration with Kubernetes will be [deprecated and removed](https://about.gitlab.com/blog/2021/11/15/deprecating-the-cert-based-kubernetes-integration/). As a GitLab SaaS customer, on new namespaces, you will no longer be able to integrate GitLab and your cluster using the certificate-based approach as of GitLab 15.0. The integration for current users will be enabled per namespace. For a more robust, secure, forthcoming, and reliable integration with Kubernetes, we recommend you use the [agent for Kubernetes](https://docs.gitlab.com/ee/user/clusters/agent/) to connect Kubernetes clusters with GitLab. [How do I migrate?](https://docs.gitlab.com/ee/user/infrastructure/clusters/migrate_to_gitlab_agent.html) diff --git a/lib/gitlab/ci/reports/coverage_report_generator.rb b/lib/gitlab/ci/reports/coverage_report_generator.rb index 5625707431a..88b3b14d5c9 100644 --- a/lib/gitlab/ci/reports/coverage_report_generator.rb +++ b/lib/gitlab/ci/reports/coverage_report_generator.rb @@ -35,7 +35,7 @@ module Gitlab private def report_builds - @pipeline.latest_report_builds_in_self_and_descendants(::Ci::JobArtifact.of_report_type(:coverage)) + @pipeline.latest_report_builds_in_self_and_project_descendants(::Ci::JobArtifact.of_report_type(:coverage)) end end end diff --git a/lib/learn_gitlab/onboarding.rb b/lib/learn_gitlab/onboarding.rb index 54af01a21fe..4215221cdf1 100644 --- a/lib/learn_gitlab/onboarding.rb +++ b/lib/learn_gitlab/onboarding.rb @@ -39,13 +39,13 @@ module LearnGitlab def onboarding_progress strong_memoize(:onboarding_progress) do - OnboardingProgress.find_by(namespace: namespace) # rubocop: disable CodeReuse/ActiveRecord + ::Onboarding::Progress.find_by(namespace: namespace) # rubocop: disable CodeReuse/ActiveRecord end end def action_columns strong_memoize(:action_columns) do - tracked_actions.map { |action_key| OnboardingProgress.column_name(action_key) } + tracked_actions.map { |action_key| ::Onboarding::Progress.column_name(action_key) } end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 621500f4d87..8f0bb16fb74 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4354,6 +4354,9 @@ msgstr "" msgid "An error occurred while trying to generate the report. Please try again later." msgstr "" +msgid "An error occurred while trying to render the content editor. Please try again." +msgstr "" + msgid "An error occurred while trying to run a new pipeline for this merge request." msgstr "" @@ -15152,6 +15155,9 @@ msgstr "" msgid "Error deleting project. Check logs for error details." msgstr "" +msgid "Error fetching branches" +msgstr "" + msgid "Error fetching burnup chart data" msgstr "" @@ -44476,9 +44482,6 @@ msgstr "" msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{wikiLinkStart}the page%{wikiLinkEnd} and make sure your changes will not unintentionally remove theirs." msgstr "" -msgid "WikiPage|An error occurred while trying to render the content editor. Please try again later." -msgstr "" - msgid "WikiPage|Cancel" msgstr "" @@ -44503,9 +44506,6 @@ msgstr "" msgid "WikiPage|Page title" msgstr "" -msgid "WikiPage|Retry" -msgstr "" - msgid "WikiPage|Save changes" msgstr "" diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/project_owner_permissions_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/project_owner_permissions_spec.rb index 29e590976d2..ca934337fbb 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/project_owner_permissions_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/project_owner_permissions_spec.rb @@ -2,7 +2,11 @@ module QA RSpec.describe 'Manage' do - describe 'Project owner permissions', :reliable do + describe 'Project owner permissions', :reliable, quarantine: { + only: { subdomain: %i[staging staging-canary] }, + type: :investigating, + issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/373038' + } do let!(:owner) do Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) end diff --git a/spec/controllers/projects/settings/integration_hook_logs_controller_spec.rb b/spec/controllers/projects/settings/integration_hook_logs_controller_spec.rb index 9569fe22b1f..8261461e8aa 100644 --- a/spec/controllers/projects/settings/integration_hook_logs_controller_spec.rb +++ b/spec/controllers/projects/settings/integration_hook_logs_controller_spec.rb @@ -5,8 +5,7 @@ require 'spec_helper' RSpec.describe Projects::Settings::IntegrationHookLogsController do let(:project) { create(:project, :repository) } let(:user) { create(:user) } - let(:service_hook) { create(:service_hook) } - let(:integration) { create(:drone_ci_integration, project: project, service_hook: service_hook) } + let(:integration) { create(:drone_ci_integration, project: project) } let(:log) { create(:web_hook_log, web_hook: integration.service_hook) } let(:log_params) do { diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb index 448587c937a..da62acb1fda 100644 --- a/spec/controllers/repositories/git_http_controller_spec.rb +++ b/spec/controllers/repositories/git_http_controller_spec.rb @@ -55,11 +55,11 @@ RSpec.describe Repositories::GitHttpController do let_it_be(:namespace) { project.namespace } before do - OnboardingProgress.onboard(namespace) + Onboarding::Progress.onboard(namespace) send_request end - subject { OnboardingProgress.completed?(namespace, :git_pull) } + subject { Onboarding::Progress.completed?(namespace, :git_pull) } it { is_expected.to be(true) } end diff --git a/spec/factories/onboarding_progresses.rb b/spec/factories/onboarding/progresses.rb similarity index 53% rename from spec/factories/onboarding_progresses.rb rename to spec/factories/onboarding/progresses.rb index e39bad91b19..15f58b482d3 100644 --- a/spec/factories/onboarding_progresses.rb +++ b/spec/factories/onboarding/progresses.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true FactoryBot.define do - factory :onboarding_progress do + factory :onboarding_progress, class: 'Onboarding::Progress' do namespace end end diff --git a/spec/features/merge_request/user_edits_merge_request_spec.rb b/spec/features/merge_request/user_edits_merge_request_spec.rb index 0b4b9d7452a..4ac25ea7ae0 100644 --- a/spec/features/merge_request/user_edits_merge_request_spec.rb +++ b/spec/features/merge_request/user_edits_merge_request_spec.rb @@ -3,8 +3,6 @@ require 'spec_helper' RSpec.describe 'User edits a merge request', :js do - include Select2Helper - let(:project) { create(:project, :repository) } let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let(:user) { create(:user) } @@ -89,7 +87,12 @@ RSpec.describe 'User edits a merge request', :js do it 'allows user to change target branch' do expect(page).to have_content('From master into feature') - select2('merge-test', from: '#merge_request_target_branch') + first('.js-target-branch').click + + wait_for_requests + + first('.js-target-branch-dropdown a', text: 'merge-test').click + click_button('Save changes') expect(page).to have_content("requested to merge #{merge_request.source_branch} into merge-test") @@ -101,7 +104,7 @@ RSpec.describe 'User edits a merge request', :js do it 'does not allow user to change target branch' do expect(page).to have_content('From master into feature') - expect(page).not_to have_selector('.select2-container') + expect(page).not_to have_selector('.js-target-branch.js-compare-dropdown') end end end diff --git a/spec/frontend/content_editor/components/content_editor_alert_spec.js b/spec/frontend/content_editor/components/content_editor_alert_spec.js index 12484cb13c6..ee9ead8f8a7 100644 --- a/spec/frontend/content_editor/components/content_editor_alert_spec.js +++ b/spec/frontend/content_editor/components/content_editor_alert_spec.js @@ -51,6 +51,16 @@ describe('content_editor/components/content_editor_alert', () => { }, ); + it('does not show primary action by default', async () => { + const message = 'error message'; + + createWrapper(); + eventHub.$emit(ALERT_EVENT, { message }); + await nextTick(); + + expect(findErrorAlert().attributes().primaryButtonText).toBeUndefined(); + }); + it('allows dismissing the error', async () => { const message = 'error message'; @@ -62,4 +72,19 @@ describe('content_editor/components/content_editor_alert', () => { expect(findErrorAlert().exists()).toBe(false); }); + + it('allows dismissing the error with a primary action button', async () => { + const message = 'error message'; + const actionLabel = 'Retry'; + const action = jest.fn(); + + createWrapper(); + eventHub.$emit(ALERT_EVENT, { message, action, actionLabel }); + await nextTick(); + findErrorAlert().vm.$emit('primaryAction'); + await nextTick(); + + expect(action).toHaveBeenCalled(); + expect(findErrorAlert().exists()).toBe(false); + }); }); diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js index 4c87ccca85b..ae52cb05eaf 100644 --- a/spec/frontend/content_editor/components/content_editor_spec.js +++ b/spec/frontend/content_editor/components/content_editor_spec.js @@ -1,4 +1,6 @@ -import { EditorContent } from '@tiptap/vue-2'; +import { GlAlert } from '@gitlab/ui'; +import { EditorContent, Editor } from '@tiptap/vue-2'; +import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import ContentEditor from '~/content_editor/components/content_editor.vue'; import ContentEditorAlert from '~/content_editor/components/content_editor_alert.vue'; @@ -10,112 +12,205 @@ import LinkBubbleMenu from '~/content_editor/components/bubble_menus/link_bubble import MediaBubbleMenu from '~/content_editor/components/bubble_menus/media_bubble_menu.vue'; import TopToolbar from '~/content_editor/components/top_toolbar.vue'; import LoadingIndicator from '~/content_editor/components/loading_indicator.vue'; -import { emitEditorEvent } from '../test_utils'; +import waitForPromises from 'helpers/wait_for_promises'; jest.mock('~/emoji'); describe('ContentEditor', () => { let wrapper; - let contentEditor; let renderMarkdown; const uploadsPath = '/uploads'; const findEditorElement = () => wrapper.findByTestId('content-editor'); const findEditorContent = () => wrapper.findComponent(EditorContent); const findEditorStateObserver = () => wrapper.findComponent(EditorStateObserver); - const createWrapper = (propsData = {}) => { - renderMarkdown = jest.fn(); - + const findLoadingIndicator = () => wrapper.findComponent(LoadingIndicator); + const findContentEditorAlert = () => wrapper.findComponent(ContentEditorAlert); + const createWrapper = ({ markdown } = {}) => { wrapper = shallowMountExtended(ContentEditor, { propsData: { renderMarkdown, uploadsPath, - ...propsData, + markdown, }, stubs: { EditorStateObserver, ContentEditorProvider, - }, - listeners: { - initialized(editor) { - contentEditor = editor; - }, + ContentEditorAlert, }, }); }; + beforeEach(() => { + renderMarkdown = jest.fn(); + }); + afterEach(() => { wrapper.destroy(); }); - it('triggers initialized event and provides contentEditor instance as event data', () => { + it('triggers initialized event', () => { createWrapper(); - expect(contentEditor).not.toBe(false); + expect(wrapper.emitted('initialized')).toHaveLength(1); }); - it('renders EditorContent component and provides tiptapEditor instance', () => { - createWrapper(); + it('renders EditorContent component and provides tiptapEditor instance', async () => { + const markdown = 'hello world'; + + createWrapper({ markdown }); + + renderMarkdown.mockResolvedValueOnce(markdown); + + await nextTick(); const editorContent = findEditorContent(); - expect(editorContent.props().editor).toBe(contentEditor.tiptapEditor); + expect(editorContent.props().editor).toBeInstanceOf(Editor); expect(editorContent.classes()).toContain('md'); }); - it('renders ContentEditorProvider component', () => { - createWrapper(); + it('renders ContentEditorProvider component', async () => { + await createWrapper(); expect(wrapper.findComponent(ContentEditorProvider).exists()).toBe(true); }); - it('renders top toolbar component', () => { - createWrapper(); + it('renders top toolbar component', async () => { + await createWrapper(); expect(wrapper.findComponent(TopToolbar).exists()).toBe(true); }); - it('adds is-focused class when focus event is emitted', async () => { - createWrapper(); + describe('when setting initial content', () => { + it('displays loading indicator', async () => { + createWrapper(); - await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'focus' }); + await nextTick(); - expect(findEditorElement().classes()).toContain('is-focused'); + expect(findLoadingIndicator().exists()).toBe(true); + }); + + it('emits loading event', async () => { + createWrapper(); + + await nextTick(); + + expect(wrapper.emitted('loading')).toHaveLength(1); + }); + + describe('succeeds', () => { + beforeEach(async () => { + renderMarkdown.mockResolvedValueOnce('hello world'); + + createWrapper({ markddown: 'hello world' }); + await nextTick(); + }); + + it('hides loading indicator', async () => { + await nextTick(); + expect(findLoadingIndicator().exists()).toBe(false); + }); + + it('emits loadingSuccess event', () => { + expect(wrapper.emitted('loadingSuccess')).toHaveLength(1); + }); + }); + + describe('fails', () => { + beforeEach(async () => { + renderMarkdown.mockRejectedValueOnce(new Error()); + + createWrapper({ markddown: 'hello world' }); + await nextTick(); + }); + + it('sets the content editor as read only when loading content fails', async () => { + await nextTick(); + + expect(findEditorContent().props().editor.isEditable).toBe(false); + }); + + it('hides loading indicator', async () => { + await nextTick(); + + expect(findLoadingIndicator().exists()).toBe(false); + }); + + it('emits loadingError event', () => { + expect(wrapper.emitted('loadingError')).toHaveLength(1); + }); + + it('displays error alert indicating that the content editor failed to load', () => { + expect(findContentEditorAlert().text()).toContain( + 'An error occurred while trying to render the content editor. Please try again.', + ); + }); + + describe('when clicking the retry button in the loading error alert and loading succeeds', () => { + beforeEach(async () => { + renderMarkdown.mockResolvedValueOnce('hello markdown'); + await wrapper.findComponent(GlAlert).vm.$emit('primaryAction'); + }); + + it('hides the loading error alert', () => { + expect(findContentEditorAlert().text()).toBe(''); + }); + + it('sets the content editor as writable', async () => { + await nextTick(); + + expect(findEditorContent().props().editor.isEditable).toBe(true); + }); + }); + }); }); - it('removes is-focused class when blur event is emitted', async () => { - createWrapper(); + describe('when focused event is emitted', () => { + beforeEach(async () => { + createWrapper(); - await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'focus' }); - await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'blur' }); + findEditorStateObserver().vm.$emit('focus'); - expect(findEditorElement().classes()).not.toContain('is-focused'); + await nextTick(); + }); + + it('adds is-focused class when focus event is emitted', () => { + expect(findEditorElement().classes()).toContain('is-focused'); + }); + + it('removes is-focused class when blur event is emitted', async () => { + findEditorStateObserver().vm.$emit('blur'); + + await nextTick(); + + expect(findEditorElement().classes()).not.toContain('is-focused'); + }); }); - it('emits change event when document is updated', async () => { - createWrapper(); + describe('when editorStateObserver emits docUpdate event', () => { + it('emits change event with the latest markdown', async () => { + const markdown = 'Loaded content'; - await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'update' }); + renderMarkdown.mockResolvedValueOnce(markdown); - expect(wrapper.emitted('change')).toEqual([ - [ - { - empty: contentEditor.empty, - }, - ], - ]); - }); + createWrapper({ markdown: 'initial content' }); - it('renders content_editor_alert component', () => { - createWrapper(); + await nextTick(); + await waitForPromises(); - expect(wrapper.findComponent(ContentEditorAlert).exists()).toBe(true); - }); + findEditorStateObserver().vm.$emit('docUpdate'); - it('renders loading indicator component', () => { - createWrapper(); - - expect(wrapper.findComponent(LoadingIndicator).exists()).toBe(true); + expect(wrapper.emitted('change')).toEqual([ + [ + { + markdown, + changed: false, + empty: false, + }, + ], + ]); + }); }); it.each` @@ -129,17 +224,4 @@ describe('ContentEditor', () => { expect(wrapper.findComponent(component).exists()).toBe(true); }); - - it.each` - event - ${'loading'} - ${'loadingSuccess'} - ${'loadingError'} - `('broadcasts $event event triggered by editor-state-observer component', ({ event }) => { - createWrapper(); - - findEditorStateObserver().vm.$emit(event); - - expect(wrapper.emitted(event)).toHaveLength(1); - }); }); diff --git a/spec/frontend/content_editor/components/editor_state_observer_spec.js b/spec/frontend/content_editor/components/editor_state_observer_spec.js index 51a594a606b..e8c2d8c8793 100644 --- a/spec/frontend/content_editor/components/editor_state_observer_spec.js +++ b/spec/frontend/content_editor/components/editor_state_observer_spec.js @@ -4,12 +4,7 @@ import EditorStateObserver, { tiptapToComponentMap, } from '~/content_editor/components/editor_state_observer.vue'; import eventHubFactory from '~/helpers/event_hub_factory'; -import { - LOADING_CONTENT_EVENT, - LOADING_SUCCESS_EVENT, - LOADING_ERROR_EVENT, - ALERT_EVENT, -} from '~/content_editor/constants'; +import { ALERT_EVENT } from '~/content_editor/constants'; import { createTestEditor } from '../test_utils'; describe('content_editor/components/editor_state_observer', () => { @@ -18,9 +13,6 @@ describe('content_editor/components/editor_state_observer', () => { let onDocUpdateListener; let onSelectionUpdateListener; let onTransactionListener; - let onLoadingContentListener; - let onLoadingSuccessListener; - let onLoadingErrorListener; let onAlertListener; let eventHub; @@ -38,9 +30,6 @@ describe('content_editor/components/editor_state_observer', () => { selectionUpdate: onSelectionUpdateListener, transaction: onTransactionListener, [ALERT_EVENT]: onAlertListener, - [LOADING_CONTENT_EVENT]: onLoadingContentListener, - [LOADING_SUCCESS_EVENT]: onLoadingSuccessListener, - [LOADING_ERROR_EVENT]: onLoadingErrorListener, }, }); }; @@ -50,9 +39,6 @@ describe('content_editor/components/editor_state_observer', () => { onSelectionUpdateListener = jest.fn(); onTransactionListener = jest.fn(); onAlertListener = jest.fn(); - onLoadingSuccessListener = jest.fn(); - onLoadingContentListener = jest.fn(); - onLoadingErrorListener = jest.fn(); buildEditor(); }); @@ -81,11 +67,8 @@ describe('content_editor/components/editor_state_observer', () => { }); it.each` - event | listener - ${ALERT_EVENT} | ${() => onAlertListener} - ${LOADING_CONTENT_EVENT} | ${() => onLoadingContentListener} - ${LOADING_SUCCESS_EVENT} | ${() => onLoadingSuccessListener} - ${LOADING_ERROR_EVENT} | ${() => onLoadingErrorListener} + event | listener + ${ALERT_EVENT} | ${() => onAlertListener} `('listens to $event event in the eventBus object', ({ event, listener }) => { const args = {}; @@ -114,9 +97,6 @@ describe('content_editor/components/editor_state_observer', () => { it.each` event ${ALERT_EVENT} - ${LOADING_CONTENT_EVENT} - ${LOADING_SUCCESS_EVENT} - ${LOADING_ERROR_EVENT} `('removes $event event hook from eventHub', ({ event }) => { jest.spyOn(eventHub, '$off'); jest.spyOn(eventHub, '$on'); diff --git a/spec/frontend/content_editor/components/loading_indicator_spec.js b/spec/frontend/content_editor/components/loading_indicator_spec.js index e4fb09b70a4..0065103d01b 100644 --- a/spec/frontend/content_editor/components/loading_indicator_spec.js +++ b/spec/frontend/content_editor/components/loading_indicator_spec.js @@ -1,18 +1,10 @@ import { GlLoadingIcon } from '@gitlab/ui'; -import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import LoadingIndicator from '~/content_editor/components/loading_indicator.vue'; -import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue'; -import { - LOADING_CONTENT_EVENT, - LOADING_SUCCESS_EVENT, - LOADING_ERROR_EVENT, -} from '~/content_editor/constants'; describe('content_editor/components/loading_indicator', () => { let wrapper; - const findEditorStateObserver = () => wrapper.findComponent(EditorStateObserver); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const createWrapper = () => { @@ -24,48 +16,12 @@ describe('content_editor/components/loading_indicator', () => { }); describe('when loading content', () => { - beforeEach(async () => { + beforeEach(() => { createWrapper(); - - findEditorStateObserver().vm.$emit(LOADING_CONTENT_EVENT); - - await nextTick(); }); it('displays loading indicator', () => { expect(findLoadingIcon().exists()).toBe(true); }); }); - - describe('when loading content succeeds', () => { - beforeEach(async () => { - createWrapper(); - - findEditorStateObserver().vm.$emit(LOADING_CONTENT_EVENT); - await nextTick(); - findEditorStateObserver().vm.$emit(LOADING_SUCCESS_EVENT); - await nextTick(); - }); - - it('hides loading indicator', () => { - expect(findLoadingIcon().exists()).toBe(false); - }); - }); - - describe('when loading content fails', () => { - const error = 'error'; - - beforeEach(async () => { - createWrapper(); - - findEditorStateObserver().vm.$emit(LOADING_CONTENT_EVENT); - await nextTick(); - findEditorStateObserver().vm.$emit(LOADING_ERROR_EVENT, error); - await nextTick(); - }); - - it('hides loading indicator', () => { - expect(findLoadingIcon().exists()).toBe(false); - }); - }); }); diff --git a/spec/frontend/content_editor/services/content_editor_spec.js b/spec/frontend/content_editor/services/content_editor_spec.js index a3553e612ca..6175cbdd3d4 100644 --- a/spec/frontend/content_editor/services/content_editor_spec.js +++ b/spec/frontend/content_editor/services/content_editor_spec.js @@ -1,8 +1,3 @@ -import { - LOADING_CONTENT_EVENT, - LOADING_SUCCESS_EVENT, - LOADING_ERROR_EVENT, -} from '~/content_editor/constants'; import { ContentEditor } from '~/content_editor/services/content_editor'; import eventHubFactory from '~/helpers/event_hub_factory'; import { createTestEditor, createDocBuilder } from '../test_utils'; @@ -14,6 +9,7 @@ describe('content_editor/services/content_editor', () => { let eventHub; let doc; let p; + const testMarkdown = '**bold text**'; beforeEach(() => { const tiptapEditor = createTestEditor(); @@ -36,6 +32,9 @@ describe('content_editor/services/content_editor', () => { }); }); + const testDoc = () => doc(p('document')); + const testEmptyDoc = () => doc(); + describe('.dispose', () => { it('destroys the tiptapEditor', () => { expect(contentEditor.tiptapEditor.destroy).not.toHaveBeenCalled(); @@ -46,51 +45,77 @@ describe('content_editor/services/content_editor', () => { }); }); + describe('empty', () => { + it('returns true when tiptapEditor is empty', async () => { + deserializer.deserialize.mockResolvedValueOnce({ document: testEmptyDoc() }); + + await contentEditor.setSerializedContent(testMarkdown); + + expect(contentEditor.empty).toBe(true); + }); + + it('returns false when tiptapEditor is not empty', async () => { + deserializer.deserialize.mockResolvedValueOnce({ document: testDoc() }); + + await contentEditor.setSerializedContent(testMarkdown); + + expect(contentEditor.empty).toBe(false); + }); + }); + + describe('editable', () => { + it('returns true when tiptapEditor is editable', async () => { + contentEditor.setEditable(true); + + expect(contentEditor.editable).toBe(true); + }); + + it('returns false when tiptapEditor is readonly', async () => { + contentEditor.setEditable(false); + + expect(contentEditor.editable).toBe(false); + }); + }); + + describe('changed', () => { + it('returns true when the initial document changes', async () => { + deserializer.deserialize.mockResolvedValueOnce({ document: testDoc() }); + + await contentEditor.setSerializedContent(testMarkdown); + + contentEditor.tiptapEditor.commands.insertContent(' new content'); + + expect(contentEditor.changed).toBe(true); + }); + + it('returns false when the initial document hasn’t changed', async () => { + deserializer.deserialize.mockResolvedValueOnce({ document: testDoc() }); + + await contentEditor.setSerializedContent(testMarkdown); + + expect(contentEditor.changed).toBe(false); + }); + + it('returns false when an initial document is not set and the document is empty', () => { + expect(contentEditor.changed).toBe(false); + }); + + it('returns true when an initial document is not set and the document is not empty', () => { + contentEditor.tiptapEditor.commands.insertContent('new content'); + + expect(contentEditor.changed).toBe(true); + }); + }); + describe('when setSerializedContent succeeds', () => { - let document; - const languages = ['javascript']; - const testMarkdown = '**bold text**'; - - beforeEach(() => { - document = doc(p('document')); - deserializer.deserialize.mockResolvedValueOnce({ document, languages }); - }); - - it('emits loadingContent and loadingSuccess event in the eventHub', () => { - let loadingContentEmitted = false; - - eventHub.$on(LOADING_CONTENT_EVENT, () => { - loadingContentEmitted = true; - }); - eventHub.$on(LOADING_SUCCESS_EVENT, () => { - expect(loadingContentEmitted).toBe(true); - }); - - contentEditor.setSerializedContent(testMarkdown); - }); - it('sets the deserialized document in the tiptap editor object', async () => { + const document = testDoc(); + + deserializer.deserialize.mockResolvedValueOnce({ document }); + await contentEditor.setSerializedContent(testMarkdown); expect(contentEditor.tiptapEditor.state.doc.toJSON()).toEqual(document.toJSON()); }); }); - - describe('when setSerializedContent fails', () => { - const error = 'error'; - - beforeEach(() => { - deserializer.deserialize.mockRejectedValueOnce(error); - }); - - it('emits loadingError event', async () => { - eventHub.$on(LOADING_ERROR_EVENT, (e) => { - expect(e).toBe('error'); - }); - - await expect(() => contentEditor.setSerializedContent('**bold text**')).rejects.toEqual( - error, - ); - }); - }); }); diff --git a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js index 204c48f8de1..36a926990f2 100644 --- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js +++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js @@ -302,19 +302,15 @@ describe('WikiForm', () => { }); it.each` - format | enabled | action + format | exists | action ${'markdown'} | ${true} | ${'displays'} ${'rdoc'} | ${false} | ${'hides'} ${'asciidoc'} | ${false} | ${'hides'} ${'org'} | ${false} | ${'hides'} - `('$action toggle editing mode button when format is $format', async ({ format, enabled }) => { + `('$action toggle editing mode button when format is $format', async ({ format, exists }) => { await setFormat(format); - expect(findToggleEditingModeButton().exists()).toBe(enabled); - }); - - it('displays toggle editing mode button', () => { - expect(findToggleEditingModeButton().exists()).toBe(true); + expect(findToggleEditingModeButton().exists()).toBe(exists); }); describe('when content editor is not active', () => { @@ -351,15 +347,8 @@ describe('WikiForm', () => { }); describe('when content editor is active', () => { - let mockContentEditor; - beforeEach(() => { createWrapper(); - mockContentEditor = { - getSerializedContent: jest.fn(), - setSerializedContent: jest.fn(), - }; - findToggleEditingModeButton().vm.$emit('input', 'richText'); }); @@ -368,14 +357,7 @@ describe('WikiForm', () => { }); describe('when clicking the toggle editing mode button', () => { - const contentEditorFakeSerializedContent = 'fake content'; - beforeEach(async () => { - mockContentEditor.getSerializedContent.mockReturnValueOnce( - contentEditorFakeSerializedContent, - ); - - findContentEditor().vm.$emit('initialized', mockContentEditor); await findToggleEditingModeButton().vm.$emit('input', 'source'); await nextTick(); }); @@ -387,10 +369,6 @@ describe('WikiForm', () => { it('displays the classic editor', () => { expect(findClassicEditor().exists()).toBe(true); }); - - it('updates the classic editor content field', () => { - expect(findContent().element.value).toBe(contentEditorFakeSerializedContent); - }); }); describe('when content editor is loading', () => { @@ -480,8 +458,14 @@ describe('WikiForm', () => { }); describe('when wiki content is updated', () => { + const updatedMarkdown = 'hello **world**'; + beforeEach(() => { - findContentEditor().vm.$emit('change', { empty: false }); + findContentEditor().vm.$emit('change', { + empty: false, + changed: true, + markdown: updatedMarkdown, + }); }); it('sets before unload warning', () => { @@ -512,16 +496,8 @@ describe('WikiForm', () => { }); }); - it('updates content from content editor on form submit', async () => { - // old value - expect(findContent().element.value).toBe(' My page content '); - - // wait for content editor to load - await waitForPromises(); - - await triggerFormSubmit(); - - expect(findContent().element.value).toBe('hello **world**'); + it('sets content field to the content editor updated markdown', async () => { + expect(findContent().element.value).toBe(updatedMarkdown); }); }); }); diff --git a/spec/frontend_integration/content_editor/content_editor_integration_spec.js b/spec/frontend_integration/content_editor/content_editor_integration_spec.js index 12cd6dcad83..7781a463fd6 100644 --- a/spec/frontend_integration/content_editor/content_editor_integration_spec.js +++ b/spec/frontend_integration/content_editor/content_editor_integration_spec.js @@ -1,6 +1,7 @@ import { nextTick } from 'vue'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import { ContentEditor } from '~/content_editor'; +import waitForPromises from 'helpers/wait_for_promises'; /** * This spec exercises some workflows in the Content Editor without mocking @@ -10,32 +11,34 @@ import { ContentEditor } from '~/content_editor'; describe('content_editor', () => { let wrapper; let renderMarkdown; - let contentEditorService; - const buildWrapper = () => { - renderMarkdown = jest.fn(); + const buildWrapper = ({ markdown = '' } = {}) => { wrapper = mountExtended(ContentEditor, { propsData: { renderMarkdown, uploadsPath: '/', - }, - listeners: { - initialized(contentEditor) { - contentEditorService = contentEditor; - }, + markdown, }, }); }; + const waitUntilContentIsLoaded = async () => { + await waitForPromises(); + await nextTick(); + }; + + beforeEach(() => { + renderMarkdown = jest.fn(); + }); + describe('when loading initial content', () => { describe('when the initial content is empty', () => { it('still hides the loading indicator', async () => { - buildWrapper(); - renderMarkdown.mockResolvedValue(''); - await contentEditorService.setSerializedContent(''); - await nextTick(); + buildWrapper(); + + await waitUntilContentIsLoaded(); expect(wrapper.findByTestId('content-editor-loading-indicator').exists()).toBe(false); }); @@ -44,14 +47,13 @@ describe('content_editor', () => { describe('when the initial content is not empty', () => { const initialContent = '

bold text

'; beforeEach(async () => { - buildWrapper(); - renderMarkdown.mockResolvedValue(initialContent); - await contentEditorService.setSerializedContent('**bold text**'); - await nextTick(); + buildWrapper(); + + await waitUntilContentIsLoaded(); }); - it('hides the loading indicator', async () => { + it('hides the loading indicator', () => { expect(wrapper.findByTestId('content-editor-loading-indicator').exists()).toBe(false); }); @@ -70,27 +72,29 @@ describe('content_editor', () => { }); it('processes and renders footnote ids alongside the footnote definition', async () => { - buildWrapper(); - - await contentEditorService.setSerializedContent(` + buildWrapper({ + markdown: ` This reference tag is a mix of letters and numbers [^footnote]. [^footnote]: This is another footnote. - `); - await nextTick(); + `, + }); + + await waitUntilContentIsLoaded(); expect(wrapper.text()).toContain('footnote: This is another footnote'); }); it('processes and displays reference definitions', async () => { - buildWrapper(); - - await contentEditorService.setSerializedContent(` + buildWrapper({ + markdown: ` [GitLab][gitlab] [gitlab]: https://gitlab.com - `); - await nextTick(); + `, + }); + + await waitUntilContentIsLoaded(); expect(wrapper.find('pre').text()).toContain('[gitlab]: https://gitlab.com'); }); @@ -99,9 +103,7 @@ This reference tag is a mix of letters and numbers [^footnote]. it('renders table of contents', async () => { jest.useFakeTimers(); - buildWrapper(); - - renderMarkdown.mockResolvedValue(` + renderMarkdown.mockResolvedValueOnce(`

@@ -112,16 +114,17 @@ This reference tag is a mix of letters and numbers [^footnote].

`); - await contentEditorService.setSerializedContent(` + buildWrapper({ + markdown: ` [TOC] # Heading 1 ## Heading 2 - `); + `, + }); - await nextTick(); - jest.runAllTimers(); + await waitUntilContentIsLoaded(); expect(wrapper.findByTestId('table-of-contents').text()).toContain('Heading 1'); expect(wrapper.findByTestId('table-of-contents').text()).toContain('Heading 2'); diff --git a/spec/graphql/resolvers/group_members_resolver_spec.rb b/spec/graphql/resolvers/group_members_resolver_spec.rb index bd0b4870062..d860b87875e 100644 --- a/spec/graphql/resolvers/group_members_resolver_spec.rb +++ b/spec/graphql/resolvers/group_members_resolver_spec.rb @@ -2,9 +2,11 @@ require 'spec_helper' -RSpec.describe Resolvers::GroupMembersResolver do +RSpec.describe 'Resolvers::GroupMembersResolver' do include GraphqlHelpers + let(:described_class) { Resolvers::GroupMembersResolver } + specify do expect(described_class).to have_nullable_graphql_type(Types::GroupMemberType.connection_type) end diff --git a/spec/graphql/resolvers/project_members_resolver_spec.rb b/spec/graphql/resolvers/project_members_resolver_spec.rb index 2f4145b3215..c38cb3d157b 100644 --- a/spec/graphql/resolvers/project_members_resolver_spec.rb +++ b/spec/graphql/resolvers/project_members_resolver_spec.rb @@ -2,9 +2,11 @@ require 'spec_helper' -RSpec.describe Resolvers::ProjectMembersResolver do +RSpec.describe 'Resolvers::ProjectMembersResolver' do include GraphqlHelpers + let(:described_class) { Resolvers::ProjectMembersResolver } + it_behaves_like 'querying members with a group' do let_it_be(:project) { create(:project, group: group_1) } let_it_be(:resource_member) { create(:project_member, user: user_1, project: project) } diff --git a/spec/helpers/learn_gitlab_helper_spec.rb b/spec/helpers/learn_gitlab_helper_spec.rb index 7c9dfd6b5be..68f847e80b0 100644 --- a/spec/helpers/learn_gitlab_helper_spec.rb +++ b/spec/helpers/learn_gitlab_helper_spec.rb @@ -15,8 +15,8 @@ RSpec.describe LearnGitlabHelper do allow(learn_gitlab).to receive(:project).and_return(project) end - OnboardingProgress.onboard(namespace) - OnboardingProgress.register(namespace, :git_write) + Onboarding::Progress.onboard(namespace) + Onboarding::Progress.register(namespace, :git_write) end describe '#learn_gitlab_enabled?' do @@ -37,7 +37,7 @@ RSpec.describe LearnGitlabHelper do with_them do before do - allow(OnboardingProgress).to receive(:onboarding?).with(project.namespace).and_return(onboarding) + allow(Onboarding::Progress).to receive(:onboarding?).with(project.namespace).and_return(onboarding) allow_next(LearnGitlab::Project, user).to receive(:available?).and_return(learn_gitlab_available) end diff --git a/spec/lib/gitlab/web_hooks/rate_limiter_spec.rb b/spec/lib/gitlab/web_hooks/rate_limiter_spec.rb index 3a5864e1832..b25ce4ea9da 100644 --- a/spec/lib/gitlab/web_hooks/rate_limiter_spec.rb +++ b/spec/lib/gitlab/web_hooks/rate_limiter_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Gitlab::WebHooks::RateLimiter, :clean_gitlab_redis_rate_limiting let_it_be(:plan) { create(:default_plan) } let_it_be_with_reload(:project_hook) { create(:project_hook) } let_it_be_with_reload(:system_hook) { create(:system_hook) } - let_it_be_with_reload(:integration_hook) { create(:service_hook) } + let_it_be_with_reload(:integration_hook) { create(:jenkins_integration).service_hook } let_it_be(:limit) { 1 } using RSpec::Parameterized::TableSyntax diff --git a/spec/lib/learn_gitlab/onboarding_spec.rb b/spec/lib/learn_gitlab/onboarding_spec.rb index 3e22ce59091..97926f8a612 100644 --- a/spec/lib/learn_gitlab/onboarding_spec.rb +++ b/spec/lib/learn_gitlab/onboarding_spec.rb @@ -13,11 +13,11 @@ RSpec.describe LearnGitlab::Onboarding do *described_class::ACTION_ISSUE_IDS.keys, *described_class::ACTION_PATHS, :security_scan_enabled - ].map { |key| OnboardingProgress.column_name(key) } + ].map { |key| ::Onboarding::Progress.column_name(key) } end before do - expect(OnboardingProgress).to receive(:find_by).with(namespace: namespace).and_return(onboarding_progress) + expect(::Onboarding::Progress).to receive(:find_by).with(namespace: namespace).and_return(onboarding_progress) end subject { described_class.new(namespace).completed_percentage } diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 85a30fe02cc..84ca8ac47fe 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -3629,8 +3629,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end - describe '#environments_in_self_and_descendants' do - subject { pipeline.environments_in_self_and_descendants } + describe '#environments_in_self_and_project_descendants' do + subject { pipeline.environments_in_self_and_project_descendants } context 'when pipeline is not child nor parent' do let_it_be(:pipeline) { create(:ci_pipeline, :created) } @@ -4036,13 +4036,13 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end - describe '#self_and_descendants_complete?' do + describe '#self_and_project_descendants_complete?' do let_it_be(:pipeline) { create(:ci_pipeline, :success) } let_it_be(:child_pipeline) { create(:ci_pipeline, :success, child_of: pipeline) } let_it_be_with_reload(:grandchild_pipeline) { create(:ci_pipeline, :success, child_of: child_pipeline) } context 'when all pipelines in the hierarchy is complete' do - it { expect(pipeline.self_and_descendants_complete?).to be(true) } + it { expect(pipeline.self_and_project_descendants_complete?).to be(true) } end context 'when a pipeline in the hierarchy is not complete' do @@ -4050,12 +4050,12 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do grandchild_pipeline.update!(status: :running) end - it { expect(pipeline.self_and_descendants_complete?).to be(false) } + it { expect(pipeline.self_and_project_descendants_complete?).to be(false) } end end - describe '#builds_in_self_and_descendants' do - subject(:builds) { pipeline.builds_in_self_and_descendants } + describe '#builds_in_self_and_project_descendants' do + subject(:builds) { pipeline.builds_in_self_and_project_descendants } let(:pipeline) { create(:ci_pipeline) } let!(:build) { create(:ci_build, pipeline: pipeline) } @@ -4087,7 +4087,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end - describe '#build_with_artifacts_in_self_and_descendants' do + describe '#build_with_artifacts_in_self_and_project_descendants' do let_it_be(:pipeline) { create(:ci_pipeline) } let!(:build) { create(:ci_build, name: 'test', pipeline: pipeline) } @@ -4095,14 +4095,14 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do let!(:child_build) { create(:ci_build, :artifacts, name: 'test', pipeline: child_pipeline) } it 'returns the build with a given name, having artifacts' do - expect(pipeline.build_with_artifacts_in_self_and_descendants('test')).to eq(child_build) + expect(pipeline.build_with_artifacts_in_self_and_project_descendants('test')).to eq(child_build) end context 'when same job name is present in both parent and child pipeline' do let!(:build) { create(:ci_build, :artifacts, name: 'test', pipeline: pipeline) } it 'returns the job in the parent pipeline' do - expect(pipeline.build_with_artifacts_in_self_and_descendants('test')).to eq(build) + expect(pipeline.build_with_artifacts_in_self_and_project_descendants('test')).to eq(build) end end end @@ -4183,7 +4183,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end - describe '#latest_report_builds_in_self_and_descendants' do + describe '#latest_report_builds_in_self_and_project_descendants' do let_it_be(:pipeline) { create(:ci_pipeline, project: project) } let_it_be(:child_pipeline) { create(:ci_pipeline, child_of: pipeline) } let_it_be(:grandchild_pipeline) { create(:ci_pipeline, child_of: child_pipeline) } @@ -4193,21 +4193,21 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do child_build = create(:ci_build, :coverage_reports, pipeline: child_pipeline) grandchild_build = create(:ci_build, :codequality_reports, pipeline: grandchild_pipeline) - expect(pipeline.latest_report_builds_in_self_and_descendants).to contain_exactly(parent_build, child_build, grandchild_build) + expect(pipeline.latest_report_builds_in_self_and_project_descendants).to contain_exactly(parent_build, child_build, grandchild_build) end it 'filters builds by scope' do create(:ci_build, :test_reports, pipeline: pipeline) grandchild_build = create(:ci_build, :codequality_reports, pipeline: grandchild_pipeline) - expect(pipeline.latest_report_builds_in_self_and_descendants(Ci::JobArtifact.of_report_type(:codequality))).to contain_exactly(grandchild_build) + expect(pipeline.latest_report_builds_in_self_and_project_descendants(Ci::JobArtifact.of_report_type(:codequality))).to contain_exactly(grandchild_build) end it 'only returns builds that are not retried' do create(:ci_build, :codequality_reports, :retried, pipeline: grandchild_pipeline) grandchild_build = create(:ci_build, :codequality_reports, pipeline: grandchild_pipeline) - expect(pipeline.latest_report_builds_in_self_and_descendants).to contain_exactly(grandchild_build) + expect(pipeline.latest_report_builds_in_self_and_project_descendants).to contain_exactly(grandchild_build) end end @@ -4967,8 +4967,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end - describe '#self_and_ancestors' do - subject(:self_and_ancestors) { pipeline.self_and_ancestors } + describe '#self_and_project_ancestors' do + subject(:self_and_project_ancestors) { pipeline.self_and_project_ancestors } context 'when pipeline is child' do let(:pipeline) { create(:ci_pipeline, :created) } @@ -4981,7 +4981,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end it 'returns parent and self' do - expect(self_and_ancestors).to contain_exactly(parent, pipeline) + expect(self_and_project_ancestors).to contain_exactly(parent, pipeline) end end @@ -4995,7 +4995,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end it 'returns only self' do - expect(self_and_ancestors).to contain_exactly(pipeline) + expect(self_and_project_ancestors).to contain_exactly(pipeline) end end end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index ae8748f8ae3..70fa942d8b8 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -1326,54 +1326,6 @@ RSpec.describe Ci::Runner do end end - describe '#assigned_to_group?' do - subject { runner.assigned_to_group? } - - context 'when project runner' do - let(:runner) { create(:ci_runner, :project, description: 'Project runner', projects: [project]) } - let(:project) { create(:project) } - - it { is_expected.to be_falsey } - end - - context 'when shared runner' do - let(:runner) { create(:ci_runner, :instance, description: 'Shared runner') } - - it { is_expected.to be_falsey } - end - - context 'when group runner' do - let(:group) { create(:group) } - let(:runner) { create(:ci_runner, :group, description: 'Group runner', groups: [group]) } - - it { is_expected.to be_truthy } - end - end - - describe '#assigned_to_project?' do - subject { runner.assigned_to_project? } - - context 'when group runner' do - let(:runner) { create(:ci_runner, :group, description: 'Group runner', groups: [group]) } - let(:group) { create(:group) } - - it { is_expected.to be_falsey } - end - - context 'when shared runner' do - let(:runner) { create(:ci_runner, :instance, description: 'Shared runner') } - - it { is_expected.to be_falsey } - end - - context 'when project runner' do - let(:runner) { create(:ci_runner, :project, description: 'Project runner', projects: [project]) } - let(:project) { create(:project) } - - it { is_expected.to be_truthy } - end - end - describe '#pick_build!' do let(:build) { create(:ci_build) } let(:runner) { create(:ci_runner) } diff --git a/spec/models/integrations/drone_ci_spec.rb b/spec/models/integrations/drone_ci_spec.rb index c43d969eec8..905fee075ad 100644 --- a/spec/models/integrations/drone_ci_spec.rb +++ b/spec/models/integrations/drone_ci_spec.rb @@ -115,7 +115,6 @@ RSpec.describe Integrations::DroneCi, :use_clean_rails_memory_store_caching do it_behaves_like Integrations::HasWebHook do include_context :drone_ci_integration - let(:drone_url) { 'https://cloud.drone.io' } let(:integration) { drone } let(:hook_url) { "#{drone_url}/hook?owner=#{project.namespace.full_path}&name=#{project.path}&access_token=#{token}" } diff --git a/spec/models/onboarding_progress_spec.rb b/spec/models/onboarding/progress_spec.rb similarity index 82% rename from spec/models/onboarding_progress_spec.rb rename to spec/models/onboarding/progress_spec.rb index 9688dd01c71..9d91af2487a 100644 --- a/spec/models/onboarding_progress_spec.rb +++ b/spec/models/onboarding/progress_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe OnboardingProgress do +RSpec.describe Onboarding::Progress do let(:namespace) { create(:namespace) } let(:action) { :subscription_created } @@ -34,7 +34,9 @@ RSpec.describe OnboardingProgress do subject { described_class.incomplete_actions(actions) } let!(:no_actions_completed) { create(:onboarding_progress) } - let!(:one_action_completed_one_action_incompleted) { create(:onboarding_progress, "#{action}_at" => Time.current) } + let!(:one_action_completed_one_action_incompleted) do + create(:onboarding_progress, "#{action}_at" => Time.current) + end context 'when given one action' do let(:actions) { action } @@ -52,8 +54,13 @@ RSpec.describe OnboardingProgress do describe '.completed_actions' do subject { described_class.completed_actions(actions) } - let!(:one_action_completed_one_action_incompleted) { create(:onboarding_progress, "#{action}_at" => Time.current) } - let!(:both_actions_completed) { create(:onboarding_progress, "#{action}_at" => Time.current, git_write_at: Time.current) } + let!(:one_action_completed_one_action_incompleted) do + create(:onboarding_progress, "#{action}_at" => Time.current) + end + + let!(:both_actions_completed) do + create(:onboarding_progress, "#{action}_at" => Time.current, git_write_at: Time.current) + end context 'when given one action' do let(:actions) { action } @@ -69,12 +76,23 @@ RSpec.describe OnboardingProgress do end describe '.completed_actions_with_latest_in_range' do - subject { described_class.completed_actions_with_latest_in_range(actions, 1.day.ago.beginning_of_day..1.day.ago.end_of_day) } + subject do + described_class.completed_actions_with_latest_in_range(actions, + 1.day.ago.beginning_of_day..1.day.ago.end_of_day) + end + + let!(:one_action_completed_in_range_one_action_incompleted) do + create(:onboarding_progress, "#{action}_at" => 1.day.ago.middle_of_day) + end - let!(:one_action_completed_in_range_one_action_incompleted) { create(:onboarding_progress, "#{action}_at" => 1.day.ago.middle_of_day) } let!(:git_write_action_completed_in_range) { create(:onboarding_progress, git_write_at: 1.day.ago.middle_of_day) } - let!(:both_actions_completed_latest_action_out_of_range) { create(:onboarding_progress, "#{action}_at" => 1.day.ago.middle_of_day, git_write_at: Time.current) } - let!(:both_actions_completed_latest_action_in_range) { create(:onboarding_progress, "#{action}_at" => 1.day.ago.middle_of_day, git_write_at: 2.days.ago.middle_of_day) } + let!(:both_actions_completed_latest_action_out_of_range) do + create(:onboarding_progress, "#{action}_at" => 1.day.ago.middle_of_day, git_write_at: Time.current) + end + + let!(:both_actions_completed_latest_action_in_range) do + create(:onboarding_progress, "#{action}_at" => 1.day.ago.middle_of_day, git_write_at: 2.days.ago.middle_of_day) + end context 'when given one action' do let(:actions) { :git_write } @@ -147,7 +165,9 @@ RSpec.describe OnboardingProgress do expect(described_class.find_by_namespace_id(namespace.id).subscription_created_at).to be_nil register_action expect(described_class.find_by_namespace_id(namespace.id).subscription_created_at).not_to be_nil - expect { described_class.register(namespace, action) }.not_to change { described_class.find_by_namespace_id(namespace.id).subscription_created_at } + expect do + described_class.register(namespace, action) + end.not_to change { described_class.find_by_namespace_id(namespace.id).subscription_created_at } end context 'when the action does not exist' do @@ -209,7 +229,9 @@ RSpec.describe OnboardingProgress do context 'when the namespace was not onboarded' do it 'does not register the action for the namespace' do expect { register_action }.not_to change { described_class.completed?(namespace, action1) }.from(false) - expect { described_class.register(namespace, action) }.not_to change { described_class.completed?(namespace, action2) }.from(false) + expect do + described_class.register(namespace, action) + end.not_to change { described_class.completed?(namespace, action2) }.from(false) end end end @@ -270,21 +292,23 @@ RSpec.describe OnboardingProgress do end describe '#number_of_completed_actions' do - subject { build(:onboarding_progress, actions.map { |x| { x => Time.current } }.inject(:merge)).number_of_completed_actions } + subject do + build(:onboarding_progress, actions.map { |x| { x => Time.current } }.inject(:merge)).number_of_completed_actions + end - context '0 completed actions' do + context 'with 0 completed actions' do let(:actions) { [:created_at, :updated_at] } it { is_expected.to eq(0) } end - context '1 completed action' do + context 'with 1 completed action' do let(:actions) { [:created_at, :subscription_created_at] } it { is_expected.to eq(1) } end - context '2 completed actions' do + context 'with 2 completed actions' do let(:actions) { [:subscription_created_at, :git_write_at] } it { is_expected.to eq(2) } diff --git a/spec/services/environments/stop_service_spec.rb b/spec/services/environments/stop_service_spec.rb index b0c9826b137..4c581f31ccb 100644 --- a/spec/services/environments/stop_service_spec.rb +++ b/spec/services/environments/stop_service_spec.rb @@ -193,7 +193,7 @@ RSpec.describe Environments::StopService do end it 'has active environment at first' do - expect(pipeline.environments_in_self_and_descendants.first).to be_available + expect(pipeline.environments_in_self_and_project_descendants.first).to be_available end context 'when user is a developer' do @@ -203,7 +203,7 @@ RSpec.describe Environments::StopService do it 'stops the active environment' do subject - expect(pipeline.environments_in_self_and_descendants.first).to be_stopping + expect(pipeline.environments_in_self_and_project_descendants.first).to be_stopping end context 'when pipeline is a branch pipeline for merge request' do @@ -218,7 +218,7 @@ RSpec.describe Environments::StopService do it 'does not stop the active environment' do subject - expect(pipeline.environments_in_self_and_descendants.first).to be_available + expect(pipeline.environments_in_self_and_project_descendants.first).to be_available end end @@ -244,7 +244,7 @@ RSpec.describe Environments::StopService do it 'does not stop the active environment' do subject - expect(pipeline.environments_in_self_and_descendants.first).to be_available + expect(pipeline.environments_in_self_and_project_descendants.first).to be_available end end @@ -268,7 +268,7 @@ RSpec.describe Environments::StopService do it 'does not stop the active environment' do subject - expect(pipeline.environments_in_self_and_descendants.first).to be_available + expect(pipeline.environments_in_self_and_project_descendants.first).to be_available end end end diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb index 0cfde9ef434..0a8164c9ca3 100644 --- a/spec/services/groups/create_service_spec.rb +++ b/spec/services/groups/create_service_spec.rb @@ -79,7 +79,7 @@ RSpec.describe Groups::CreateService, '#execute' do it { is_expected.to be_persisted } it 'adds an onboarding progress record' do - expect { subject }.to change(OnboardingProgress, :count).from(0).to(1) + expect { subject }.to change(Onboarding::Progress, :count).from(0).to(1) end context 'with before_commit callback' do @@ -108,7 +108,7 @@ RSpec.describe Groups::CreateService, '#execute' do it { is_expected.to be_persisted } it 'does not add an onboarding progress record' do - expect { subject }.not_to change(OnboardingProgress, :count).from(0) + expect { subject }.not_to change(Onboarding::Progress, :count).from(0) end it_behaves_like 'has sync-ed traversal_ids' diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb index fe9f3ddc14d..25696ca209e 100644 --- a/spec/services/members/create_service_spec.rb +++ b/spec/services/members/create_service_spec.rb @@ -20,10 +20,10 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_ case source when Project source.add_maintainer(user) - OnboardingProgress.onboard(source.namespace) + Onboarding::Progress.onboard(source.namespace) when Group source.add_owner(user) - OnboardingProgress.onboard(source) + Onboarding::Progress.onboard(source) end end @@ -59,7 +59,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_ it 'adds a user to members' do expect(execute_service[:status]).to eq(:success) expect(source.users).to include member - expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(true) + expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(true) end context 'when user_id is passed as an integer' do @@ -68,7 +68,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_ it 'successfully creates member' do expect(execute_service[:status]).to eq(:success) expect(source.users).to include member - expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(true) + expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(true) end end @@ -78,7 +78,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_ it 'successfully creates members' do expect(execute_service[:status]).to eq(:success) expect(source.users).to include(member, user_invited_by_id) - expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(true) + expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(true) end end @@ -88,7 +88,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_ it 'successfully creates members' do expect(execute_service[:status]).to eq(:success) expect(source.users).to include(member, user_invited_by_id) - expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(true) + expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(true) end end @@ -98,7 +98,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_ it 'adds a user to members' do expect(execute_service[:status]).to eq(:success) expect(source.users).to include member - expect(OnboardingProgress.completed?(source, :user_added)).to be(true) + expect(Onboarding::Progress.completed?(source, :user_added)).to be(true) end it 'triggers a members added event' do @@ -119,7 +119,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_ expect(execute_service[:status]).to eq(:error) expect(execute_service[:message]).to be_present expect(source.users).not_to include member - expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(false) + expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(false) end end @@ -130,7 +130,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_ expect(execute_service[:status]).to eq(:error) expect(execute_service[:message]).to be_present expect(source.users).not_to include member - expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(false) + expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(false) end end @@ -141,7 +141,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_ expect(execute_service[:status]).to eq(:error) expect(execute_service[:message]).to include("#{member.username}: Access level is not included in the list") expect(source.users).not_to include member - expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(false) + expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(false) end end @@ -153,7 +153,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_ it 'allows already invited members to be re-invited by email and updates the member access' do expect(execute_service[:status]).to eq(:success) expect(invited_member.reset.access_level).to eq ProjectMember::MAINTAINER - expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(true) + expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(true) end end @@ -170,7 +170,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_ it 'does not update the member' do expect(execute_service[:status]).to eq(:error) expect(execute_service[:message]).to eq("#{project_bot.username}: not authorized to update member") - expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(false) + expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(false) end end @@ -178,7 +178,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_ it 'adds the member' do expect(execute_service[:status]).to eq(:success) expect(source.users).to include project_bot - expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(true) + expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(true) end end end diff --git a/spec/services/onboarding_progress_service_spec.rb b/spec/services/onboarding_progress_service_spec.rb index ef4f4f0d822..6ff6b03d808 100644 --- a/spec/services/onboarding_progress_service_spec.rb +++ b/spec/services/onboarding_progress_service_spec.rb @@ -19,12 +19,12 @@ RSpec.describe OnboardingProgressService do context 'when onboarded' do before do - OnboardingProgress.onboard(namespace) + Onboarding::Progress.onboard(namespace) end context 'when action is already completed' do before do - OnboardingProgress.register(namespace, action) + Onboarding::Progress.register(namespace, action) end it 'does not schedule a worker' do @@ -52,13 +52,13 @@ RSpec.describe OnboardingProgressService do context 'when the namespace is a root' do before do - OnboardingProgress.onboard(namespace) + Onboarding::Progress.onboard(namespace) end it 'registers a namespace onboarding progress action for the given namespace' do execute_service - expect(OnboardingProgress.completed?(namespace, :subscription_created)).to eq(true) + expect(Onboarding::Progress.completed?(namespace, :subscription_created)).to eq(true) end end @@ -66,13 +66,13 @@ RSpec.describe OnboardingProgressService do let(:group) { create(:group, :nested) } before do - OnboardingProgress.onboard(group) + Onboarding::Progress.onboard(group) end it 'does not register a namespace onboarding progress action' do execute_service - expect(OnboardingProgress.completed?(group, :subscription_created)).to be(nil) + expect(Onboarding::Progress.completed?(group, :subscription_created)).to be(nil) end end @@ -82,7 +82,7 @@ RSpec.describe OnboardingProgressService do it 'does not register a namespace onboarding progress action' do execute_service - expect(OnboardingProgress.completed?(namespace, :subscription_created)).to be(nil) + expect(Onboarding::Progress.completed?(namespace, :subscription_created)).to be(nil) end end end diff --git a/spec/support/shared_examples/graphql/members_shared_examples.rb b/spec/support/shared_examples/graphql/members_shared_examples.rb index f0c50e20a72..5cba8baa829 100644 --- a/spec/support/shared_examples/graphql/members_shared_examples.rb +++ b/spec/support/shared_examples/graphql/members_shared_examples.rb @@ -52,6 +52,15 @@ RSpec.shared_examples 'querying members with a group' do expect(subject).to contain_exactly(resource_member, group_1_member, root_group_member) end + context 'with sort options' do + let(:args) { { sort: 'name_asc' } } + + it 'searches users by user name' do + # the order is important here + expect(subject.items).to eq([root_group_member, resource_member, group_1_member]) + end + end + context 'with search' do context 'when the search term matches a user' do let(:args) { { search: 'test' } } diff --git a/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb b/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb index 64b2dcb8d36..2f693edeb53 100644 --- a/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb +++ b/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb @@ -7,6 +7,30 @@ RSpec.shared_examples Integrations::HasWebHook do it { is_expected.to have_one(:service_hook).inverse_of(:integration).with_foreign_key(:service_id) } end + describe 'callbacks' do + it 'calls #update_web_hook! when enabled' do + expect(integration).to receive(:update_web_hook!) + + integration.active = true + integration.save! + end + + it 'does not call #update_web_hook! when disabled' do + expect(integration).not_to receive(:update_web_hook!) + + integration.active = false + integration.save! + end + + it 'does not call #update_web_hook! when validation fails' do + expect(integration).not_to receive(:update_web_hook!) + + integration.active = true + integration.project = nil + expect(integration.save).to be(false) + end + end + describe '#hook_url' do it 'returns a string' do expect(integration.hook_url).to be_a(String) diff --git a/spec/workers/namespaces/onboarding_issue_created_worker_spec.rb b/spec/workers/namespaces/onboarding_issue_created_worker_spec.rb index 53116815ce7..0a896d864b7 100644 --- a/spec/workers/namespaces/onboarding_issue_created_worker_spec.rb +++ b/spec/workers/namespaces/onboarding_issue_created_worker_spec.rb @@ -19,11 +19,11 @@ RSpec.describe Namespaces::OnboardingIssueCreatedWorker, '#perform' do let(:job_args) { [namespace.id] } it 'sets the onboarding progress action' do - OnboardingProgress.onboard(namespace) + Onboarding::Progress.onboard(namespace) subject - expect(OnboardingProgress.completed?(namespace, :issue_created)).to eq(true) + expect(Onboarding::Progress.completed?(namespace, :issue_created)).to eq(true) end end end