From cf05fd7f3956f0b1a17caf313e89bb7b3315d947 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 5 May 2021 15:10:05 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- GITALY_SERVER_VERSION | 2 +- .../boards/components/board_card.vue | 3 +- .../javascripts/lib/utils/recurrence.js | 154 ++++++++ .../drawer/pipeline_editor_drawer.vue | 76 ++++ .../pipeline_editor/pipeline_editor_home.vue | 10 +- .../projects/ci/pipeline_editor_controller.rb | 1 + .../projects/pipelines_controller.rb | 2 +- app/graphql/types/metadata/kas_type.rb | 18 + app/graphql/types/metadata_type.rb | 2 + app/helpers/groups_helper.rb | 8 + app/models/ci/stage.rb | 7 +- app/models/instance_metadata.rb | 3 +- app/models/instance_metadata/kas.rb | 15 + app/presenters/ci/stage_presenter.rb | 32 ++ .../layouts/nav/sidebar/_group.html.haml | 17 +- app/views/projects/stage/_stage.html.haml | 6 +- .../feature-adds-kas-metadata-to-graphql.yml | 5 + .../sh-fix-nplus-one-pipelines-show.yml | 5 + .../development/pipeline_editor_drawer.yml | 8 + doc/api/graphql/reference/index.md | 11 + doc/development/migration_style_guide.md | 12 + .../database/migrations/instrumentation.rb | 3 + lib/gitlab/kas.rb | 22 ++ lib/tasks/gitlab/db.rake | 10 +- locale/gitlab.pot | 6 + package.json | 2 +- .../projects/pipelines_controller_spec.rb | 33 ++ ...ith_external_authorization_service_spec.rb | 6 +- spec/features/groups/navbar_spec.rb | 3 +- spec/features/groups_spec.rb | 6 +- .../boards/components/board_card_spec.js | 112 +++--- spec/frontend/lib/utils/recurrence_spec.js | 333 ++++++++++++++++++ .../drawer/pipeline_editor_drawer_spec.js | 74 ++++ .../pipeline_editor_home_spec.js | 26 +- .../resolvers/metadata_resolver_spec.rb | 5 +- spec/graphql/types/metadata/kas_type_spec.rb | 8 + spec/lib/gitlab/kas_spec.rb | 40 +++ spec/models/instance_metadata/kas_spec.rb | 33 ++ spec/models/instance_metadata_spec.rb | 3 +- spec/presenters/ci/stage_presenter_spec.rb | 49 +++ .../api/graphql/metadata_query_spec.rb | 46 ++- .../navbar_structure_context.rb | 17 +- spec/tasks/gitlab/db_rake_spec.rb | 22 +- .../nav/sidebar/_group.html.haml_spec.rb | 34 +- yarn.lock | 8 +- 45 files changed, 1168 insertions(+), 130 deletions(-) create mode 100644 app/assets/javascripts/lib/utils/recurrence.js create mode 100644 app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue create mode 100644 app/graphql/types/metadata/kas_type.rb create mode 100644 app/models/instance_metadata/kas.rb create mode 100644 app/presenters/ci/stage_presenter.rb create mode 100644 changelogs/unreleased/feature-adds-kas-metadata-to-graphql.yml create mode 100644 changelogs/unreleased/sh-fix-nplus-one-pipelines-show.yml create mode 100644 config/feature_flags/development/pipeline_editor_drawer.yml create mode 100644 spec/frontend/lib/utils/recurrence_spec.js create mode 100644 spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js create mode 100644 spec/graphql/types/metadata/kas_type_spec.rb create mode 100644 spec/models/instance_metadata/kas_spec.rb create mode 100644 spec/presenters/ci/stage_presenter_spec.rb diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 94c23ab5110..b15a3879a77 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -9523fe6434ea464a6a16c895222a4b001a5c0bca +1481a9195c200e375a177cf201058b88bebe271b diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index 3e9c663a036..2821b799cef 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -1,5 +1,5 @@ + diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue index adba55f9f4b..dfe9c82b912 100644 --- a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue +++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue @@ -1,5 +1,7 @@ diff --git a/app/controllers/projects/ci/pipeline_editor_controller.rb b/app/controllers/projects/ci/pipeline_editor_controller.rb index 4136a10e124..13c22356b60 100644 --- a/app/controllers/projects/ci/pipeline_editor_controller.rb +++ b/app/controllers/projects/ci/pipeline_editor_controller.rb @@ -6,6 +6,7 @@ class Projects::Ci::PipelineEditorController < Projects::ApplicationController push_frontend_feature_flag(:ci_config_merged_tab, @project, default_enabled: :yaml) push_frontend_feature_flag(:pipeline_editor_empty_state_action, @project, default_enabled: :yaml) push_frontend_feature_flag(:pipeline_editor_branch_switcher, @project, default_enabled: :yaml) + push_frontend_feature_flag(:pipeline_editor_drawer, @project, default_enabled: :yaml) end feature_category :pipeline_authoring diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 62b464fe955..82ff7d77a6a 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -227,7 +227,7 @@ class Projects::PipelinesController < Projects::ApplicationController end def render_show - @stages = @pipeline.stages.with_latest_and_retried_statuses + @stages = @pipeline.stages respond_to do |format| format.html do diff --git a/app/graphql/types/metadata/kas_type.rb b/app/graphql/types/metadata/kas_type.rb new file mode 100644 index 00000000000..8af4c23270b --- /dev/null +++ b/app/graphql/types/metadata/kas_type.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Types + module Metadata + class KasType < ::Types::BaseObject + graphql_name 'Kas' + + authorize :read_instance_metadata + + field :enabled, GraphQL::BOOLEAN_TYPE, null: false, + description: 'Indicates whether the Kubernetes Agent Server is enabled.' + field :version, GraphQL::STRING_TYPE, null: true, + description: 'KAS version.' + field :external_url, GraphQL::STRING_TYPE, null: true, + description: 'The URL used by the Agents to communicate with KAS.' + end + end +end diff --git a/app/graphql/types/metadata_type.rb b/app/graphql/types/metadata_type.rb index 0c360d4f292..851c2a3f1e3 100644 --- a/app/graphql/types/metadata_type.rb +++ b/app/graphql/types/metadata_type.rb @@ -10,5 +10,7 @@ module Types description: 'Version.' field :revision, GraphQL::STRING_TYPE, null: false, description: 'Revision.' + field :kas, ::Types::Metadata::KasType, null: false, + description: 'Metadata about KAS.' end end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 79d89c55f28..a78cd752223 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -38,6 +38,14 @@ module GroupsHelper ] end + def group_information_title(group) + if Feature.enabled?(:sidebar_refactor, current_user) + group.subgroup? ? _('Subgroup information') : _('Group information') + else + group.subgroup? ? _('Subgroup overview') : _('Group overview') + end + end + def group_container_registry_nav? Gitlab.config.registry.enabled && can?(current_user, :read_container_image, @group) diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index 7c5324a2181..ef920b2d589 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -6,6 +6,7 @@ module Ci include Importable include Ci::HasStatus include Gitlab::OptimisticLocking + include Presentable enum status: Ci::HasStatus::STATUSES_ENUM @@ -22,12 +23,6 @@ module Ci scope :ordered, -> { order(position: :asc) } scope :in_pipelines, ->(pipelines) { where(pipeline: pipelines) } scope :by_name, ->(names) { where(name: names) } - scope :with_latest_and_retried_statuses, -> do - includes( - latest_statuses: [:pipeline, project: :namespace], - retried_statuses: [:pipeline, project: :namespace] - ) - end with_options unless: :importing? do validates :project, presence: true diff --git a/app/models/instance_metadata.rb b/app/models/instance_metadata.rb index 96622d0b1b3..6cac78178e0 100644 --- a/app/models/instance_metadata.rb +++ b/app/models/instance_metadata.rb @@ -1,10 +1,11 @@ # frozen_string_literal: true class InstanceMetadata - attr_reader :version, :revision + attr_reader :version, :revision, :kas def initialize(version: Gitlab::VERSION, revision: Gitlab.revision) @version = version @revision = revision + @kas = ::InstanceMetadata::Kas.new end end diff --git a/app/models/instance_metadata/kas.rb b/app/models/instance_metadata/kas.rb new file mode 100644 index 00000000000..7d2d71120b5 --- /dev/null +++ b/app/models/instance_metadata/kas.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class InstanceMetadata::Kas + attr_reader :enabled, :version, :external_url + + def initialize + @enabled = Gitlab::Kas.enabled? + @version = Gitlab::Kas.version if @enabled + @external_url = Gitlab::Kas.external_url if @enabled + end + + def self.declarative_policy_class + "InstanceMetadataPolicy" + end +end diff --git a/app/presenters/ci/stage_presenter.rb b/app/presenters/ci/stage_presenter.rb new file mode 100644 index 00000000000..9ec3f8d153a --- /dev/null +++ b/app/presenters/ci/stage_presenter.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Ci + class StagePresenter < Gitlab::View::Presenter::Delegated + presents :stage + + def latest_ordered_statuses + preload_statuses(stage.statuses.latest_ordered) + end + + def retried_ordered_statuses + preload_statuses(stage.statuses.retried_ordered) + end + + private + + def preload_statuses(statuses) + loaded_statuses = statuses.load + statuses.tap do |statuses| + # rubocop: disable CodeReuse/ActiveRecord + ActiveRecord::Associations::Preloader.new.preload(preloadable_statuses(loaded_statuses), %w[pipeline tags job_artifacts_archive metadata]) + # rubocop: enable CodeReuse/ActiveRecord + end + end + + def preloadable_statuses(statuses) + statuses.reject do |status| + status.instance_of?(::GenericCommitStatus) || status.instance_of?(::Ci::Bridge) + end + end + end +end diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index a89ba072c0e..286d12b9ac8 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -1,7 +1,6 @@ - issues_count = cached_issuables_count(@group, type: :issues) - merge_requests_count = group_open_merge_requests_count(@group) - aside_title = @group.subgroup? ? _('Subgroup navigation') : _('Group navigation') -- overview_title = @group.subgroup? ? _('Subgroup overview') : _('Group overview') %aside.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?), **sidebar_tracking_attributes_by_object(@group), 'aria-label': aside_title } .nav-sidebar-inner-scroll @@ -19,21 +18,23 @@ = nav_link(path: paths, unless: -> { current_path?('groups/contribution_analytics#show') }, html_options: { class: 'home' }) do = link_to group_path(@group) do .nav-icon-container - = sprite_icon('home') + - sprite = Feature.enabled?(:sidebar_refactor, current_user) ? 'group' : 'home' + = sprite_icon(sprite) %span.nav-item-name - = overview_title + = group_information_title(@group) %ul.sidebar-sub-level-items = nav_link(path: ['groups#show', 'groups#details', 'groups#activity', 'groups#subgroups'], html_options: { class: "fly-out-top-item" } ) do = link_to group_path(@group) do %strong.fly-out-top-item-name - = overview_title + = group_information_title(@group) %li.divider.fly-out-top-item - = nav_link(path: ['groups#show', 'groups#details', 'groups#subgroups'], html_options: { class: 'home' }) do - = link_to details_group_path(@group), title: _('Group details') do - %span - = _('Details') + - if Feature.disabled?(:sidebar_refactor, current_user) + = nav_link(path: ['groups#show', 'groups#details', 'groups#subgroups'], html_options: { class: 'home' }) do + = link_to details_group_path(@group), title: _('Group details') do + %span + = _('Details') - if group_sidebar_link?(:activity) = nav_link(path: 'groups#activity') do diff --git a/app/views/projects/stage/_stage.html.haml b/app/views/projects/stage/_stage.html.haml index 92bfd5a48a8..387c8fb3234 100644 --- a/app/views/projects/stage/_stage.html.haml +++ b/app/views/projects/stage/_stage.html.haml @@ -1,3 +1,5 @@ +- stage = stage.present(current_user: current_user) + %tr %th{ colspan: 10 } %strong @@ -6,8 +8,8 @@ = ci_icon_for_status(stage.status)   = stage.name.titleize -= render stage.latest_statuses, stage: false, ref: false, pipeline_link: false, allow_retry: true -= render stage.retried_statuses, stage: false, ref: false, pipeline_link: false, retried: true += render stage.latest_ordered_statuses, stage: false, ref: false, pipeline_link: false, allow_retry: true += render stage.retried_ordered_statuses, stage: false, ref: false, pipeline_link: false, retried: true %tr %td{ colspan: 10 }   diff --git a/changelogs/unreleased/feature-adds-kas-metadata-to-graphql.yml b/changelogs/unreleased/feature-adds-kas-metadata-to-graphql.yml new file mode 100644 index 00000000000..dd4204308a9 --- /dev/null +++ b/changelogs/unreleased/feature-adds-kas-metadata-to-graphql.yml @@ -0,0 +1,5 @@ +--- +title: Expose KAS metadata through GraphQL - enabled, version and externalUrl +merge_request: 59696 +author: +type: added diff --git a/changelogs/unreleased/sh-fix-nplus-one-pipelines-show.yml b/changelogs/unreleased/sh-fix-nplus-one-pipelines-show.yml new file mode 100644 index 00000000000..ebaf2aee123 --- /dev/null +++ b/changelogs/unreleased/sh-fix-nplus-one-pipelines-show.yml @@ -0,0 +1,5 @@ +--- +title: Fix N+1 SQL queries in PipelinesController#show +merge_request: 60794 +author: +type: fixed diff --git a/config/feature_flags/development/pipeline_editor_drawer.yml b/config/feature_flags/development/pipeline_editor_drawer.yml new file mode 100644 index 00000000000..354161b0ae8 --- /dev/null +++ b/config/feature_flags/development/pipeline_editor_drawer.yml @@ -0,0 +1,8 @@ +--- +name: pipeline_editor_drawer +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60856 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329806 +milestone: '13.12' +type: development +group: group::pipeline authoring +default_enabled: false diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 8b2c264b49d..6651da9267c 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -9511,6 +9511,16 @@ four standard [pagination arguments](#connection-pagination-arguments): | `readJobArtifacts` | [`Boolean!`](#boolean) | Indicates the user can perform `read_job_artifacts` on this resource. | | `updateBuild` | [`Boolean!`](#boolean) | Indicates the user can perform `update_build` on this resource. | +### `Kas` + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `enabled` | [`Boolean!`](#boolean) | Indicates whether the Kubernetes Agent Server is enabled. | +| `externalUrl` | [`String`](#string) | The URL used by the Agents to communicate with KAS. | +| `version` | [`String`](#string) | KAS version. | + ### `Label` #### Fields @@ -10110,6 +10120,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | +| `kas` | [`Kas!`](#kas) | Metadata about KAS. | | `revision` | [`String!`](#string) | Revision. | | `version` | [`String!`](#string) | Version. | diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index 9aefbed4f07..e1444f1a726 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -126,6 +126,18 @@ Examples: - `index_projects_on_id_service_desk_enabled` - `index_clusters_on_enabled_cluster_type_id_and_created_at` +### Truncate long index names + +PostgreSQL [limits the length of identifiers](https://www.postgresql.org/docs/current/limits.html), +like column or index names. Column names are not usually a problem, but index names tend +to be longer. Some methods for shortening a name that's too long: + +- Prefix it with `i_` instead of `index_`. +- Skip redundant prefixes. For example, + `index_vulnerability_findings_remediations_on_vulnerability_remediation_id` becomes + `index_vulnerability_findings_remediations_on_remediation_id`. +- Instead of columns, specify the purpose of the index, such as `index_users_for_unconfirmation_notification`. + ## Heavy operations in a single transaction When using a single-transaction migration, a transaction holds a database connection diff --git a/lib/gitlab/database/migrations/instrumentation.rb b/lib/gitlab/database/migrations/instrumentation.rb index 9cc1196946e..e9ef80d5198 100644 --- a/lib/gitlab/database/migrations/instrumentation.rb +++ b/lib/gitlab/database/migrations/instrumentation.rb @@ -4,6 +4,9 @@ module Gitlab module Database module Migrations class Instrumentation + RESULT_DIR = Rails.root.join('tmp', 'migration-testing').freeze + STATS_FILENAME = 'migration-stats.json' + attr_reader :observations def initialize(observers = ::Gitlab::Database::Migrations::Observers.all_observers) diff --git a/lib/gitlab/kas.rb b/lib/gitlab/kas.rb index 7a674cb5c21..7b2c792ebca 100644 --- a/lib/gitlab/kas.rb +++ b/lib/gitlab/kas.rb @@ -3,6 +3,7 @@ module Gitlab module Kas INTERNAL_API_REQUEST_HEADER = 'Gitlab-Kas-Api-Request' + VERSION_FILE = 'GITLAB_KAS_VERSION' JWT_ISSUER = 'gitlab-kas' include JwtAuthenticatable @@ -29,6 +30,27 @@ module Gitlab Feature.enabled?(:kubernetes_agent_on_gitlab_com, project, default_enabled: :yaml) end + + # Return GitLab KAS version + # + # @return [String] version + def version + @_version ||= Rails.root.join(VERSION_FILE).read.chomp + end + + # Return GitLab KAS external_url + # + # @return [String] external_url + def external_url + Gitlab.config.gitlab_kas.external_url + end + + # Return whether GitLab KAS is enabled + # + # @return [Boolean] external_url + def enabled? + !!Gitlab.config['gitlab_kas']&.fetch('enabled', false) + end end end end diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake index 939053697c5..bbfdf598e42 100644 --- a/lib/tasks/gitlab/db.rake +++ b/lib/tasks/gitlab/db.rake @@ -217,9 +217,11 @@ namespace :gitlab do end desc 'Run migrations with instrumentation' - task :migration_testing, [:result_file] => :environment do |_, args| - result_file = args[:result_file] || raise("Please specify result_file argument") - raise "File exists already, won't overwrite: #{result_file}" if File.exist?(result_file) + task migration_testing: :environment do + result_dir = Gitlab::Database::Migrations::Instrumentation::RESULT_DIR + raise "Directory exists already, won't overwrite: #{result_dir}" if File.exist?(result_dir) + + Dir.mkdir(result_dir) verbose_was = ActiveRecord::Migration.verbose ActiveRecord::Migration.verbose = true @@ -240,7 +242,7 @@ namespace :gitlab do end ensure if instrumentation - File.open(result_file, 'wb+') do |io| + File.open(File.join(result_dir, Gitlab::Database::Migrations::Instrumentation::STATS_FILENAME), 'wb+') do |io| io << instrumentation.observations.to_json end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 16e13d780e0..2097a1b5661 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -15477,6 +15477,9 @@ msgstr "" msgid "Group info:" msgstr "" +msgid "Group information" +msgstr "" + msgid "Group is required when cluster_type is :group" msgstr "" @@ -30725,6 +30728,9 @@ msgstr "" msgid "StorageSize|Unknown" msgstr "" +msgid "Subgroup information" +msgstr "" + msgid "Subgroup milestone" msgstr "" diff --git a/package.json b/package.json index 7e127bd77c4..47ca621611a 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "@gitlab/favicon-overlay": "2.0.0", "@gitlab/svgs": "1.192.0", "@gitlab/tributejs": "1.0.0", - "@gitlab/ui": "29.13.0", + "@gitlab/ui": "29.14.0", "@gitlab/visual-review-tools": "1.6.1", "@rails/actioncable": "^6.0.3-4", "@rails/ujs": "^6.0.3-4", diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index 6236a47cde1..2cbc85232b4 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -290,6 +290,39 @@ RSpec.describe Projects::PipelinesController do end end + describe 'GET #show' do + render_views + + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + + subject { get_pipeline_html } + + def get_pipeline_html + get :show, params: { namespace_id: project.namespace, project_id: project, id: pipeline }, format: :html + end + + def create_build_with_artifacts(stage, stage_idx, name) + create(:ci_build, :artifacts, :tags, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name) + end + + before do + create_build_with_artifacts('build', 0, 'job1') + create_build_with_artifacts('build', 0, 'job2') + end + + it 'avoids N+1 database queries', :request_store do + get_pipeline_html + + control_count = ActiveRecord::QueryRecorder.new { get_pipeline_html }.count + expect(response).to have_gitlab_http_status(:ok) + + create_build_with_artifacts('build', 0, 'job3') + + expect { get_pipeline_html }.not_to exceed_query_limit(control_count) + expect(response).to have_gitlab_http_status(:ok) + end + end + describe 'GET show.json' do let(:pipeline) { create(:ci_pipeline, project: project) } diff --git a/spec/features/groups/group_page_with_external_authorization_service_spec.rb b/spec/features/groups/group_page_with_external_authorization_service_spec.rb index 187d878472e..59a7feb813b 100644 --- a/spec/features/groups/group_page_with_external_authorization_service_spec.rb +++ b/spec/features/groups/group_page_with_external_authorization_service_spec.rb @@ -15,8 +15,7 @@ RSpec.describe 'The group page' do def expect_all_sidebar_links within('.nav-sidebar') do - expect(page).to have_link('Group overview') - expect(page).to have_link('Details') + expect(page).to have_link('Group information') expect(page).to have_link('Activity') expect(page).to have_link('Issues') expect(page).to have_link('Merge requests') @@ -44,8 +43,7 @@ RSpec.describe 'The group page' do visit group_path(group) within('.nav-sidebar') do - expect(page).to have_link('Group overview') - expect(page).to have_link('Details') + expect(page).to have_link('Group information') expect(page).not_to have_link('Activity') expect(page).not_to have_link('Contribution') diff --git a/spec/features/groups/navbar_spec.rb b/spec/features/groups/navbar_spec.rb index 021b1af54d4..7f0aef6b300 100644 --- a/spec/features/groups/navbar_spec.rb +++ b/spec/features/groups/navbar_spec.rb @@ -14,9 +14,8 @@ RSpec.describe 'Group navbar' do let(:structure) do [ { - nav_item: _('Group overview'), + nav_item: _('Group information'), nav_sub_items: [ - _('Details'), _('Activity') ] }, diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index a43946925bf..0fab5718aa6 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -368,14 +368,14 @@ RSpec.describe 'Group' do expect(page).to have_content(nested_group.name) expect(page).to have_content(project.name) - expect(page).to have_link('Group overview') + expect(page).to have_link('Group information') end - it 'renders subgroup page with the text "Subgroup overview"' do + it 'renders subgroup page with the text "Subgroup information"' do visit group_path(nested_group) wait_for_requests - expect(page).to have_link('Subgroup overview') + expect(page).to have_link('Subgroup information') end it 'renders project page with the text "Project overview"' do diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js index e95cb17ee84..ceafa6ead94 100644 --- a/spec/frontend/boards/components/board_card_spec.js +++ b/spec/frontend/boards/components/board_card_spec.js @@ -15,7 +15,7 @@ describe('Board card', () => { const localVue = createLocalVue(); localVue.use(Vuex); - const createStore = ({ initialState = {}, isSwimlanesOn = false } = {}) => { + const createStore = ({ initialState = {} } = {}) => { mockActions = { toggleBoardItem: jest.fn(), toggleBoardItemMultiSelection: jest.fn(), @@ -30,7 +30,6 @@ describe('Board card', () => { }, actions: mockActions, getters: { - isSwimlanesOn: () => isSwimlanesOn, isEpicBoard: () => false, }, }); @@ -90,72 +89,65 @@ describe('Board card', () => { }); }); - describe.each` - isSwimlanesOn - ${true} | ${false} - `('when isSwimlanesOn is $isSwimlanesOn', ({ isSwimlanesOn }) => { - it('should not highlight the card by default', async () => { - createStore({ isSwimlanesOn }); - mountComponent(); + it('should not highlight the card by default', async () => { + createStore(); + mountComponent(); - expect(wrapper.classes()).not.toContain('is-active'); - expect(wrapper.classes()).not.toContain('multi-select'); + expect(wrapper.classes()).not.toContain('is-active'); + expect(wrapper.classes()).not.toContain('multi-select'); + }); + + it('should highlight the card with a correct style when selected', async () => { + createStore({ + initialState: { + activeId: mockIssue.id, + }, + }); + mountComponent(); + + expect(wrapper.classes()).toContain('is-active'); + expect(wrapper.classes()).not.toContain('multi-select'); + }); + + it('should highlight the card with a correct style when multi-selected', async () => { + createStore({ + initialState: { + activeId: inactiveId, + selectedBoardItems: [mockIssue], + }, + }); + mountComponent(); + + expect(wrapper.classes()).toContain('multi-select'); + expect(wrapper.classes()).not.toContain('is-active'); + }); + + describe('when mouseup event is called on the card', () => { + beforeEach(() => { + createStore(); + mountComponent(); }); - it('should highlight the card with a correct style when selected', async () => { - createStore({ - initialState: { - activeId: mockIssue.id, - }, - isSwimlanesOn, - }); - mountComponent(); + describe('when not using multi-select', () => { + it('should call vuex action "toggleBoardItem" with correct parameters', async () => { + await selectCard(); - expect(wrapper.classes()).toContain('is-active'); - expect(wrapper.classes()).not.toContain('multi-select'); - }); - - it('should highlight the card with a correct style when multi-selected', async () => { - createStore({ - initialState: { - activeId: inactiveId, - selectedBoardItems: [mockIssue], - }, - isSwimlanesOn, - }); - mountComponent(); - - expect(wrapper.classes()).toContain('multi-select'); - expect(wrapper.classes()).not.toContain('is-active'); - }); - - describe('when mouseup event is called on the card', () => { - beforeEach(() => { - createStore({ isSwimlanesOn }); - mountComponent(); - }); - - describe('when not using multi-select', () => { - it('should call vuex action "toggleBoardItem" with correct parameters', async () => { - await selectCard(); - - expect(mockActions.toggleBoardItem).toHaveBeenCalledTimes(1); - expect(mockActions.toggleBoardItem).toHaveBeenCalledWith(expect.any(Object), { - boardItem: mockIssue, - }); + expect(mockActions.toggleBoardItem).toHaveBeenCalledTimes(1); + expect(mockActions.toggleBoardItem).toHaveBeenCalledWith(expect.any(Object), { + boardItem: mockIssue, }); }); + }); - describe('when using multi-select', () => { - it('should call vuex action "multiSelectBoardItem" with correct parameters', async () => { - await multiSelectCard(); + describe('when using multi-select', () => { + it('should call vuex action "multiSelectBoardItem" with correct parameters', async () => { + await multiSelectCard(); - expect(mockActions.toggleBoardItemMultiSelection).toHaveBeenCalledTimes(1); - expect(mockActions.toggleBoardItemMultiSelection).toHaveBeenCalledWith( - expect.any(Object), - mockIssue, - ); - }); + expect(mockActions.toggleBoardItemMultiSelection).toHaveBeenCalledTimes(1); + expect(mockActions.toggleBoardItemMultiSelection).toHaveBeenCalledWith( + expect.any(Object), + mockIssue, + ); }); }); }); diff --git a/spec/frontend/lib/utils/recurrence_spec.js b/spec/frontend/lib/utils/recurrence_spec.js new file mode 100644 index 00000000000..fc22529dffc --- /dev/null +++ b/spec/frontend/lib/utils/recurrence_spec.js @@ -0,0 +1,333 @@ +import { create, free, recall } from '~/lib/utils/recurrence'; + +const HEX = /[a-f0-9]/i; +const HEX_RE = HEX.source; +const UUIDV4 = new RegExp( + `${HEX_RE}{8}-${HEX_RE}{4}-4${HEX_RE}{3}-[89ab]${HEX_RE}{3}-${HEX_RE}{12}`, + 'i', +); + +describe('recurrence', () => { + let recurInstance; + let id; + + beforeEach(() => { + recurInstance = create(); + id = recurInstance.id; + }); + + afterEach(() => { + id = null; + recurInstance.free(); + }); + + describe('create', () => { + it('returns an object with the correct external api', () => { + expect(recurInstance).toMatchObject( + expect.objectContaining({ + id: expect.stringMatching(UUIDV4), + count: 0, + handlers: {}, + free: expect.any(Function), + handle: expect.any(Function), + eject: expect.any(Function), + occur: expect.any(Function), + reset: expect.any(Function), + }), + ); + }); + }); + + describe('recall', () => { + it('returns a previously created RecurInstance', () => { + expect(recall(id).id).toBe(id); + }); + + it("returns undefined if the provided UUID doesn't refer to a stored RecurInstance", () => { + expect(recall('1234')).toBeUndefined(); + }); + }); + + describe('free', () => { + it('returns true when the RecurInstance exists', () => { + expect(free(id)).toBe(true); + }); + + it("returns false when the ID doesn't refer to a known RecurInstance", () => { + expect(free('1234')).toBe(false); + }); + + it('removes the correct RecurInstance from the list of references', () => { + const anotherInstance = create(); + + expect(recall(id)).toEqual(recurInstance); + expect(recall(anotherInstance.id)).toEqual(anotherInstance); + + free(id); + + expect(recall(id)).toBeUndefined(); + expect(recall(anotherInstance.id)).toEqual(anotherInstance); + + anotherInstance.free(); + }); + }); + + describe('RecurInstance (`create()` return value)', () => { + it.each` + property | value | alias + ${'id'} | ${expect.stringMatching(UUIDV4)} | ${'[a string matching the UUIDv4 specification]'} + ${'count'} | ${0} | ${0} + ${'handlers'} | ${{}} | ${{}} + `( + 'has the correct primitive value $alias for the member `$property` to start', + ({ property, value }) => { + expect(recurInstance[property]).toEqual(value); + }, + ); + + describe('id', () => { + it('cannot be changed manually', () => { + expect(() => { + recurInstance.id = 'new-id'; + }).toThrow(TypeError); + + expect(recurInstance.id).toBe(id); + }); + + it.each` + method + ${'free'} + ${'handle'} + ${'eject'} + ${'occur'} + ${'reset'} + `('does not change across any method call - like after `$method`', ({ method }) => { + recurInstance[method](); + + expect(recurInstance.id).toBe(id); + }); + }); + + describe('count', () => { + it('cannot be changed manually', () => { + expect(() => { + recurInstance.count = 9999; + }).toThrow(TypeError); + + expect(recurInstance.count).toBe(0); + }); + + it.each` + method + ${'free'} + ${'handle'} + ${'eject'} + ${'reset'} + `("doesn't change in unexpected scenarios - like after a call to `$method`", ({ method }) => { + recurInstance[method](); + + expect(recurInstance.count).toBe(0); + }); + + it('increments by one each time `.occur()` is called', () => { + expect(recurInstance.count).toBe(0); + recurInstance.occur(); + expect(recurInstance.count).toBe(1); + recurInstance.occur(); + expect(recurInstance.count).toBe(2); + }); + }); + + describe('handlers', () => { + it('cannot be changed manually', () => { + const fn = jest.fn(); + + recurInstance.handle(1, fn); + expect(() => { + recurInstance.handlers = {}; + }).toThrow(TypeError); + + expect(recurInstance.handlers).toStrictEqual({ + 1: fn, + }); + }); + + it.each` + method + ${'free'} + ${'occur'} + ${'eject'} + ${'reset'} + `("doesn't change in unexpected scenarios - like after a call to `$method`", ({ method }) => { + recurInstance[method](); + + expect(recurInstance.handlers).toEqual({}); + }); + + it('adds handlers to the correct slots', () => { + const fn1 = jest.fn(); + const fn2 = jest.fn(); + + recurInstance.handle(100, fn1); + recurInstance.handle(1000, fn2); + + expect(recurInstance.handlers).toMatchObject({ + 100: fn1, + 1000: fn2, + }); + }); + }); + + describe('free', () => { + it('removes itself from recallable memory', () => { + expect(recall(id)).toEqual(recurInstance); + + recurInstance.free(); + + expect(recall(id)).toBeUndefined(); + }); + }); + + describe('handle', () => { + it('adds a handler for the provided count', () => { + const fn = jest.fn(); + + recurInstance.handle(5, fn); + + expect(recurInstance.handlers[5]).toEqual(fn); + }); + + it("doesn't add any handlers if either the count or behavior aren't provided", () => { + const fn = jest.fn(); + + recurInstance.handle(null, fn); + // Note that it's not possible to react to something not happening (without timers) + recurInstance.handle(0, fn); + recurInstance.handle(5, null); + + expect(recurInstance.handlers).toEqual({}); + }); + }); + + describe('eject', () => { + it('removes the handler assigned to the particular count slot', () => { + recurInstance.handle(1, jest.fn()); + + expect(recurInstance.handlers[1]).toBeTruthy(); + + recurInstance.eject(1); + + expect(recurInstance.handlers).toEqual({}); + }); + + it("succeeds (or fails gracefully) when the count provided doesn't have a handler assigned", () => { + recurInstance.eject('abc'); + recurInstance.eject(1); + + expect(recurInstance.handlers).toEqual({}); + }); + + it('makes no changes if no count is provided', () => { + const fn = jest.fn(); + + recurInstance.handle(1, fn); + + recurInstance.eject(); + + expect(recurInstance.handlers[1]).toStrictEqual(fn); + }); + }); + + describe('occur', () => { + it('increments the .count property by 1', () => { + expect(recurInstance.count).toBe(0); + + recurInstance.occur(); + + expect(recurInstance.count).toBe(1); + }); + + it('calls the appropriate handlers', () => { + const fn1 = jest.fn(); + const fn5 = jest.fn(); + const fn10 = jest.fn(); + + recurInstance.handle(1, fn1); + recurInstance.handle(5, fn5); + recurInstance.handle(10, fn10); + + expect(fn1).not.toHaveBeenCalled(); + expect(fn5).not.toHaveBeenCalled(); + expect(fn10).not.toHaveBeenCalled(); + + recurInstance.occur(); + + expect(fn1).toHaveBeenCalledTimes(1); + expect(fn5).not.toHaveBeenCalled(); + expect(fn10).not.toHaveBeenCalled(); + + recurInstance.occur(); + recurInstance.occur(); + recurInstance.occur(); + recurInstance.occur(); + + expect(fn1).toHaveBeenCalledTimes(1); + expect(fn5).toHaveBeenCalledTimes(1); + expect(fn10).not.toHaveBeenCalled(); + + recurInstance.occur(); + recurInstance.occur(); + recurInstance.occur(); + recurInstance.occur(); + recurInstance.occur(); + + expect(fn1).toHaveBeenCalledTimes(1); + expect(fn5).toHaveBeenCalledTimes(1); + expect(fn10).toHaveBeenCalledTimes(1); + }); + }); + + describe('reset', () => { + it('resets the count only, by default', () => { + const fn = jest.fn(); + + recurInstance.handle(3, fn); + recurInstance.occur(); + recurInstance.occur(); + + expect(recurInstance.count).toBe(2); + + recurInstance.reset(); + + expect(recurInstance.count).toBe(0); + expect(recurInstance.handlers).toEqual({ 3: fn }); + }); + + it('also resets the handlers, by specific request', () => { + const fn = jest.fn(); + + recurInstance.handle(3, fn); + recurInstance.occur(); + recurInstance.occur(); + + expect(recurInstance.count).toBe(2); + + recurInstance.reset({ handlersList: true }); + + expect(recurInstance.count).toBe(0); + expect(recurInstance.handlers).toEqual({}); + }); + + it('leaves the count in place, by request', () => { + recurInstance.occur(); + recurInstance.occur(); + + expect(recurInstance.count).toBe(2); + + recurInstance.reset({ currentCount: false }); + + expect(recurInstance.count).toBe(2); + }); + }); + }); +}); diff --git a/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js b/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js new file mode 100644 index 00000000000..587373c99b4 --- /dev/null +++ b/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js @@ -0,0 +1,74 @@ +import { shallowMount } from '@vue/test-utils'; +import PipelineEditorDrawer from '~/pipeline_editor/components/drawer/pipeline_editor_drawer.vue'; + +describe('Pipeline editor drawer', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMount(PipelineEditorDrawer); + }; + + const findToggleBtn = () => wrapper.find('[data-testid="toggleBtn"]'); + const findArrowIcon = () => wrapper.find('[data-testid="toggle-icon"]'); + const findCollapseText = () => wrapper.find('[data-testid="collapse-text"]'); + const findDrawerContent = () => wrapper.find('[data-testid="drawer-content"]'); + + const clickToggleBtn = async () => findToggleBtn().vm.$emit('click'); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when the drawer is collapsed', () => { + beforeEach(() => { + createComponent(); + }); + + it('show the left facing arrow icon', () => { + expect(findArrowIcon().props('name')).toBe('chevron-double-lg-left'); + }); + + it('does not show the collapse text', () => { + expect(findCollapseText().exists()).toBe(false); + }); + + it('does not show the drawer content', () => { + expect(findDrawerContent().exists()).toBe(false); + }); + + it('can open the drawer by clicking on the toggle button', async () => { + expect(findDrawerContent().exists()).toBe(false); + + await clickToggleBtn(); + + expect(findDrawerContent().exists()).toBe(true); + }); + }); + + describe('when the drawer is expanded', () => { + beforeEach(async () => { + createComponent(); + await clickToggleBtn(); + }); + + it('show the right facing arrow icon', () => { + expect(findArrowIcon().props('name')).toBe('chevron-double-lg-right'); + }); + + it('shows the collapse text', () => { + expect(findCollapseText().exists()).toBe(true); + }); + + it('show the drawer content', () => { + expect(findDrawerContent().exists()).toBe(true); + }); + + it('can close the drawer by clicking on the toggle button', async () => { + expect(findDrawerContent().exists()).toBe(true); + + await clickToggleBtn(); + + expect(findDrawerContent().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js index a1e3d24acfa..7aba336b8e8 100644 --- a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js +++ b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js @@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue'; +import PipelineEditorDrawer from '~/pipeline_editor/components/drawer/pipeline_editor_drawer.vue'; import PipelineEditorFileNav from '~/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue'; import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_editor_header.vue'; import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue'; @@ -13,7 +14,7 @@ import { mockLintResponse, mockCiYml } from './mock_data'; describe('Pipeline editor home wrapper', () => { let wrapper; - const createComponent = ({ props = {} } = {}) => { + const createComponent = ({ props = {}, glFeatures = {} } = {}) => { wrapper = shallowMount(PipelineEditorHome, { propsData: { ciConfigData: mockLintResponse, @@ -22,13 +23,20 @@ describe('Pipeline editor home wrapper', () => { isNewCiConfigFile: false, ...props, }, + provide: { + glFeatures: { + pipelineEditorDrawer: true, + ...glFeatures, + }, + }, }); }; - const findPipelineEditorHeader = () => wrapper.findComponent(PipelineEditorHeader); - const findPipelineEditorTabs = () => wrapper.findComponent(PipelineEditorTabs); const findCommitSection = () => wrapper.findComponent(CommitSection); const findFileNav = () => wrapper.findComponent(PipelineEditorFileNav); + const findPipelineEditorDrawer = () => wrapper.findComponent(PipelineEditorDrawer); + const findPipelineEditorHeader = () => wrapper.findComponent(PipelineEditorHeader); + const findPipelineEditorTabs = () => wrapper.findComponent(PipelineEditorTabs); afterEach(() => { wrapper.destroy(); @@ -55,6 +63,10 @@ describe('Pipeline editor home wrapper', () => { it('shows the commit section by default', () => { expect(findCommitSection().exists()).toBe(true); }); + + it('show the pipeline drawer', () => { + expect(findPipelineEditorDrawer().exists()).toBe(true); + }); }); describe('commit form toggle', () => { @@ -82,4 +94,12 @@ describe('Pipeline editor home wrapper', () => { expect(findCommitSection().exists()).toBe(true); }); }); + + describe('Pipeline drawer', () => { + it('hides the drawer when the feature flag is off', () => { + createComponent({ glFeatures: { pipelineEditorDrawer: false } }); + + expect(findPipelineEditorDrawer().exists()).toBe(false); + }); + }); }); diff --git a/spec/graphql/resolvers/metadata_resolver_spec.rb b/spec/graphql/resolvers/metadata_resolver_spec.rb index f8c01f9d531..56875e185e7 100644 --- a/spec/graphql/resolvers/metadata_resolver_spec.rb +++ b/spec/graphql/resolvers/metadata_resolver_spec.rb @@ -7,7 +7,10 @@ RSpec.describe Resolvers::MetadataResolver do describe '#resolve' do it 'returns version and revision' do - expect(resolve(described_class)).to have_attributes(version: Gitlab::VERSION, revision: Gitlab.revision) + expect(resolve(described_class)).to have_attributes( + version: Gitlab::VERSION, + revision: Gitlab.revision, + kas: kind_of(InstanceMetadata::Kas)) end end end diff --git a/spec/graphql/types/metadata/kas_type_spec.rb b/spec/graphql/types/metadata/kas_type_spec.rb new file mode 100644 index 00000000000..f90c64f0068 --- /dev/null +++ b/spec/graphql/types/metadata/kas_type_spec.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['Kas'] do + specify { expect(described_class.graphql_name).to eq('Kas') } + specify { expect(described_class).to require_graphql_authorizations(:read_instance_metadata) } +end diff --git a/spec/lib/gitlab/kas_spec.rb b/spec/lib/gitlab/kas_spec.rb index 01ced407883..e323f76b42e 100644 --- a/spec/lib/gitlab/kas_spec.rb +++ b/spec/lib/gitlab/kas_spec.rb @@ -33,6 +33,46 @@ RSpec.describe Gitlab::Kas do end end + describe '.enabled?' do + before do + allow(Gitlab).to receive(:config).and_return(gitlab_config) + end + + subject { described_class.enabled? } + + context 'gitlab_config is not enabled' do + let(:gitlab_config) { { 'gitlab_kas' => { 'enabled' => false } } } + + it { is_expected.to be_falsey } + end + + context 'gitlab_config is enabled' do + let(:gitlab_config) { { 'gitlab_kas' => { 'enabled' => true } } } + + it { is_expected.to be_truthy } + end + + context 'enabled is unset' do + let(:gitlab_config) { { 'gitlab_kas' => {} } } + + it { is_expected.to be_falsey } + end + end + + describe '.external_url' do + it 'returns gitlab_kas external_url config' do + expect(described_class.external_url).to eq(Gitlab.config.gitlab_kas.external_url) + end + end + + describe '.version' do + it 'returns gitlab_kas version config' do + version_file = Rails.root.join(described_class::VERSION_FILE) + + expect(described_class.version).to eq(version_file.read.chomp) + end + end + describe '.ensure_secret!' do context 'secret file exists' do before do diff --git a/spec/models/instance_metadata/kas_spec.rb b/spec/models/instance_metadata/kas_spec.rb new file mode 100644 index 00000000000..f8cc34fa8d3 --- /dev/null +++ b/spec/models/instance_metadata/kas_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::InstanceMetadata::Kas do + it 'has InstanceMetadataPolicy as declarative policy' do + expect(described_class.declarative_policy_class).to eq("InstanceMetadataPolicy") + end + + context 'when KAS is enabled' do + it 'has the correct properties' do + allow(Gitlab::Kas).to receive(:enabled?).and_return(true) + + expect(subject).to have_attributes( + enabled: Gitlab::Kas.enabled?, + version: Gitlab::Kas.version, + external_url: Gitlab::Kas.external_url + ) + end + end + + context 'when KAS is disabled' do + it 'has the correct properties' do + allow(Gitlab::Kas).to receive(:enabled?).and_return(false) + + expect(subject).to have_attributes( + enabled: Gitlab::Kas.enabled?, + version: nil, + external_url: nil + ) + end + end +end diff --git a/spec/models/instance_metadata_spec.rb b/spec/models/instance_metadata_spec.rb index 1835dc8a9af..e3a9167620b 100644 --- a/spec/models/instance_metadata_spec.rb +++ b/spec/models/instance_metadata_spec.rb @@ -6,7 +6,8 @@ RSpec.describe InstanceMetadata do it 'has the correct properties' do expect(subject).to have_attributes( version: Gitlab::VERSION, - revision: Gitlab.revision + revision: Gitlab.revision, + kas: kind_of(::InstanceMetadata::Kas) ) end end diff --git a/spec/presenters/ci/stage_presenter_spec.rb b/spec/presenters/ci/stage_presenter_spec.rb new file mode 100644 index 00000000000..368f03b0150 --- /dev/null +++ b/spec/presenters/ci/stage_presenter_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::StagePresenter do + let(:stage) { create(:ci_stage) } + let(:presenter) { described_class.new(stage) } + + let!(:build) { create(:ci_build, :tags, :artifacts, pipeline: stage.pipeline, stage: stage.name) } + let!(:retried_build) { create(:ci_build, :tags, :artifacts, :retried, pipeline: stage.pipeline, stage: stage.name) } + + before do + create(:generic_commit_status, pipeline: stage.pipeline, stage: stage.name) + end + + shared_examples 'preloaded associations for CI status' do + it 'preloads project' do + expect(presented_stage.association(:project)).to be_loaded + end + + it 'preloads build pipeline' do + expect(presented_stage.association(:pipeline)).to be_loaded + end + + it 'preloads build tags' do + expect(presented_stage.association(:tags)).to be_loaded + end + + it 'preloads build artifacts archive' do + expect(presented_stage.association(:job_artifacts_archive)).to be_loaded + end + + it 'preloads build artifacts metadata' do + expect(presented_stage.association(:metadata)).to be_loaded + end + end + + describe '#latest_ordered_statuses' do + subject(:presented_stage) { presenter.latest_ordered_statuses.second } + + it_behaves_like 'preloaded associations for CI status' + end + + describe '#retried_ordered_statuses' do + subject(:presented_stage) { presenter.retried_ordered_statuses.first } + + it_behaves_like 'preloaded associations for CI status' + end +end diff --git a/spec/requests/api/graphql/metadata_query_spec.rb b/spec/requests/api/graphql/metadata_query_spec.rb index 6344ec371c8..840bd7c018c 100644 --- a/spec/requests/api/graphql/metadata_query_spec.rb +++ b/spec/requests/api/graphql/metadata_query_spec.rb @@ -8,16 +8,48 @@ RSpec.describe 'getting project information' do let(:query) { graphql_query_for('metadata', {}, all_graphql_fields_for('Metadata')) } context 'logged in' do - it 'returns version and revision' do - post_graphql(query, current_user: create(:user)) - - expect(graphql_errors).to be_nil - expect(graphql_data).to eq( + let(:expected_data) do + { 'metadata' => { 'version' => Gitlab::VERSION, - 'revision' => Gitlab.revision + 'revision' => Gitlab.revision, + 'kas' => { + 'enabled' => Gitlab::Kas.enabled?, + 'version' => expected_kas_version, + 'externalUrl' => expected_kas_external_url + } } - ) + } + end + + context 'kas is enabled' do + let(:expected_kas_version) { Gitlab::Kas.version } + let(:expected_kas_external_url) { Gitlab::Kas.external_url } + + before do + allow(Gitlab::Kas).to receive(:enabled?).and_return(true) + post_graphql(query, current_user: create(:user)) + end + + it 'returns version, revision, kas_enabled, kas_version, kas_external_url' do + expect(graphql_errors).to be_nil + expect(graphql_data).to eq(expected_data) + end + end + + context 'kas is disabled' do + let(:expected_kas_version) { nil } + let(:expected_kas_external_url) { nil } + + before do + allow(Gitlab::Kas).to receive(:enabled?).and_return(false) + post_graphql(query, current_user: create(:user)) + end + + it 'returns version and revision' do + expect(graphql_errors).to be_nil + expect(graphql_data).to eq(expected_data) + end end end diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index 51aa52a28f7..65528f3900f 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -170,15 +170,18 @@ RSpec.shared_context 'group navbar structure' do } end + let(:group_information_nav_item) do + { + nav_item: _('Group information'), + nav_sub_items: [ + _('Activity') + ] + } + end + let(:structure) do [ - { - nav_item: _('Group overview'), - nav_sub_items: [ - _('Details'), - _('Activity') - ] - }, + group_information_nav_item, { nav_item: _('Issues'), nav_sub_items: [ diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb index 1392bae055a..c4623061944 100644 --- a/spec/tasks/gitlab/db_rake_spec.rb +++ b/spec/tasks/gitlab/db_rake_spec.rb @@ -298,15 +298,15 @@ RSpec.describe 'gitlab:db namespace rake task' do end describe '#migrate_with_instrumentation' do - subject { run_rake_task('gitlab:db:migration_testing', "[#{filename}]") } + subject { run_rake_task('gitlab:db:migration_testing') } let(:ctx) { double('ctx', migrations: all_migrations, schema_migration: double, get_all_versions: existing_versions) } let(:instrumentation) { instance_double(Gitlab::Database::Migrations::Instrumentation, observations: observations) } let(:existing_versions) { [1] } let(:all_migrations) { [double('migration1', version: 1), pending_migration] } let(:pending_migration) { double('migration2', version: 2) } - let(:filename) { 'results-file.json'} - let(:buffer) { StringIO.new } + let(:filename) { Gitlab::Database::Migrations::Instrumentation::STATS_FILENAME } + let!(:directory) { Dir.mktmpdir } let(:observations) { %w[some data] } before do @@ -316,17 +316,19 @@ RSpec.describe 'gitlab:db namespace rake task' do allow(instrumentation).to receive(:observe).and_yield - allow(File).to receive(:open).with(filename, 'wb+').and_yield(buffer) + allow(Dir).to receive(:mkdir) + allow(File).to receive(:exist?).with(directory).and_return(false) + stub_const('Gitlab::Database::Migrations::Instrumentation::RESULT_DIR', directory) end - it 'fails when given no filename argument' do - expect { run_rake_task('gitlab:db:migration_testing') }.to raise_error(/specify result_file/) + after do + FileUtils.rm_rf([directory]) end - it 'fails when the given file already exists' do - expect(File).to receive(:exist?).with(filename).and_return(true) + it 'fails when the directory already exists' do + expect(File).to receive(:exist?).with(directory).and_return(true) - expect { subject }.to raise_error(/File exists/) + expect { subject }.to raise_error(/Directory exists/) end it 'instruments the pending migration' do @@ -344,7 +346,7 @@ RSpec.describe 'gitlab:db namespace rake task' do it 'writes observations out to JSON file' do subject - expect(buffer.string).to eq(observations.to_json) + expect(File.read(File.join(directory, filename))).to eq(observations.to_json) end end diff --git a/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb index 640f463b45d..d46ba86a22e 100644 --- a/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb +++ b/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe 'layouts/nav/sidebar/_group' do - let(:group) { create(:group) } + let_it_be(:group) { create(:group) } before do assign(:group, group) @@ -11,4 +11,36 @@ RSpec.describe 'layouts/nav/sidebar/_group' do it_behaves_like 'has nav sidebar' it_behaves_like 'sidebar includes snowplow attributes', 'render', 'groups_side_navigation', 'groups_side_navigation' + + describe 'Group information' do + it 'has a link to the group path' do + render + + expect(rendered).to have_link('Group information', href: group_path(group)) + end + + it 'does not have a link to the details menu item' do + render + + expect(rendered).not_to have_link('Details', href: details_group_path(group)) + end + + context 'when feature flag :sidebar_refactor is disabled' do + before do + stub_feature_flags(sidebar_refactor: false) + end + + it 'has a link to the group path with the "Group overview" title' do + render + + expect(rendered).to have_link('Group overview', href: group_path(group)) + end + + it 'has a link to the details menu item' do + render + + expect(rendered).to have_link('Details', href: details_group_path(group)) + end + end + end end diff --git a/yarn.lock b/yarn.lock index 31eca04db6f..e510df1873c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -907,10 +907,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8" integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw== -"@gitlab/ui@29.13.0": - version "29.13.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-29.13.0.tgz#6e222106a0ae14f56c361b0cc86152d09170cc09" - integrity sha512-JZAIuYT9gUhv/My/+IVwbBacTJAL+9g7wZWfSl9DS8PY/H2GCGgMcgvcSJMDuqcJZvKZdNkQ0XzXem+SFo5t1A== +"@gitlab/ui@29.14.0": + version "29.14.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-29.14.0.tgz#0b5dc564fa26194ddbea6fe78418dc46c0e557ac" + integrity sha512-SYRokscvZD/F0TFa2gc0CgBtLeBlv4mPDhGPQUvh6uaX68NgMx9CstfYb286j5dKlvqBw+7r83fMiAHEzpberw== dependencies: "@babel/standalone" "^7.0.0" "@gitlab/vue-toasted" "^1.3.0"