@@ -2978,6 +2980,8 @@ To prepare for this change, users on GitLab.com or self-managed GitLab 15.9 or l
In 16.3, the names of these settings were changed to clarify their meanings: the deprecated **Limit CI_JOB_TOKEN access** setting is now called **Limit access _from_ this project**, and the newer **Allow access to this project with a CI_JOB_TOKEN** setting is now called **Limit access _to_ this project**.
+In 17.1, the name of the **Limit access _to_ this project** setting was further clarified: it is now called **Grant access to this project**.
+
diff --git a/doc/user/project/repository/code_suggestions/index.md b/doc/user/project/repository/code_suggestions/index.md
index 95399fee492..0ce1535c035 100644
--- a/doc/user/project/repository/code_suggestions/index.md
+++ b/doc/user/project/repository/code_suggestions/index.md
@@ -2,6 +2,7 @@
stage: Create
group: Code Creation
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
+description: "Code Suggestions helps you write code in GitLab more efficiently by using AI to suggest code as you type."
---
# Code Suggestions
diff --git a/doc/user/project/repository/code_suggestions/repository_xray.md b/doc/user/project/repository/code_suggestions/repository_xray.md
index 829ff378ace..f764e723817 100644
--- a/doc/user/project/repository/code_suggestions/repository_xray.md
+++ b/doc/user/project/repository/code_suggestions/repository_xray.md
@@ -2,6 +2,7 @@
stage: Create
group: Code Creation
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
+description: "Repository X-Ray gives Code Suggestions more insight into your project's codebase and dependencies."
---
# Repository X-Ray
diff --git a/doc/user/project/repository/code_suggestions/supported_extensions.md b/doc/user/project/repository/code_suggestions/supported_extensions.md
index 23ba2bad55d..520af0746cf 100644
--- a/doc/user/project/repository/code_suggestions/supported_extensions.md
+++ b/doc/user/project/repository/code_suggestions/supported_extensions.md
@@ -2,6 +2,7 @@
stage: Create
group: Code Creation
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
+description: "Code Suggestions supports multiple editors and languages."
---
# Supported extensions and languages
diff --git a/doc/user/project/repository/code_suggestions/troubleshooting.md b/doc/user/project/repository/code_suggestions/troubleshooting.md
index e5c6291bf76..515ca3fe3d7 100644
--- a/doc/user/project/repository/code_suggestions/troubleshooting.md
+++ b/doc/user/project/repository/code_suggestions/troubleshooting.md
@@ -2,6 +2,7 @@
stage: Create
group: Code Creation
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
+description: "Troubleshooting tips for common problems in Code Suggestions."
---
# Troubleshooting Code Suggestions
diff --git a/gems/gitlab-utils/spec/gitlab/utils/system_spec.rb b/gems/gitlab-utils/spec/gitlab/utils/system_spec.rb
index 6d4f3bf039c..74da4fa3571 100644
--- a/gems/gitlab-utils/spec/gitlab/utils/system_spec.rb
+++ b/gems/gitlab-utils/spec/gitlab/utils/system_spec.rb
@@ -227,13 +227,13 @@ RSpec.describe Gitlab::Utils::System do
describe '.summary' do
it 'contains a selection of the available fields' do
- stub_const('RUBY_DESCRIPTION', 'ruby-3.0-patch1')
+ stub_const('RUBY_DESCRIPTION', 'ruby-3.2-patch1')
mock_existing_proc_file('/proc/self/status', proc_status)
mock_existing_proc_file('/proc/self/smaps_rollup', proc_smaps_rollup)
summary = described_class.summary
- expect(summary[:version]).to eq('ruby-3.0-patch1')
+ expect(summary[:version]).to eq('ruby-3.2-patch1')
expect(summary[:gc_stat].keys).to eq(GC.stat.keys)
expect(summary[:memory_rss]).to eq(2527232)
expect(summary[:memory_uss]).to eq(475136)
diff --git a/lib/gitlab/background_migration/backfill_remote_development_agent_configs_project_id.rb b/lib/gitlab/background_migration/backfill_remote_development_agent_configs_project_id.rb
new file mode 100644
index 00000000000..33f493fab47
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_remote_development_agent_configs_project_id.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # rubocop: disable Migration/BackgroundMigrationBaseClass -- BackfillDesiredShardingKeyJob inherits from BatchedMigrationJob.
+ class BackfillRemoteDevelopmentAgentConfigsProjectId < BackfillDesiredShardingKeyJob
+ operation_name :backfill_remote_development_agent_configs_project_id
+ feature_category :remote_development
+ end
+ # rubocop: enable Migration/BackgroundMigrationBaseClass
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d5409593aff..2fb897c187b 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -9925,7 +9925,10 @@ msgstr ""
msgid "CICD|Add an existing project to the scope"
msgstr ""
-msgid "CICD|Allow access to this project from authorized groups or projects by adding them to the allowlist. It is a security risk to disable this feature, because unauthorized projects might attempt to retrieve an active token and access the API. %{linkStart}Learn more%{linkEnd}."
+msgid "CICD|Allow CI/CD job token access"
+msgstr ""
+
+msgid "CICD|Authorized groups and projects"
msgstr ""
msgid "CICD|Auto DevOps"
@@ -9958,7 +9961,7 @@ msgstr ""
msgid "CICD|Enable feature to limit job token access to the following projects."
msgstr ""
-msgid "CICD|Groups and projects with access"
+msgid "CICD|Ensure only groups and projects with members authorized to access sensitive project data are added to the allowlist."
msgstr ""
msgid "CICD|Jobs"
@@ -9970,9 +9973,6 @@ msgstr ""
msgid "CICD|Limit access %{italicStart}from%{italicEnd} this project (Deprecated)"
msgstr ""
-msgid "CICD|Limit access %{italicStart}to%{italicEnd} this project"
-msgstr ""
-
msgid "CICD|Maintainer"
msgstr ""
@@ -9985,7 +9985,7 @@ msgstr ""
msgid "CICD|Prevent CI/CD job tokens from this project from being used to access other projects unless the other project is added to the allowlist. It is a security risk to disable this feature, because unauthorized projects might attempt to retrieve an active token and access the API. %{linkStart}Learn more%{linkEnd}."
msgstr ""
-msgid "CICD|The %{boldStart}Limit access %{boldEnd}%{italicAndBoldStart}from%{italicAndBoldEnd}%{boldStart} this project%{boldEnd} setting is deprecated and will be removed in the 18.0 milestone. Use the %{boldStart}Limit access %{boldEnd}%{italicAndBoldStart}to%{italicAndBoldEnd}%{boldStart} this project%{boldEnd} setting and allowlist instead. %{linkStart}How do I do this?%{linkEnd}"
+msgid "CICD|The %{boldStart}Limit access %{boldEnd}%{italicAndBoldStart}from%{italicAndBoldEnd}%{boldStart} this project%{boldEnd} setting is deprecated and will be removed in the 18.0 milestone. Use the %{boldStart}Allow CI/CD job token access%{boldEnd} setting and allowlist instead. %{linkStart}How do I do this?%{linkEnd}"
msgstr ""
msgid "CICD|The Auto DevOps pipeline runs by default in all projects with no CI/CD configuration file. %{link_start}What is Auto DevOps?%{link_end}"
@@ -10003,6 +10003,9 @@ msgstr ""
msgid "CICD|Use separate caches for protected branches"
msgstr ""
+msgid "CICD|When enabled, groups and projects listed in the allowlist are authorized to use a CI/CD job token to authenticate requests to this project. %{linkStart}Learn more%{linkEnd}."
+msgstr ""
+
msgid "CICD|group enabled"
msgstr ""
@@ -12583,6 +12586,12 @@ msgstr ""
msgid "Code block"
msgstr ""
+msgid "Code completion test failed: %{error}"
+msgstr ""
+
+msgid "Code completion test was successful"
+msgstr ""
+
msgid "Code coverage statistics for %{ref} %{start_date} - %{end_date}"
msgstr ""
@@ -14734,7 +14743,7 @@ msgstr ""
msgid "Contributor analytics"
msgstr ""
-msgid "Control how the CI_JOB_TOKEN CI/CD variable is used for API access between projects."
+msgid "Control whether CI/CD job tokens can be used to authenticate with this project."
msgstr ""
msgid "Control whether to display customer experience improvement content and third-party offers in GitLab."
@@ -29453,6 +29462,9 @@ msgstr ""
msgid "Job logs and artifacts"
msgstr ""
+msgid "Job token permissions"
+msgstr ""
+
msgid "Job was retried"
msgstr ""
@@ -54861,9 +54873,6 @@ msgstr ""
msgid "Token"
msgstr ""
-msgid "Token Access"
-msgstr ""
-
msgid "Token name"
msgstr ""
diff --git a/scripts/frontend/lib/tailwind_migration.mjs b/scripts/frontend/lib/tailwind_migration.mjs
index b6ac5a68778..9e97130e6bd 100644
--- a/scripts/frontend/lib/tailwind_migration.mjs
+++ b/scripts/frontend/lib/tailwind_migration.mjs
@@ -46,6 +46,13 @@ export const mismatchAllowList = [
'.border-l\\!',
'.border-r\\!',
'.border-t\\!',
+ // Tailwind's line-clamp utils don't set `white-space: normal`, while our custom utils did.
+ // We have added `gl-whitespace-normal` wherever line-clamp utils were being used, so these
+ // mismatches can be ignored.
+ '.line-clamp-1',
+ '.line-clamp-2',
+ '.line-clamp-3',
+
];
export function loadCSSFromFile(filePath) {
diff --git a/scripts/pipeline/pre_merge_checks.rb b/scripts/pipeline/pre_merge_checks.rb
new file mode 100755
index 00000000000..1b65a8f43f3
--- /dev/null
+++ b/scripts/pipeline/pre_merge_checks.rb
@@ -0,0 +1,126 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require 'time'
+require 'gitlab' unless Object.const_defined?(:Gitlab)
+
+class PreMergeChecks
+ DEFAULT_API_ENDPOINT = "https://gitlab.com/api/v4"
+ TIER_IDENTIFIER_REGEX = /tier:\d/
+ REQUIRED_TIER_IDENTIFIER = 'tier:3'
+ PREDICTIVE_PIPELINE_IDENTIFIER = 'predictive'
+ PIPELINE_FRESHNESS_THRESHOLD_IN_HOURS = 4
+
+ def initialize(
+ api_endpoint: ENV.fetch('CI_API_V4_URL', DEFAULT_API_ENDPOINT),
+ project_id: ENV['CI_PROJECT_ID'],
+ merge_request_iid: ENV['CI_MERGE_REQUEST_IID'],
+ current_pipeline_id: ENV['CI_PIPELINE_ID'])
+ @api_endpoint = api_endpoint
+ @project_id = project_id
+ @merge_request_iid = merge_request_iid.to_i
+ @current_pipeline_id = current_pipeline_id.to_i
+
+ check_required_ids!
+ end
+
+ def execute
+ latest_pipeline_id = api_client.merge_request_pipelines(project_id, merge_request_iid).auto_paginate do |pipeline|
+ # Skip the current merge train pipeline, as we want the latest merge request pipeline
+ next if pipeline.id == current_pipeline_id
+
+ break pipeline.id
+ end
+ raise "Expected to have a latest pipeline but got none!" unless latest_pipeline_id
+
+ latest_pipeline = api_client.pipeline(project_id, latest_pipeline_id)
+
+ check_pipeline_for_merged_results!(latest_pipeline)
+ check_pipeline_freshness!(latest_pipeline)
+ check_pipeline_identifier!(latest_pipeline)
+
+ puts "All good for merge! 🚀"
+ end
+
+ private
+
+ attr_reader :api_endpoint, :project_id, :merge_request_iid, :current_pipeline_id
+
+ def api_client
+ @api_client ||= begin
+ GitLab.configure do |config|
+ config.endpoint = api_endpoint
+ config.private_token = ENV.fetch('GITLAB_API_PRIVATE_TOKEN', '')
+ end
+ GitLab.client
+ end
+ end
+
+ def check_required_ids!
+ raise 'Missing project_id' unless project_id
+ raise 'Missing merge_request_iid' if merge_request_iid == 0
+ raise 'Missing current_pipeline_id' if current_pipeline_id == 0
+ end
+
+ def check_pipeline_for_merged_results!(pipeline)
+ return if pipeline.ref == "refs/merge-requests/#{merge_request_iid}/merge"
+
+ raise "Expected to have a Merged Results pipeline but got #{pipeline.ref}!"
+ end
+
+ def check_pipeline_freshness!(pipeline)
+ hours_ago = ((Time.now - Time.parse(pipeline.created_at)) / 3600).ceil(2)
+ return if hours_ago < PIPELINE_FRESHNESS_THRESHOLD_IN_HOURS
+
+ raise "Expected latest pipeline to be created within the last 4 hours (it was created #{hours_ago} hours ago)!"
+ end
+
+ def check_pipeline_identifier!(pipeline)
+ if pipeline.name.match?(TIER_IDENTIFIER_REGEX)
+ raise <<~MSG unless pipeline.name.include?(REQUIRED_TIER_IDENTIFIER)
+ Expected latest pipeline to be a tier-3 pipeline!
+
+ Please ensure the MR has all the required approvals, start a new pipeline and put the MR back on the Merge Train.
+ MSG
+ elsif pipeline.name.include?(PREDICTIVE_PIPELINE_IDENTIFIER)
+ raise <<~MSG
+ Expected latest pipeline not to be a predictive pipeline!
+
+ Please ensure the MR has all the required approvals, start a new pipeline and put the MR back on the Merge Train.
+ MSG
+ end
+ end
+end
+
+if $PROGRAM_NAME == __FILE__
+ require 'optparse'
+ options = {}
+
+ OptionParser.new do |opts|
+ opts.on("-p", "--project_id [string]", String, "Project ID") do |value|
+ options[:project_id] = value
+ end
+
+ opts.on("-m", "--merge_request_iid [string]", String, "Merge request IID") do |value|
+ options[:merge_request_iid] = value
+ end
+
+ opts.on("-c", "--current_pipeline_id [string]", String, "Current pipeline ID") do |value|
+ options[:current_pipeline_id] = value
+ end
+
+ opts.on("-h", "--help") do
+ puts "Usage: merge-train-checks.rb [--project_id
] [--merge_request_iid ] " \
+ "[--current_pipeline_id ]"
+ puts
+ puts "Examples:"
+ puts
+ puts "merge-train-checks.rb --project_id \"gitlab-org/gitlab\" --merge_request_iid \"1\" " \
+ "--current_pipeline_id \"2\""
+
+ exit
+ end
+ end.parse!
+
+ PreMergeChecks.new(**options).execute
+end
diff --git a/scripts/process_custom_semgrep_results.rb b/scripts/process_custom_semgrep_results.rb
index 8103d9ce5db..c0b108a3ecb 100755
--- a/scripts/process_custom_semgrep_results.rb
+++ b/scripts/process_custom_semgrep_results.rb
@@ -11,7 +11,7 @@ ALLOWED_API_URLS = %w[https://gitlab.com/api/v4].freeze
# Remove this when the feature is fully working
MESSAGE_FOOTER = <<-FOOTER
-
+\n
This AppSec automation is currently under testing.
Use ~"appsec-sast::helpful" or ~"appsec-sast::unhelpful" for quick feedback.
@@ -125,7 +125,7 @@ def remove_duplicate_findings(path_line_message_dict)
puts "existing comment from BOT: #{comment}"
existing_path = comment['position']['new_path']
existing_line = comment['position']['new_line']
- existing_message = comment['body']
+ existing_message = comment['body'].gsub(MESSAGE_FOOTER, '')
puts "Found existing comment in file #{existing_path} for line #{existing_line}" if path_line_message_dict[existing_path].include?({ line: existing_line,
message: existing_message })
@@ -196,8 +196,8 @@ def create_inline_comments(path_line_message_dict)
next if response.instance_of?(Net::HTTPCreated)
puts "Failed to post inline comment with status code #{response.code}: #{response.body}"
- post_comment "SAST finding at line #{info[:line]} in file #{path}: #{info[:message]}. " \
- "\n\n Ping `@gitlab-com/gl-security/product-security/appsec` if you need assistance regarding this finding." + MESSAGE_FOOTER
+ post_comment "SAST finding at line #{new_line} in file #{path}: #{message}." \
+ "\n Ping `@gitlab-com/gl-security/product-security/appsec` if you need assistance regarding this finding" + MESSAGE_FOOTER
exit 0
end
diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb
index 16510c618ef..e4f3cd5efe3 100644
--- a/spec/controllers/groups/clusters_controller_spec.rb
+++ b/spec/controllers/groups/clusters_controller_spec.rb
@@ -7,8 +7,7 @@ RSpec.describe Groups::ClustersController, feature_category: :deployment_managem
include GoogleApi::CloudPlatformHelpers
let_it_be(:group) { create(:group) }
-
- let(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
before do
group.add_maintainer(user)
@@ -22,13 +21,11 @@ RSpec.describe Groups::ClustersController, feature_category: :deployment_managem
describe 'functionality' do
context 'when group has one or more clusters' do
- let(:group) { create(:group) }
-
- let!(:enabled_cluster) do
+ let_it_be(:enabled_cluster) do
create(:cluster, :provided_by_gcp, cluster_type: :group_type, groups: [group])
end
- let!(:disabled_cluster) do
+ let_it_be(:disabled_cluster) do
create(:cluster, :disabled, :provided_by_gcp, :production_environment, cluster_type: :group_type, groups: [group])
end
@@ -89,8 +86,6 @@ RSpec.describe Groups::ClustersController, feature_category: :deployment_managem
end
context 'when group does not have a cluster' do
- let(:group) { create(:group) }
-
it 'returns an empty state page' do
go
@@ -102,7 +97,7 @@ RSpec.describe Groups::ClustersController, feature_category: :deployment_managem
end
describe 'security' do
- let(:cluster) { create(:cluster, :provided_by_gcp, cluster_type: :group_type, groups: [group]) }
+ let_it_be(:cluster) { create(:cluster, :provided_by_gcp, cluster_type: :group_type, groups: [group]) }
it('is allowed for admin when admin mode is enabled', :enable_admin_mode) { expect { go }.to be_allowed_for(:admin) }
it('is denied for admin when admin mode is disabled') { expect { go }.to be_denied_for(:admin) }
@@ -220,13 +215,8 @@ RSpec.describe Groups::ClustersController, feature_category: :deployment_managem
end
describe 'DELETE clear cluster cache' do
- let(:cluster) { create(:cluster, :group, groups: [group]) }
- let!(:kubernetes_namespace) do
- create(:cluster_kubernetes_namespace,
- cluster: cluster,
- project: create(:project)
- )
- end
+ let_it_be(:cluster) { create(:cluster, :group, groups: [group]) }
+ let_it_be(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, cluster: cluster, project: create(:project)) }
def go
delete :clear_cache,
@@ -441,7 +431,7 @@ RSpec.describe Groups::ClustersController, feature_category: :deployment_managem
end
describe 'DELETE destroy' do
- let!(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, cluster_type: :group_type, groups: [group]) }
+ let_it_be(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, cluster_type: :group_type, groups: [group]) }
def go
delete :destroy,
@@ -470,7 +460,7 @@ RSpec.describe Groups::ClustersController, feature_category: :deployment_managem
end
context 'when cluster is being created' do
- let!(:cluster) { create(:cluster, :providing_by_gcp, :production_environment, cluster_type: :group_type, groups: [group]) }
+ let_it_be(:cluster) { create(:cluster, :providing_by_gcp, :production_environment, cluster_type: :group_type, groups: [group]) }
it 'destroys and redirects back to clusters list' do
expect { go }
@@ -484,7 +474,7 @@ RSpec.describe Groups::ClustersController, feature_category: :deployment_managem
end
context 'when cluster is provided by user' do
- let!(:cluster) { create(:cluster, :provided_by_user, :production_environment, cluster_type: :group_type, groups: [group]) }
+ let_it_be(:cluster) { create(:cluster, :provided_by_user, :production_environment, cluster_type: :group_type, groups: [group]) }
it 'destroys and redirects back to clusters list' do
expect { go }
diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb
index 91ed3dd67e6..3dd79041f1b 100644
--- a/spec/controllers/groups/milestones_controller_spec.rb
+++ b/spec/controllers/groups/milestones_controller_spec.rb
@@ -3,12 +3,12 @@
require 'spec_helper'
RSpec.describe Groups::MilestonesController, feature_category: :team_planning do
- let(:group) { create(:group, :public) }
- let!(:project) { create(:project, :public, group: group) }
- let!(:project2) { create(:project, group: group) }
- let(:user) { create(:user) }
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:project) { create(:project, :public, group: group) }
+ let_it_be(:project2) { create(:project, group: group) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:milestone) { create(:milestone, project: project) }
let(:title) { '肯定不是中文的问题' }
- let(:milestone) { create(:milestone, project: project) }
let(:milestone_params) do
{
@@ -86,13 +86,13 @@ RSpec.describe Groups::MilestonesController, feature_category: :team_planning do
end
end
- let!(:public_group) { create(:group, :public) }
+ let_it_be(:public_group) { create(:group, :public) }
- let!(:public_project_with_private_issues_and_mrs) do
+ let_it_be(:public_project_with_private_issues_and_mrs) do
create(:project, :public, :issues_private, :merge_requests_private, group: public_group)
end
- let!(:private_milestone) { create(:milestone, project: public_project_with_private_issues_and_mrs, title: 'project milestone') }
+ let_it_be(:private_milestone) { create(:milestone, project: public_project_with_private_issues_and_mrs, title: 'project milestone') }
context 'when anonymous user' do
before do
@@ -165,17 +165,19 @@ RSpec.describe Groups::MilestonesController, feature_category: :team_planning do
end
context 'as JSON' do
- let!(:milestone) { create(:milestone, group: group, title: 'group milestone') }
- let!(:project_milestone1) { create(:milestone, project: project, title: 'same name') }
- let!(:project_milestone2) { create(:milestone, project: project2, title: 'same name') }
+ before do
+ create(:milestone, group: group, title: 'group milestone')
+ create(:milestone, project: project, title: 'same name')
+ create(:milestone, project: project2, title: 'same name')
+ end
it 'lists project and group milestones' do
get :index, params: { group_id: group.to_param }, format: :json
milestones = json_response
- expect(milestones.count).to eq(3)
- expect(milestones.collect { |m| m['title'] }).to match_array(['same name', 'same name', 'group milestone'])
+ expect(milestones.count).to eq(4)
+ expect(milestones.collect { |m| m['title'] }).to match_array([milestone.name, 'same name', 'same name', 'group milestone'])
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq 'application/json'
end
@@ -189,8 +191,8 @@ RSpec.describe Groups::MilestonesController, feature_category: :team_planning do
milestones = json_response
milestone_titles = milestones.map { |m| m['title'] }
- expect(milestones.count).to eq(4)
- expect(milestone_titles).to match_array(['same name', 'same name', 'group milestone', 'subgroup milestone'])
+ expect(milestones.count).to eq(5)
+ expect(milestone_titles).to match_array([milestone.name, 'same name', 'same name', 'group milestone', 'subgroup milestone'])
end
end
@@ -245,7 +247,7 @@ RSpec.describe Groups::MilestonesController, feature_category: :team_planning do
end
describe "#update" do
- let(:milestone) { create(:milestone, group: group) }
+ let_it_be_with_reload(:milestone) { create(:milestone, group: group) }
subject do
put :update, params: {
diff --git a/spec/controllers/groups/settings/applications_controller_spec.rb b/spec/controllers/groups/settings/applications_controller_spec.rb
index aa50ef9a92c..99acc26a0c8 100644
--- a/spec/controllers/groups/settings/applications_controller_spec.rb
+++ b/spec/controllers/groups/settings/applications_controller_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Groups::Settings::ApplicationsController do
let_it_be(:user) { create(:user) }
+ let_it_be(:admin) { create(:user, :admin) }
let_it_be(:group) { create(:group) }
let_it_be(:application) { create(:oauth_application, owner_id: group.id, owner_type: 'Namespace') }
@@ -25,7 +26,7 @@ RSpec.describe Groups::Settings::ApplicationsController do
end
context 'when admin mode is enabled' do
- let!(:user) { create(:user, :admin) }
+ let(:user) { admin }
before do
Gitlab::Session.with_session(controller.session) do
@@ -56,7 +57,7 @@ RSpec.describe Groups::Settings::ApplicationsController do
end
context "when admin mode is enabled for the admin user who is a #{role} of a group" do
- let!(:user) { create(:user, :admin) }
+ let(:user) { admin }
before do
Gitlab::Session.with_session(controller.session) do
@@ -90,7 +91,7 @@ RSpec.describe Groups::Settings::ApplicationsController do
end
context 'when admin mode is enabled' do
- let!(:user) { create(:user, :admin) }
+ let(:user) { admin }
before do
Gitlab::Session.with_session(controller.session) do
@@ -121,7 +122,7 @@ RSpec.describe Groups::Settings::ApplicationsController do
end
context "when admin mode is enabled for the admin user who is a #{role} of a group" do
- let!(:user) { create(:user, :admin) }
+ let(:user) { admin }
before do
Gitlab::Session.with_session(controller.session) do
@@ -199,7 +200,7 @@ RSpec.describe Groups::Settings::ApplicationsController do
end
context 'when admin mode is enabled' do
- let!(:user) { create(:user, :admin) }
+ let(:user) { admin }
before do
Gitlab::Session.with_session(controller.session) do
@@ -239,7 +240,7 @@ RSpec.describe Groups::Settings::ApplicationsController do
end
context "when admin mode is enabled for the admin user who is a #{role} of a group" do
- let!(:user) { create(:user, :admin) }
+ let(:user) { admin }
before do
Gitlab::Session.with_session(controller.session) do
@@ -291,7 +292,7 @@ RSpec.describe Groups::Settings::ApplicationsController do
end
context 'when admin mode is enabled' do
- let!(:user) { create(:user, :admin) }
+ let(:user) { admin }
before do
Gitlab::Session.with_session(controller.session) do
@@ -342,7 +343,7 @@ RSpec.describe Groups::Settings::ApplicationsController do
end
context "when admin mode is enabled for the admin user who is a #{role} of a group" do
- let!(:user) { create(:user, :admin) }
+ let(:user) { admin }
before do
Gitlab::Session.with_session(controller.session) do
@@ -401,7 +402,7 @@ RSpec.describe Groups::Settings::ApplicationsController do
end
context 'when admin mode is enabled' do
- let!(:user) { create(:user, :admin) }
+ let(:user) { admin }
before do
Gitlab::Session.with_session(controller.session) do
@@ -439,7 +440,7 @@ RSpec.describe Groups::Settings::ApplicationsController do
end
context "when admin mode is enabled for the admin user who is a #{role} of a group" do
- let!(:user) { create(:user, :admin) }
+ let(:user) { admin }
before do
Gitlab::Session.with_session(controller.session) do
@@ -478,7 +479,7 @@ RSpec.describe Groups::Settings::ApplicationsController do
end
context 'when admin mode is enabled' do
- let!(:user) { create(:user, :admin) }
+ let(:user) { admin }
before do
Gitlab::Session.with_session(controller.session) do
@@ -509,7 +510,7 @@ RSpec.describe Groups::Settings::ApplicationsController do
end
context "when admin mode is enabled for the admin user who is a #{role} of a group" do
- let!(:user) { create(:user, :admin) }
+ let(:user) { admin }
before do
Gitlab::Session.with_session(controller.session) do
diff --git a/spec/features/projects/members/tabs_spec.rb b/spec/features/projects/members/tabs_spec.rb
index a48bca532fb..55f07f47f54 100644
--- a/spec/features/projects/members/tabs_spec.rb
+++ b/spec/features/projects/members/tabs_spec.rb
@@ -30,7 +30,6 @@ RSpec.describe 'Projects > Members > Tabs', :js, feature_category: :groups_and_p
'Members' | 3
'Pending invitations' | 2
'Groups' | 2
- 'Access requests' | 2
end
with_them do
@@ -39,6 +38,10 @@ RSpec.describe 'Projects > Members > Tabs', :js, feature_category: :groups_and_p
end
end
+ it "renders Access requests tab", quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/448572' do
+ expect(page).to have_selector('.nav-link', text: "Access requests 2")
+ end
+
context 'displays "Members" tab by default' do
it_behaves_like 'active "Members" tab'
end
diff --git a/spec/features/projects/releases/user_views_releases_spec.rb b/spec/features/projects/releases/user_views_releases_spec.rb
index be37325d9d9..134c19a3c56 100644
--- a/spec/features/projects/releases/user_views_releases_spec.rb
+++ b/spec/features/projects/releases/user_views_releases_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe 'User views releases', :js, feature_category: :continuous_deliver
shared_examples 'when the project does not have releases' do
before do
- project.releases.delete_all
+ project.releases.delete_all(:delete_all)
visit project_releases_path(project)
end
diff --git a/spec/finders/releases_finder_spec.rb b/spec/finders/releases_finder_spec.rb
index 43ad9ef2a12..51b78e44809 100644
--- a/spec/finders/releases_finder_spec.rb
+++ b/spec/finders/releases_finder_spec.rb
@@ -267,7 +267,7 @@ RSpec.describe ReleasesFinder, feature_category: :release_orchestration do
context 'when one project does not have releases' do
it 'returns the latest release of only the project with releases' do
- project.releases.delete_all
+ project.releases.delete_all(:delete_all)
is_expected.to eq([v2_1_0])
end
@@ -275,8 +275,8 @@ RSpec.describe ReleasesFinder, feature_category: :release_orchestration do
context 'when all projects do not have releases' do
it 'returns empty response' do
- project.releases.delete_all
- project2.releases.delete_all
+ project.releases.delete_all(:delete_all)
+ project2.releases.delete_all(:delete_all)
is_expected.to be_empty
end
diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js
index 9afed0e9013..420b3a69eeb 100644
--- a/spec/frontend/boards/components/board_card_spec.js
+++ b/spec/frontend/boards/components/board_card_spec.js
@@ -188,9 +188,8 @@ describe('Board card', () => {
});
});
- describe('when Epic colors are enabled', () => {
+ describe('epic colors', () => {
it('applies the correct color', () => {
- window.gon.features = { epicColorHighlight: true };
mountComponent({
item: {
...mockIssue,
@@ -204,21 +203,4 @@ describe('Board card', () => {
expect(wrapper.attributes('style')).toContain(`border-color: ${DEFAULT_COLOR}`);
});
});
-
- describe('when Epic colors are not enabled', () => {
- it('applies the correct color', () => {
- window.gon.features = { epicColorHighlight: false };
- mountComponent({
- item: {
- ...mockIssue,
- color: DEFAULT_COLOR,
- },
- });
-
- expect(wrapper.classes()).not.toEqual(
- expect.arrayContaining(['gl-pl-4', 'gl-border-l-solid', 'gl-border-4']),
- );
- expect(wrapper.attributes('style')).toBeUndefined();
- });
- });
});
diff --git a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_overview_spec.js b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_overview_spec.js
index a5b7b912744..03c1fd24bb5 100644
--- a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_overview_spec.js
+++ b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_overview_spec.js
@@ -1,13 +1,18 @@
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import { GlEmptyState, GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
import KubernetesOverview from '~/environments/environment_details/components/kubernetes/kubernetes_overview.vue';
import KubernetesStatusBar from '~/environments/environment_details/components/kubernetes/kubernetes_status_bar.vue';
import KubernetesAgentInfo from '~/environments/environment_details/components/kubernetes/kubernetes_agent_info.vue';
import KubernetesTabs from '~/environments/environment_details/components/kubernetes/kubernetes_tabs.vue';
import { k8sResourceType } from '~/environments/graphql/resolvers/kubernetes/constants';
-import { agent, kubernetesNamespace, fluxResourcePathMock } from '../../../graphql/mock_data';
-import { mockKasTunnelUrl } from '../../../mock_data';
+import { agent, kubernetesNamespace } from '../../../graphql/mock_data';
+import { mockKasTunnelUrl, fluxResourceStatus, fluxKustomization } from '../../../mock_data';
+
+Vue.use(VueApollo);
describe('~/environments/environment_details/components/kubernetes/kubernetes_overview.vue', () => {
let wrapper;
@@ -15,7 +20,6 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_ov
const defaultProps = {
environmentName: 'production',
kubernetesNamespace,
- fluxResourcePath: fluxResourcePathMock,
};
const provide = {
@@ -32,13 +36,36 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_ov
credentials: 'include',
};
- const createWrapper = (clusterAgent = agent) => {
+ const kustomizationResourcePath =
+ 'kustomize.toolkit.fluxcd.io/v1/namespaces/my-namespace/kustomizations/app';
+
+ const fluxKustomizationQuery = jest.fn().mockReturnValue({});
+ const fluxHelmReleaseStatusQuery = jest.fn().mockReturnValue({});
+
+ const createApolloProvider = () => {
+ const mockResolvers = {
+ Query: {
+ fluxKustomization: fluxKustomizationQuery,
+ fluxHelmReleaseStatus: fluxHelmReleaseStatusQuery,
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ const createWrapper = ({
+ clusterAgent = agent,
+ fluxResourcePath = kustomizationResourcePath,
+ apolloProvider = createApolloProvider(),
+ } = {}) => {
return shallowMount(KubernetesOverview, {
provide,
propsData: {
...defaultProps,
clusterAgent,
+ fluxResourcePath,
},
+ apolloProvider,
});
};
@@ -46,38 +73,46 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_ov
const findKubernetesStatusBar = () => wrapper.findComponent(KubernetesStatusBar);
const findKubernetesTabs = () => wrapper.findComponent(KubernetesTabs);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
-
const findAlert = () => wrapper.findComponent(GlAlert);
describe('when the agent data is present', () => {
- beforeEach(() => {
- wrapper = createWrapper();
- });
-
it('renders kubernetes agent info', () => {
+ wrapper = createWrapper();
+
expect(findAgentInfo().props('clusterAgent')).toEqual(agent);
});
it('renders kubernetes tabs', () => {
- expect(findKubernetesTabs().props()).toEqual({
+ wrapper = createWrapper();
+
+ expect(findKubernetesTabs().props()).toMatchObject({
namespace: kubernetesNamespace,
configuration,
value: k8sResourceType.k8sPods,
+ fluxKustomization: {},
});
});
it('renders kubernetes status bar', () => {
+ wrapper = createWrapper();
+
expect(findKubernetesStatusBar().props()).toEqual({
clusterHealthStatus: 'success',
configuration,
environmentName: defaultProps.environmentName,
- fluxResourcePath: fluxResourcePathMock,
+ fluxResourcePath: kustomizationResourcePath,
namespace: kubernetesNamespace,
resourceType: k8sResourceType.k8sPods,
+ fluxApiError: '',
+ fluxResourceStatus: [],
});
});
describe('Kubernetes health status', () => {
+ beforeEach(() => {
+ wrapper = createWrapper();
+ });
+
it("doesn't set `clusterHealthStatus` when pods are still loading", async () => {
findKubernetesTabs().vm.$emit('loading', true);
await nextTick();
@@ -107,6 +142,132 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_ov
});
});
+ describe('Flux resource', () => {
+ describe('when no flux resource path is provided', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({ fluxResourcePath: '' });
+ });
+
+ it("doesn't request Kustomizations and HelmReleases", () => {
+ expect(fluxKustomizationQuery).not.toHaveBeenCalled();
+ expect(fluxHelmReleaseStatusQuery).not.toHaveBeenCalled();
+ });
+
+ it('provides empty `fluxResourceStatus` to KubernetesStatusBar', () => {
+ expect(findKubernetesStatusBar().props('fluxResourceStatus')).toEqual([]);
+ });
+
+ it('provides empty `fluxKustomization` to KubernetesTabs', () => {
+ expect(findKubernetesTabs().props('fluxKustomization')).toEqual({});
+ });
+ });
+
+ describe('when flux resource path is provided', () => {
+ describe('if the provided resource is a Kustomization', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({ fluxResourcePath: kustomizationResourcePath });
+ });
+
+ it('requests the Kustomization resource status', () => {
+ expect(fluxKustomizationQuery).toHaveBeenCalledWith(
+ {},
+ expect.objectContaining({
+ configuration,
+ fluxResourcePath: kustomizationResourcePath,
+ }),
+ expect.any(Object),
+ expect.any(Object),
+ );
+ });
+
+ it("doesn't request HelmRelease resource status", () => {
+ expect(fluxHelmReleaseStatusQuery).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('if the provided resource is a helmRelease', () => {
+ const helmResourcePath =
+ 'helm.toolkit.fluxcd.io/v2beta1/namespaces/my-namespace/helmreleases/app';
+
+ beforeEach(() => {
+ createWrapper({ fluxResourcePath: helmResourcePath });
+ });
+
+ it('requests the HelmRelease resource status', () => {
+ expect(fluxHelmReleaseStatusQuery).toHaveBeenCalledWith(
+ {},
+ expect.objectContaining({
+ configuration,
+ fluxResourcePath: helmResourcePath,
+ }),
+ expect.any(Object),
+ expect.any(Object),
+ );
+ });
+
+ it("doesn't request Kustomization resource status", () => {
+ expect(fluxKustomizationQuery).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('with Flux Kustomizations available', () => {
+ const createApolloProviderWithKustomizations = () => {
+ const mockResolvers = {
+ Query: {
+ fluxKustomization: jest.fn().mockReturnValue(fluxKustomization),
+ fluxHelmReleaseStatus: fluxHelmReleaseStatusQuery,
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ beforeEach(async () => {
+ wrapper = createWrapper({
+ apolloProvider: createApolloProviderWithKustomizations(),
+ });
+ await waitForPromises();
+ });
+ it('provides correct `fluxResourceStatus` to KubernetesStatusBar', () => {
+ expect(findKubernetesStatusBar().props('fluxResourceStatus')).toEqual(
+ fluxResourceStatus,
+ );
+ });
+
+ it('provides correct `fluxKustomization` to KubernetesTabs', () => {
+ expect(findKubernetesTabs().props('fluxKustomization')).toEqual(fluxKustomization);
+ });
+ });
+
+ describe('when Flux API errored', () => {
+ const error = new Error('Error from the cluster_client API');
+ const createApolloProviderWithErrors = () => {
+ const mockResolvers = {
+ Query: {
+ fluxKustomization: jest.fn().mockRejectedValueOnce(error),
+ fluxHelmReleaseStatus: jest.fn().mockRejectedValueOnce(error),
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ beforeEach(async () => {
+ wrapper = createWrapper({
+ apolloProvider: createApolloProviderWithErrors(),
+ fluxResourcePath:
+ 'kustomize.toolkit.fluxcd.io/v1/namespaces/my-namespace/kustomizations/app',
+ });
+ await waitForPromises();
+ });
+
+ it('provides api error to KubernetesStatusBar', () => {
+ expect(findKubernetesStatusBar().props('fluxApiError')).toEqual(error.message);
+ });
+ });
+ });
+ });
+
describe('on child component error', () => {
beforeEach(() => {
wrapper = createWrapper();
@@ -128,7 +289,7 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_ov
describe('when there is no cluster agent data', () => {
beforeEach(() => {
- wrapper = createWrapper(null);
+ wrapper = createWrapper({ clusterAgent: null, fluxResourcePath: '' });
});
it('renders empty state component', () => {
diff --git a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_status_bar_spec.js b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_status_bar_spec.js
index cbb31c82411..034fc6c6111 100644
--- a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_status_bar_spec.js
+++ b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_status_bar_spec.js
@@ -1,5 +1,3 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
import { GlLoadingIcon, GlPopover, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import KubernetesStatusBar from '~/environments/environment_details/components/kubernetes/kubernetes_status_bar.vue';
@@ -15,14 +13,10 @@ import {
connectionStatus,
k8sResourceType,
} from '~/environments/graphql/resolvers/kubernetes/constants';
-import waitForPromises from 'helpers/wait_for_promises';
-import createMockApollo from 'helpers/mock_apollo_helper';
import { stubComponent } from 'helpers/stub_component';
import { mockKasTunnelUrl } from '../../../mock_data';
import { kubernetesNamespace } from '../../../graphql/mock_data';
-Vue.use(VueApollo);
-
const configuration = {
basePath: mockKasTunnelUrl.replace(/\/$/, ''),
baseOptions: {
@@ -45,24 +39,11 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_st
const findFluxConnectionStatusBadge = () => wrapper.findByTestId('flux-status-badge');
const findFluxConnectionStatus = () => wrapper.findByTestId('flux-connection-status');
- const fluxKustomizationStatusQuery = jest.fn().mockReturnValue([]);
- const fluxHelmReleaseStatusQuery = jest.fn().mockReturnValue([]);
-
- const createApolloProvider = () => {
- const mockResolvers = {
- Query: {
- fluxKustomizationStatus: fluxKustomizationStatusQuery,
- fluxHelmReleaseStatus: fluxHelmReleaseStatusQuery,
- },
- };
-
- return createMockApollo([], mockResolvers);
- };
-
const createWrapper = ({
- apolloProvider = createApolloProvider(),
clusterHealthStatus = '',
fluxResourcePath = '',
+ fluxResourceStatus = [],
+ fluxApiError = '',
namespace = kubernetesNamespace,
resourceType = k8sResourceType.k8sPods,
connectionStatusValue = connectionStatus.connected,
@@ -75,8 +56,9 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_st
fluxResourcePath,
namespace,
resourceType,
+ fluxResourceStatus,
+ fluxApiError,
},
- apolloProvider,
stubs: {
GlSprintf,
KubernetesConnectionStatus: stubComponent(KubernetesConnectionStatus, {
@@ -180,153 +162,46 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_st
createWrapper();
});
- it("doesn't request Kustomizations and HelmReleases", () => {
- expect(fluxKustomizationStatusQuery).not.toHaveBeenCalled();
- expect(fluxHelmReleaseStatusQuery).not.toHaveBeenCalled();
- });
-
it('renders sync status as Unavailable', () => {
expect(findSyncBadge().text()).toBe('Unavailable');
});
});
- describe('when flux resource path is provided', () => {
- let fluxResourcePath;
+ describe('when flux status data is provided', () => {
+ const message = 'Message from Flux';
- describe('if the provided resource is a Kustomization', () => {
- beforeEach(() => {
- fluxResourcePath = kustomizationResourcePath;
-
- createWrapper({ fluxResourcePath });
- });
-
- it('requests the Kustomization resource status', () => {
- expect(fluxKustomizationStatusQuery).toHaveBeenCalledWith(
- {},
- expect.objectContaining({
- configuration,
- fluxResourcePath,
- }),
- expect.any(Object),
- expect.any(Object),
- );
- });
-
- it("doesn't request HelmRelease resource status", () => {
- expect(fluxHelmReleaseStatusQuery).not.toHaveBeenCalled();
- });
- });
-
- describe('if the provided resource is a helmRelease', () => {
- beforeEach(() => {
- fluxResourcePath =
- 'helm.toolkit.fluxcd.io/v2beta1/namespaces/my-namespace/helmreleases/app';
-
- createWrapper({ fluxResourcePath });
- });
-
- it('requests the HelmRelease resource status', () => {
- expect(fluxHelmReleaseStatusQuery).toHaveBeenCalledWith(
- {},
- expect.objectContaining({
- configuration,
- fluxResourcePath,
- }),
- expect.any(Object),
- expect.any(Object),
- );
- });
-
- it("doesn't request Kustomization resource status", () => {
- expect(fluxKustomizationStatusQuery).not.toHaveBeenCalled();
- });
- });
-
- describe('with Flux Kustomizations available', () => {
- const createApolloProviderWithKustomizations = ({
- result = { status: 'True', type: 'Ready', message: '' },
- } = {}) => {
- const mockResolvers = {
- Query: {
- fluxKustomizationStatus: jest.fn().mockReturnValue([result]),
- fluxHelmReleaseStatus: fluxHelmReleaseStatusQuery,
- },
- };
-
- return createMockApollo([], mockResolvers);
- };
-
- it("doesn't request HelmReleases when the Kustomizations were found", async () => {
+ it.each`
+ status | type | reason | statusText | statusPopover
+ ${'True'} | ${'Stalled'} | ${''} | ${'Stalled'} | ${message}
+ ${'True'} | ${'Reconciling'} | ${''} | ${'Reconciling'} | ${'Flux sync reconciling'}
+ ${'Unknown'} | ${'Ready'} | ${'Progressing'} | ${'Reconciling'} | ${message}
+ ${'True'} | ${'Ready'} | ${''} | ${'Reconciled'} | ${'Flux sync reconciled successfully'}
+ ${'False'} | ${'Ready'} | ${''} | ${'Failed'} | ${message}
+ ${'Unknown'} | ${'Ready'} | ${''} | ${'Unknown'} | ${'Unable to detect state. How are states detected?'}
+ `(
+ 'renders sync status as $statusText when status is $status, type is $type, and reason is $reason',
+ ({ status, type, reason, statusText, statusPopover }) => {
createWrapper({
- apolloProvider: createApolloProviderWithKustomizations(),
- });
- await waitForPromises();
-
- expect(fluxHelmReleaseStatusQuery).not.toHaveBeenCalled();
- });
- });
-
- describe('when receives data from the Flux', () => {
- const createApolloProviderWithKustomizations = (result) => {
- const mockResolvers = {
- Query: {
- fluxKustomizationStatus: jest.fn().mockReturnValue([result]),
- fluxHelmReleaseStatus: fluxHelmReleaseStatusQuery,
- },
- };
-
- return createMockApollo([], mockResolvers);
- };
- const message = 'Message from Flux';
-
- it.each`
- status | type | reason | statusText | statusPopover
- ${'True'} | ${'Stalled'} | ${''} | ${'Stalled'} | ${message}
- ${'True'} | ${'Reconciling'} | ${''} | ${'Reconciling'} | ${'Flux sync reconciling'}
- ${'Unknown'} | ${'Ready'} | ${'Progressing'} | ${'Reconciling'} | ${message}
- ${'True'} | ${'Ready'} | ${''} | ${'Reconciled'} | ${'Flux sync reconciled successfully'}
- ${'False'} | ${'Ready'} | ${''} | ${'Failed'} | ${message}
- ${'Unknown'} | ${'Ready'} | ${''} | ${'Unknown'} | ${'Unable to detect state. How are states detected?'}
- `(
- 'renders sync status as $statusText when status is $status, type is $type, and reason is $reason',
- async ({ status, type, reason, statusText, statusPopover }) => {
- createWrapper({
- fluxResourcePath: kustomizationResourcePath,
- apolloProvider: createApolloProviderWithKustomizations({
+ fluxResourceStatus: [
+ {
status,
type,
reason,
message,
- }),
- });
- await waitForPromises();
+ },
+ ],
+ });
- expect(findSyncBadge().text()).toBe(statusText);
- expect(findPopover().text()).toBe(statusPopover);
- },
- );
- });
+ expect(findSyncBadge().text()).toBe(statusText);
+ expect(findPopover().text()).toBe(statusPopover);
+ },
+ );
describe('when Flux API errored', () => {
- const error = new Error('Error from the cluster_client API');
- const createApolloProviderWithErrors = () => {
- const mockResolvers = {
- Query: {
- fluxKustomizationStatus: jest.fn().mockRejectedValueOnce(error),
- fluxHelmReleaseStatus: jest.fn().mockRejectedValueOnce(error),
- },
- };
+ const fluxApiError = 'Error from the cluster_client API';
- return createMockApollo([], mockResolvers);
- };
-
- beforeEach(async () => {
- createWrapper({
- apolloProvider: createApolloProviderWithErrors(),
- fluxResourcePath:
- 'kustomize.toolkit.fluxcd.io/v1/namespaces/my-namespace/kustomizations/app',
- });
- await waitForPromises();
+ beforeEach(() => {
+ createWrapper({ fluxApiError });
});
it('renders sync badge as unavailable', () => {
@@ -340,7 +215,7 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_st
});
it('renders popover with an API error message', () => {
- expect(findPopover().text()).toBe(error.message);
+ expect(findPopover().text()).toBe(fluxApiError);
expect(findPopover().props('title')).toBe('Flux sync status is unavailable');
});
});
diff --git a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_summary_spec.js b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_summary_spec.js
index 8c6a745a0d3..0d2888d82ab 100644
--- a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_summary_spec.js
+++ b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_summary_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { GlTab } from '@gitlab/ui';
import KubernetesSummary from '~/environments/environment_details/components/kubernetes/kubernetes_summary.vue';
+import { fluxKustomization } from '../../../mock_data';
describe('~/environments/environment_details/components/kubernetes/kubernetes_summary.vue', () => {
let wrapper;
@@ -9,6 +10,9 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_su
const createWrapper = () => {
wrapper = shallowMount(KubernetesSummary, {
+ propsData: {
+ fluxKustomization,
+ },
stubs: { GlTab },
});
};
@@ -23,7 +27,11 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_su
});
it('renders tree view title', () => {
- expect(findTab().text()).toBe('Tree view');
+ expect(findTab().text()).toContain('Tree view');
+ });
+
+ it('renders kustomization resource data', () => {
+ expect(findTab().text()).toContain('Kustomization: my-kustomization');
});
});
});
diff --git a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_tabs_spec.js b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_tabs_spec.js
index db5f3fe07b3..6dc86f12927 100644
--- a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_tabs_spec.js
+++ b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_tabs_spec.js
@@ -7,7 +7,7 @@ import KubernetesServices from '~/environments/environment_details/components/ku
import KubernetesSummary from '~/environments/environment_details/components/kubernetes/kubernetes_summary.vue';
import WorkloadDetails from '~/kubernetes_dashboard/components/workload_details.vue';
import { k8sResourceType } from '~/environments/graphql/resolvers/kubernetes/constants';
-import { mockKasTunnelUrl } from 'jest/environments/mock_data';
+import { mockKasTunnelUrl, fluxKustomization } from 'jest/environments/mock_data';
import { mockPodsTableItems } from 'jest/kubernetes_dashboard/graphql/mock_data';
describe('~/environments/environment_details/components/kubernetes/kubernetes_tabs.vue', () => {
@@ -20,6 +20,7 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_ta
headers: { 'GitLab-Agent-Id': '1' },
},
};
+
const findTabs = () => wrapper.findComponent(GlTabs);
const findKubernetesPods = () => wrapper.findComponent(KubernetesPods);
const findKubernetesServices = () => wrapper.findComponent(KubernetesServices);
@@ -35,15 +36,15 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_ta
provide: {
glFeatures: { k8sTreeView: k8sTreeViewEnabled },
},
- propsData: { configuration, namespace, value: activeTab },
+ propsData: { configuration, namespace, fluxKustomization, value: activeTab },
stubs: { GlDrawer },
});
};
describe('mounted', () => {
- describe('when `k8sTreeView feature flag is disabled', () => {
+ describe('when `k8sTreeView feature flag is enabled', () => {
beforeEach(() => {
- createWrapper();
+ createWrapper({ k8sTreeViewEnabled: true });
});
it('shows tabs', () => {
@@ -58,14 +59,15 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_ta
expect(findKubernetesServices().props()).toEqual({ namespace, configuration });
});
- it("doesn't render summary tab", () => {
- expect(findKubernetesSummary().exists()).toBe(false);
+ it('renders summary tab', () => {
+ expect(findKubernetesSummary().props('fluxKustomization')).toEqual(fluxKustomization);
});
});
- it('renders summary tab if the feature flag is enabled', () => {
- createWrapper({ k8sTreeViewEnabled: true });
- expect(findKubernetesSummary().exists()).toBe(true);
+ it('renders summary tab if the feature flag is disabled', () => {
+ createWrapper();
+
+ expect(findKubernetesSummary().exists()).toBe(false);
});
});
diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js
index 9501887b10c..b4104f303e0 100644
--- a/spec/frontend/environments/graphql/mock_data.js
+++ b/spec/frontend/environments/graphql/mock_data.js
@@ -815,12 +815,20 @@ export const k8sNamespacesMock = [
{ metadata: { name: 'agent' } },
];
-export const fluxKustomizationsMock = [
- {
- status: 'True',
- type: 'Ready',
- },
-];
+const fluxResourceStatusMock = [{ status: 'True', type: 'Ready', message: '', reason: '' }];
+export const fluxKustomizationMock = {
+ kind: 'Kustomization',
+ metadata: { name: 'custom-resource', namespace: 'custom-namespace' },
+ status: { conditions: fluxResourceStatusMock },
+};
+export const fluxKustomizationMapped = {
+ kind: 'Kustomization',
+ metadata: { name: 'custom-resource' },
+ conditions: fluxResourceStatusMock,
+};
+export const fluxHelmReleaseMapped = {
+ conditions: fluxResourceStatusMock,
+};
export const fluxResourcePathMock = 'kustomize.toolkit.fluxcd.io/v1/path/to/flux/resource';
diff --git a/spec/frontend/environments/graphql/resolvers/flux_spec.js b/spec/frontend/environments/graphql/resolvers/flux_spec.js
index 9f002d27277..dc2e0f14853 100644
--- a/spec/frontend/environments/graphql/resolvers/flux_spec.js
+++ b/spec/frontend/environments/graphql/resolvers/flux_spec.js
@@ -8,7 +8,11 @@ import {
connectionStatus,
k8sResourceType,
} from '~/environments/graphql/resolvers/kubernetes/constants';
-import { fluxKustomizationsMock } from '../mock_data';
+import {
+ fluxKustomizationMock,
+ fluxKustomizationMapped,
+ fluxHelmReleaseMapped,
+} from '../mock_data';
jest.mock('~/environments/graphql/resolvers/kubernetes/k8s_connection_status');
@@ -32,7 +36,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
mock.reset();
});
- describe('fluxKustomizationStatus', () => {
+ describe('fluxKustomization', () => {
const client = { writeQuery: jest.fn() };
const fluxResourcePath =
'kustomize.toolkit.fluxcd.io/v1/namespaces/my-namespace/kustomizations/app';
@@ -44,7 +48,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
const mockOnDataFn = jest.fn().mockImplementation((eventName, callback) => {
if (eventName === 'data') {
- callback(fluxKustomizationsMock);
+ callback([fluxKustomizationMock]);
}
});
const resourceName = 'custom-resource';
@@ -62,12 +66,11 @@ describe('~/frontend/environments/graphql/resolvers', () => {
.onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers })
.reply(HTTP_STATUS_OK, {
apiVersion,
- metadata: { name: resourceName, namespace: resourceNamespace },
- status: { conditions: fluxKustomizationsMock },
+ ...fluxKustomizationMock,
});
});
it('should watch Kustomization by the metadata name from the cluster_client library when the data is present', async () => {
- await mockResolvers.Query.fluxKustomizationStatus(
+ await mockResolvers.Query.fluxKustomization(
null,
{
configuration,
@@ -92,7 +95,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
it('should return data when received from the library', async () => {
- const kustomizationStatus = await mockResolvers.Query.fluxKustomizationStatus(
+ const kustomizationStatus = await mockResolvers.Query.fluxKustomization(
null,
{
configuration,
@@ -101,7 +104,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
{ client },
);
- expect(kustomizationStatus).toEqual(fluxKustomizationsMock);
+ expect(kustomizationStatus).toEqual(fluxKustomizationMapped);
});
});
@@ -110,7 +113,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
.onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers })
.reply(HTTP_STATUS_OK, {});
- await mockResolvers.Query.fluxKustomizationStatus(
+ await mockResolvers.Query.fluxKustomization(
null,
{
configuration,
@@ -128,7 +131,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
.onGet(endpoint, { withCredentials: true, headers: configuration.base })
.reply(HTTP_STATUS_UNAUTHORIZED, { message: apiError });
- const fluxKustomizationsError = mockResolvers.Query.fluxKustomizationStatus(
+ const fluxKustomizationsError = mockResolvers.Query.fluxKustomization(
null,
{
configuration,
@@ -153,7 +156,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
const mockOnDataFn = jest.fn().mockImplementation((eventName, callback) => {
if (eventName === 'data') {
- callback(fluxKustomizationsMock);
+ callback([fluxKustomizationMock]);
}
});
const resourceName = 'custom-resource';
@@ -171,8 +174,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
.onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers })
.reply(HTTP_STATUS_OK, {
apiVersion,
- metadata: { name: resourceName, namespace: resourceNamespace },
- status: { conditions: fluxKustomizationsMock },
+ ...fluxKustomizationMock,
});
});
it('should watch HelmRelease by the metadata name from the cluster_client library when the data is present', async () => {
@@ -204,7 +206,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
{ client },
);
- expect(fluxHelmReleaseStatus).toEqual(fluxKustomizationsMock);
+ expect(fluxHelmReleaseStatus).toEqual(fluxHelmReleaseMapped);
});
});
diff --git a/spec/frontend/environments/mock_data.js b/spec/frontend/environments/mock_data.js
index 0efc2fb10ab..7504054412a 100644
--- a/spec/frontend/environments/mock_data.js
+++ b/spec/frontend/environments/mock_data.js
@@ -315,6 +315,13 @@ const createEnvironment = (data = {}) => ({
const mockKasTunnelUrl = 'https://kas.gitlab.com/k8s-proxy';
+const fluxResourceStatus = [{ status: 'True', type: 'Ready', message: '', reason: '' }];
+const fluxKustomization = {
+ kind: 'Kustomization',
+ metadata: { name: 'my-kustomization' },
+ conditions: fluxResourceStatus,
+};
+
export {
environment,
environmentsList,
@@ -324,4 +331,6 @@ export {
deployBoardMockData,
createEnvironment,
mockKasTunnelUrl,
+ fluxResourceStatus,
+ fluxKustomization,
};
diff --git a/spec/initializers/rdoc_segfault_patch_spec.rb b/spec/initializers/rdoc_segfault_patch_spec.rb
deleted file mode 100644
index f9630295052..00000000000
--- a/spec/initializers/rdoc_segfault_patch_spec.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.describe 'RDoc segfault patch fix' do
- describe 'RDoc::Markup::ToHtml' do
- describe '#parseable?' do
- it 'returns false' do
- to_html = RDoc::Markup::ToHtml.new( nil)
-
- expect(to_html.parseable?('"def foo; end"')).to eq(false)
- end
- end
- end
-
- describe 'RDoc::Markup::Verbatim' do
- describe 'ruby?' do
- it 'returns false' do
- verbatim = RDoc::Markup::Verbatim.new('def foo; end')
- verbatim.format = :ruby
-
- expect(verbatim.ruby?).to eq(false)
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/background_migration/backfill_catalog_resource_version_sem_ver_spec.rb b/spec/lib/gitlab/background_migration/backfill_catalog_resource_version_sem_ver_spec.rb
index 2a97f3b55bf..63d8d707ea4 100644
--- a/spec/lib/gitlab/background_migration/backfill_catalog_resource_version_sem_ver_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_catalog_resource_version_sem_ver_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillCatalogResourceVersionSemVer
before do
tag_to_expected_semver.each_key do |tag|
- releases_table.create!(tag: tag, released_at: Time.zone.now)
+ releases_table.create!(project_id: project.id, tag: tag, released_at: Time.zone.now)
end
releases_table.find_each do |release|
diff --git a/spec/lib/gitlab/background_migration/backfill_remote_development_agent_configs_project_id_spec.rb b/spec/lib/gitlab/background_migration/backfill_remote_development_agent_configs_project_id_spec.rb
new file mode 100644
index 00000000000..9f68624a06a
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_remote_development_agent_configs_project_id_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillRemoteDevelopmentAgentConfigsProjectId,
+ feature_category: :remote_development,
+ schema: 20240530122155 do
+ include_examples 'desired sharding key backfill job' do
+ let(:batch_table) { :remote_development_agent_configs }
+ let(:backfill_column) { :project_id }
+ let(:backfill_via_table) { :cluster_agents }
+ let(:backfill_via_column) { :project_id }
+ let(:backfill_via_foreign_key) { :cluster_agent_id }
+ end
+end
diff --git a/spec/lib/gitlab/database/sharding_key_spec.rb b/spec/lib/gitlab/database/sharding_key_spec.rb
index a2e1d21f88d..51195148868 100644
--- a/spec/lib/gitlab/database/sharding_key_spec.rb
+++ b/spec/lib/gitlab/database/sharding_key_spec.rb
@@ -38,7 +38,6 @@ RSpec.describe 'new tables missing sharding_key', feature_category: :cell do
'member_roles.namespace_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/444161
*['milestones.project_id', 'milestones.group_id'],
'pages_domains.project_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/442178,
- 'releases.project_id',
'remote_mirrors.project_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/444643
'sprints.group_id',
'subscription_add_on_purchases.namespace_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/444338
diff --git a/spec/migrations/20240521051045_delete_invalid_releases_records_spec.rb b/spec/migrations/20240521051045_delete_invalid_releases_records_spec.rb
new file mode 100644
index 00000000000..5c0984b8b19
--- /dev/null
+++ b/spec/migrations/20240521051045_delete_invalid_releases_records_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe DeleteInvalidReleasesRecords, feature_category: :release_orchestration do
+ let!(:releases) { table(:releases) }
+
+ let!(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') }
+ let!(:user) { table(:users).create!(email: 'test@example.com', projects_limit: 10) }
+ let!(:project) { table(:projects).create!(namespace_id: namespace.id, project_namespace_id: namespace.id) }
+
+ describe '#up' do
+ before do
+ releases.create!(project_id: project.id, tag: 'v1', author_id: user.id, released_at: Time.current)
+ releases.create!(project_id: nil, tag: 'v2', author_id: user.id, released_at: Time.current)
+ releases.create!(project_id: nil, tag: 'v3', author_id: user.id, released_at: Time.current)
+
+ stub_const("#{described_class}::BATCH_SIZE", 1)
+ end
+
+ it 'deletes records without a project_id' do
+ migrate!
+
+ expect(releases.count).to eq(1)
+ expect(releases.first).to have_attributes(
+ project_id: project.id,
+ author_id: user.id,
+ tag: 'v1'
+ )
+ end
+
+ it 'does nothing on gitlab.com' do
+ allow(Gitlab).to receive(:com?).and_return(true)
+
+ migrate!
+
+ expect(releases.count).to eq(3)
+ end
+ end
+end
diff --git a/spec/migrations/20240530122159_queue_backfill_remote_development_agent_configs_project_id_spec.rb b/spec/migrations/20240530122159_queue_backfill_remote_development_agent_configs_project_id_spec.rb
new file mode 100644
index 00000000000..81448b75e5e
--- /dev/null
+++ b/spec/migrations/20240530122159_queue_backfill_remote_development_agent_configs_project_id_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueBackfillRemoteDevelopmentAgentConfigsProjectId, feature_category: :remote_development do
+ let!(:batched_migration) { described_class::MIGRATION }
+
+ it 'schedules a new batched migration' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).to have_scheduled_batched_migration(
+ table_name: :remote_development_agent_configs,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE,
+ gitlab_schema: :gitlab_main_cell,
+ job_arguments: [
+ :project_id,
+ :cluster_agents,
+ :project_id,
+ :cluster_agent_id
+ ]
+ )
+ }
+ end
+ end
+end
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
index a9149b0eebe..26f6b4efb72 100644
--- a/spec/models/concerns/routable_spec.rb
+++ b/spec/models/concerns/routable_spec.rb
@@ -287,7 +287,7 @@ RSpec.describe Project, 'Routable', :with_clean_rails_cache, feature_category: :
end
describe '.find_by_full_path' do
- it 'does not return a record if the sources are different, but the IDs match' do
+ it 'does not return a record if the sources are different, but the IDs match', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/451914' do
group = create(:group, id: 1992)
project = create(:project, id: 1992)
diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb
index e60a5795a16..816e4de06ba 100644
--- a/spec/models/release_spec.rb
+++ b/spec/models/release_spec.rb
@@ -178,7 +178,7 @@ RSpec.describe Release, feature_category: :release_orchestration do
context 'when there are no releases' do
it 'returns nil' do
- project.releases.delete_all
+ project.releases.delete_all(:delete_all)
expect(latest).to eq(nil)
end
@@ -214,8 +214,8 @@ RSpec.describe Release, feature_category: :release_orchestration do
context 'when there are no releases' do
it 'returns empty response' do
- project.releases.delete_all
- project2.releases.delete_all
+ project.releases.delete_all(:delete_all)
+ project2.releases.delete_all(:delete_all)
expect(latest_for_projects).to be_empty
end
diff --git a/spec/scripts/pipeline/pre_merge_checks_spec.rb b/spec/scripts/pipeline/pre_merge_checks_spec.rb
new file mode 100644
index 00000000000..186ab19a26c
--- /dev/null
+++ b/spec/scripts/pipeline/pre_merge_checks_spec.rb
@@ -0,0 +1,210 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+require_relative '../../support/webmock'
+require_relative '../../../scripts/pipeline/pre_merge_checks'
+
+RSpec.describe PreMergeChecks, time_travel_to: Time.parse('2024-05-29T08:00:00 UTC'), feature_category: :tooling do
+ include StubENV
+
+ let(:instance) { described_class.new }
+ let(:project_id) { '42' }
+ let(:merge_request_iid) { '1' }
+ let(:current_pipeline_id) { mr_pipelines[0][:id].to_s }
+ let(:mr_pipelines_url) { "https://gitlab.test/api/v4/projects/#{project_id}/merge_requests/#{merge_request_iid}/pipelines" }
+
+ let(:latest_mr_pipeline_ref) { "refs/merge-requests/1/merge" }
+ let(:latest_mr_pipeline_created_at) { "2024-05-29T07:15:00 UTC" }
+ let(:latest_mr_pipeline_name) { "Ruby 3.2 MR [tier:3, gdk]" }
+ let(:latest_mr_pipeline_short) do
+ {
+ id: 1309901620,
+ ref: latest_mr_pipeline_ref,
+ status: "success",
+ source: "merge_request_event",
+ created_at: latest_mr_pipeline_created_at
+ }
+ end
+
+ let(:latest_mr_pipeline_detailed) do
+ latest_mr_pipeline_short.merge(name: latest_mr_pipeline_name)
+ end
+
+ let(:mr_pipelines) do
+ [
+ {
+ id: 1309903341,
+ ref: "refs/merge-requests/1/train",
+ status: "success",
+ source: "merge_request_event",
+ created_at: "2024-05-29T07:30:00 UTC"
+ },
+ latest_mr_pipeline_short,
+ {
+ id: 1309753047,
+ ref: "refs/merge-requests/1/train",
+ status: "failed",
+ source: "merge_request_event",
+ created_at: "2024-05-29T06:30:00 UTC"
+ },
+ {
+ id: 1308929843,
+ ref: "refs/merge-requests/1/merge",
+ status: "success",
+ source: "merge_request_event",
+ created_at: "2024-05-29T05:30:00 UTC"
+ },
+ {
+ id: 1308699353,
+ ref: "refs/merge-requests/1/head",
+ status: "failed",
+ source: "merge_request_event",
+ created_at: "2024-05-29T04:30:00 UTC"
+ }
+ ]
+ end
+
+ before do
+ stub_env(
+ 'CI_API_V4_URL' => 'https://gitlab.test/api/v4',
+ 'CI_PROJECT_ID' => project_id,
+ 'CI_MERGE_REQUEST_IID' => merge_request_iid,
+ 'CI_PIPELINE_ID' => current_pipeline_id
+ )
+ end
+
+ describe '#initialize' do
+ context 'when project_id is missing' do
+ let(:project_id) { nil }
+
+ it 'raises an error' do
+ expect { instance }.to raise_error("Missing project_id")
+ end
+ end
+
+ context 'when merge_request_iid is missing' do
+ let(:merge_request_iid) { nil }
+
+ it 'raises an error' do
+ expect { instance }.to raise_error("Missing merge_request_iid")
+ end
+ end
+
+ context 'when current_pipeline_id is missing' do
+ let(:current_pipeline_id) { nil }
+
+ it 'raises an error' do
+ expect { instance }.to raise_error("Missing current_pipeline_id")
+ end
+ end
+ end
+
+ describe '#execute' do
+ # rubocop:disable RSpec/VerifiedDoubles -- See the disclaimer above
+ let(:api_client) { double('Gitlab::Client') }
+ let(:latest_mr_pipeline) { double('pipeline', **latest_mr_pipeline_detailed) }
+
+ # We need to take some precautions when using the `gitlab` gem in this project.
+ #
+ # See https://docs.gitlab.com/ee/development/pipelines/internals.html#using-the-gitlab-ruby-gem-in-the-canonical-project.
+ #
+ before do
+ stub_request(:get, mr_pipelines_url).to_return(status: 200, body: mr_pipelines.to_json)
+
+ allow(instance).to receive(:api_client).and_return(api_client)
+ allow(api_client).to yield_pipelines(:merge_request_pipelines, mr_pipelines)
+
+ # Ensure we don't output to stdout while running tests
+ allow(instance).to receive(:puts)
+ end
+
+ def yield_pipelines(api_method, pipelines)
+ messages = receive_message_chain(api_method, :auto_paginate)
+
+ pipelines.inject(messages) do |stub, pipeline|
+ stub.and_yield(double(**pipeline))
+ end
+ end
+ # rubocop:enable RSpec/VerifiedDoubles
+
+ context 'when default arguments are present' do
+ context 'when we have a latest pipeline' do
+ before do
+ allow(api_client).to receive(:pipeline).with(project_id, mr_pipelines[1][:id]).and_return(latest_mr_pipeline)
+ end
+
+ context 'and it passes all the checks' do
+ it 'does not raise an error' do
+ expect { instance.execute }.not_to raise_error
+ end
+ end
+
+ context 'and it is not a merged results pipeline' do
+ let(:latest_mr_pipeline_ref) { "refs/merge-requests/1/head" }
+
+ it 'raises an error' do
+ expect { instance.execute }.to raise_error(
+ "Expected to have a Merged Results pipeline but got #{latest_mr_pipeline_ref}!"
+ )
+ end
+ end
+
+ context 'and it is not fresh enough' do
+ let(:latest_mr_pipeline_created_at) { "2024-05-29T03:30:00 UTC" }
+
+ it 'raises an error' do
+ expect { instance.execute }.to raise_error(
+ "Expected latest pipeline to be created within the last 4 hours (it was created 4.5 hours ago)!"
+ )
+ end
+ end
+
+ context 'and it is a predictive pipeline' do
+ let(:latest_mr_pipeline_name) { "Ruby 3.2 MR [predictive]" }
+
+ it 'raises an error' do
+ expect { instance.execute }
+ .to raise_error(/\AExpected latest pipeline not to be a predictive pipeline!/)
+ end
+ end
+
+ context 'and it is not a tier-3 pipeline' do
+ let(:latest_mr_pipeline_name) { "Ruby 3.2 MR [tier:2]" }
+
+ it 'raises an error' do
+ expect { instance.execute }
+ .to raise_error(/\AExpected latest pipeline to be a tier-3 pipeline!/)
+ end
+ end
+
+ context 'and it is qa-only pipeline' do
+ let(:latest_mr_pipeline_name) { "Ruby 3.2 MR [types:qa,qa-gdk]" }
+
+ it 'does not raise an error' do
+ expect { instance.execute }
+ .not_to raise_error
+ end
+ end
+ end
+
+ context 'when we do not have a latest pipeline' do
+ let(:mr_pipelines) do
+ [
+ {
+ id: 1309903341,
+ ref: "refs/merge-requests/1/train",
+ status: "success",
+ source: "merge_request_event",
+ created_at: "2024-05-29T08:29:43.472Z"
+ }
+ ]
+ end
+
+ it 'raises an error' do
+ expect { instance.execute }.to raise_error("Expected to have a latest pipeline but got none!")
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index b13234deb61..abd34d4ef9b 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -4799,7 +4799,6 @@
- './spec/initializers/pages_storage_check_spec.rb'
- './spec/initializers/rack_multipart_patch_spec.rb'
- './spec/initializers/rails_asset_host_spec.rb'
-- './spec/initializers/rdoc_segfault_patch_spec.rb'
- './spec/initializers/remove_active_job_execute_callback_spec.rb'
- './spec/initializers/rest-client-hostname_override_spec.rb'
- './spec/initializers/secret_token_spec.rb'
diff --git a/spec/support/shared_examples/requests/api/graphql/releases_and_group_releases_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/releases_and_group_releases_shared_examples.rb
index fd7a530fcd6..ca0a671af02 100644
--- a/spec/support/shared_examples/requests/api/graphql/releases_and_group_releases_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/releases_and_group_releases_shared_examples.rb
@@ -18,7 +18,7 @@ RSpec.shared_examples 'when there are no releases' do
let(:data) { graphql_data.dig(resource_type.to_s, 'releases') }
before do
- project.releases.delete_all
+ project.releases.delete_all(:delete_all)
post_query
end
diff --git a/spec/views/layouts/_head.html.haml_spec.rb b/spec/views/layouts/_head.html.haml_spec.rb
index 5ef25bdbde4..992edcf95f3 100644
--- a/spec/views/layouts/_head.html.haml_spec.rb
+++ b/spec/views/layouts/_head.html.haml_spec.rb
@@ -95,6 +95,17 @@ RSpec.describe 'layouts/_head' do
end
end
+ context 'when custom_html_header_tags are set' do
+ before do
+ allow(Gitlab.config.gitlab).to receive(:custom_html_header_tags).and_return('')
+ end
+
+ it 'adds the custom html header tag' do
+ render
+ expect(rendered).to match('')
+ end
+ end
+
context 'when an asset_host is set and snowplow url is set', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/346542' do
let(:asset_host) { 'http://test.host' }
let(:snowplow_collector_hostname) { 'www.snow.plow' }