# 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-29T10:00:00 UTC'), feature_category: :tooling do include StubENV let(:instance) { described_class.new } let(:project_id) { '42' } let(:merge_request_iid) { '1' } let(:target_branch) { 'master' } 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_status) { "success" } let(:latest_mr_pipeline_created_at) { "2024-05-29T07:15:00 UTC" } let(:latest_mr_pipeline_project_id) { project_id.to_i } # We get an integer from the API let(:latest_mr_pipeline_web_url) { "https://gitlab.com/gitlab-org/gitlab/-/pipelines/1310472835" } 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: latest_mr_pipeline_status, project_id: latest_mr_pipeline_project_id, source: "merge_request_event", created_at: latest_mr_pipeline_created_at, web_url: latest_mr_pipeline_web_url } end let(:latest_mr_pipeline_detailed) do latest_mr_pipeline_short.merge(name: latest_mr_pipeline_name) end let(:mr_pipelines) do [ { id: 1309903340, ref: "refs/merge-requests/1/train", status: "success", project_id: project_id, source: "merge_request_event", created_at: "2024-05-29T07:30:00 UTC" }, { id: 1309903341, ref: "refs/merge-requests/1/train", status: "success", project_id: project_id, source: "merge_request_event", created_at: "2024-05-29T07:15:00 UTC" }, latest_mr_pipeline_short, { id: 1309753047, ref: "refs/merge-requests/1/train", status: "failed", project_id: project_id, source: "merge_request_event", created_at: "2024-05-29T06:30:00 UTC" }, { id: 1308929843, ref: "refs/merge-requests/1/train", status: "success", project_id: project_id, source: "merge_request_event", created_at: "2024-05-29T05:30:00 UTC" }, { id: 1308699353, ref: "refs/merge-requests/1/train", status: "failed", project_id: project_id, 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_MERGE_REQUEST_TARGET_BRANCH_NAME' => target_branch ) end describe '#initialize' do context 'when project_id is missing' do let(:project_id) { nil } it 'returns a failed PreMergeChecksStatus' do expect(instance.execute).to be_a(described_class::PreMergeChecksStatus) expect(instance.execute).not_to be_success expect(instance.execute.message).to include("Missing project_id") end end context 'when merge_request_iid is missing' do let(:merge_request_iid) { nil } it 'returns a failed PreMergeChecksStatus' do expect(instance.execute).to be_a(described_class::PreMergeChecksStatus) expect(instance.execute).not_to be_success expect(instance.execute.message).to include("Missing merge_request_iid") end end context 'when target_branch is missing' do let(:target_branch) { nil } it 'returns a failed PreMergeChecksStatus' do expect(instance.execute).to be_a(described_class::PreMergeChecksStatus) expect(instance.execute).not_to be_success expect(instance.execute.message).to include("Missing target_branch") 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[2][:id]).and_return(latest_mr_pipeline) end context 'and the target branch is a stable branch' do let(:target_branch) { 'a-stable-branch-stable-ee' } context 'and the latest pipeline is not fresh enough' do let(:latest_mr_pipeline_created_at) { "2024-05-26T09:30:00 UTC" } it 'returns a failed PreMergeChecksStatus' do expect(instance.execute).to be_a(described_class::PreMergeChecksStatus) expect(instance.execute).not_to be_success expect(instance.execute.message) .to include( "Expected latest pipeline (#{latest_mr_pipeline_web_url}) to be created within the last 72 hours " \ "(it was created 72.5 hours ago)!" ) end end context 'and the latest pipeline is fresh enough' do let(:latest_mr_pipeline_created_at) { "2024-05-26T10:30:00 UTC" } it 'returns a successful PreMergeChecksStatus' do expect(instance.execute).to be_a(described_class::PreMergeChecksStatus) expect(instance.execute).to be_success end end end context 'and it passes all the checks' do it 'returns a successful PreMergeChecksStatus' do expect(instance.execute).to be_a(described_class::PreMergeChecksStatus) expect(instance.execute).to be_success end end context 'and it is not a merged results pipeline' do let(:latest_mr_pipeline_ref) { "refs/merge-requests/1/head" } it 'returns a failed PreMergeChecksStatus' do expect(instance.execute).to be_a(described_class::PreMergeChecksStatus) expect(instance.execute).not_to be_success expect(instance.execute.message) .to include("Expected to have a Merged Results pipeline but got #{latest_mr_pipeline_ref}") end end shared_examples 'non-success pipeline response' do it 'returns a failed PreMergeChecksStatus' do expect(instance.execute).to be_a(described_class::PreMergeChecksStatus) expect(instance.execute).not_to be_success expect(instance.execute.message).to include( "Expected latest pipeline (#{latest_mr_pipeline_web_url}) to be successful!" ) expect(instance.execute.message).to include("Pipeline status was \"#{latest_mr_pipeline_status}\".") expect(instance.execute.message).to include("Please start a new pipeline.") end end context 'and it is running' do it_behaves_like 'non-success pipeline response' do let(:latest_mr_pipeline_status) { "running" } end end context 'and it is failed' do it_behaves_like 'non-success pipeline response' do let(:latest_mr_pipeline_status) { "failed" } end end context 'and it is canceled' do it_behaves_like 'non-success pipeline response' do let(:latest_mr_pipeline_status) { "canceled" } end end context 'and it is not fresh enough' do let(:latest_mr_pipeline_created_at) { "2024-05-29T01:30:00 UTC" } it 'returns a failed PreMergeChecksStatus' do expect(instance.execute).to be_a(described_class::PreMergeChecksStatus) expect(instance.execute).not_to be_success expect(instance.execute.message) .to include( "Expected latest pipeline (#{latest_mr_pipeline_web_url}) to be created within the last 8 hours " \ "(it was created 8.5 hours ago)!" ) end end context 'and it is a predictive pipeline' do let(:latest_mr_pipeline_name) { "Ruby 3.2 MR [predictive]" } it 'returns a failed PreMergeChecksStatus' do expect(instance.execute).to be_a(described_class::PreMergeChecksStatus) expect(instance.execute).not_to be_success expect(instance.execute.message).to include( "Expected latest pipeline (#{latest_mr_pipeline_web_url}) not to be a predictive pipeline!" ) expect(instance.execute.message).to include("Pipeline name was \"#{latest_mr_pipeline_name}\".") end context 'when also matching the required tier identifier' do let(:latest_mr_pipeline_name) { "Ruby 3.2 MR [predictive,tier:3]" } it 'returns a failed PreMergeChecksStatus' do expect(instance.execute).to be_a(described_class::PreMergeChecksStatus) expect(instance.execute).not_to be_success expect(instance.execute.message).to include( "Expected latest pipeline (#{latest_mr_pipeline_web_url}) not to be a predictive pipeline!" ) expect(instance.execute.message).to include("Pipeline name was \"#{latest_mr_pipeline_name}\".") end end end context 'and it is not a tier-3 pipeline' do let(:latest_mr_pipeline_name) { "Ruby 3.2 MR [tier:2]" } it 'returns a failed PreMergeChecksStatus' do expect(instance.execute).to be_a(described_class::PreMergeChecksStatus) expect(instance.execute).not_to be_success expect(instance.execute.message).to include( "Expected latest pipeline (#{latest_mr_pipeline_web_url}) to be a tier-3 pipeline" ) expect(instance.execute.message).to include("Pipeline name was \"#{latest_mr_pipeline_name}\".") end end context 'and it is qa-only pipeline' do let(:latest_mr_pipeline_name) { "Ruby 3.2 MR [types:qa,qa-gdk]" } it 'returns a successful PreMergeChecksStatus' do expect(instance.execute).to be_a(described_class::PreMergeChecksStatus) expect(instance.execute).to be_success end end context 'and was run in a different project' do let(:latest_mr_pipeline_project_id) { 5678 } it 'returns a failed PreMergeChecksStatus' do expect(instance.execute).to be_a(described_class::PreMergeChecksStatus) expect(instance.execute).not_to be_success expect(instance.execute.message).to include( "Expected to have a latest pipeline that ran in project ##{project_id} but got none!" ) 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 'returns a failed PreMergeChecksStatus' do expect(instance.execute).to be_a(described_class::PreMergeChecksStatus) expect(instance.execute).not_to be_success expect(instance.execute.message).to include( "Expected to have a latest pipeline that ran in project ##{project_id} but got none!" ) end end end end end