# frozen_string_literal: true require 'spec_helper' RSpec.describe API::Deployments, feature_category: :continuous_delivery do let_it_be(:user) { create(:user) } let_it_be(:non_member) { create(:user) } before do project.add_maintainer(user) end describe 'GET /projects/:id/deployments' do let_it_be(:project) { create(:project, :repository) } let_it_be(:production) { create(:environment, :production, project: project) } let_it_be(:staging) { create(:environment, :staging, project: project) } let_it_be(:build) { create(:ci_build, :success, project: project) } let_it_be(:deployment_1) { create(:deployment, :success, project: project, environment: production, deployable: build, ref: 'master', created_at: Time.now, updated_at: Time.now) } let_it_be(:deployment_2) { create(:deployment, :success, project: project, environment: staging, deployable: build, ref: 'master', created_at: 1.day.ago, finished_at: 2.hours.ago, updated_at: 2.hours.ago) } let_it_be(:deployment_3) { create(:deployment, :success, project: project, environment: staging, deployable: build, ref: 'master', created_at: 2.days.ago, finished_at: 1.hour.ago, updated_at: 1.hour.ago) } def perform_request(params = {}) get api("/projects/#{project.id}/deployments", user), params: params end context 'as member of the project' do it 'returns projects deployments sorted by id asc' do perform_request expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(3) expect(json_response.first['iid']).to eq(deployment_1.iid) expect(json_response.first['sha']).to match /\A\h{40}\z/ expect(json_response.second['iid']).to eq(deployment_2.iid) expect(json_response.last['iid']).to eq(deployment_3.iid) end context 'with updated_at filters specified' do it 'returns projects deployments with last update in specified datetime range' do perform_request({ updated_before: 30.minutes.ago, updated_after: 90.minutes.ago, order_by: :updated_at }) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response.first['id']).to eq(deployment_3.id) end context 'when forbidden order_by is specified' do before do stub_feature_flags(deployments_raise_updated_at_inefficient_error_override: false) end it 'returns an error' do perform_request({ updated_before: 30.minutes.ago, updated_after: 90.minutes.ago, order_by: :id }) expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['message']).to include('`updated_at` filter requires `updated_at` sort') end end end context 'with finished after and before filters specified' do context 'for successful deployments' do it 'returns projects deployments finished before the specified datetime range' do perform_request({ status: :success, finished_before: 90.minutes.ago, order_by: :finished_at, environment: 'staging' }) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response.first['id']).to eq(deployment_2.id) end it 'returns projects deployments finished after the specified datetime range' do perform_request({ status: :success, finished_after: 90.minutes.ago, order_by: :finished_at, environment: 'staging' }) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response.first['id']).to eq(deployment_3.id) end end context 'for unsuccessful deployments' do it 'returns an error' do perform_request({ status: :failed, finished_before: 30.minutes.ago, order_by: :finished_at }) expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['message']).to include('`finished_at` filter must be combined with `success` status filter.') end end context 'when a forbidden order_by is specified' do it 'returns an error' do perform_request({ status: :success, finished_before: 30.minutes.ago, order_by: :id }) expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['message']).to include('`finished_at` filter requires `finished_at` sort.') end end end context 'with the environment filter specifed' do it 'returns deployments for the environment' do perform_request({ environment: production.name }) expect(json_response.size).to eq(1) expect(json_response.first['iid']).to eq(deployment_1.iid) end end describe 'ordering' do let(:order_by) { 'iid' } let(:sort) { 'desc' } subject { get api("/projects/#{project.id}/deployments?order_by=#{order_by}&sort=#{sort}", user) } before do subject end def expect_deployments(ordered_deployments) expect(json_response.map { |d| d['id'] }).to eq(ordered_deployments.map(&:id)) end it 'returns ordered deployments' do expect(json_response.map { |i| i['id'] }).to eq([deployment_3.id, deployment_2.id, deployment_1.id]) end context 'with invalid order_by' do let(:order_by) { 'wrong_sorting_value' } it 'returns error' do expect(response).to have_gitlab_http_status(:bad_request) end end context 'with invalid sorting' do let(:sort) { 'wrong_sorting_direction' } it 'returns error' do expect(response).to have_gitlab_http_status(:bad_request) end end end it 'returns multiple deployments without N + 1' do perform_request # warm up the cache control_count = ActiveRecord::QueryRecorder.new { perform_request }.count create(:deployment, :success, project: project, deployable: build, iid: 21, ref: 'master') expect { perform_request }.not_to exceed_query_limit(control_count) end end context 'as non member' do it 'returns a 404 status code' do get api("/projects/#{project.id}/deployments", non_member) expect(response).to have_gitlab_http_status(:not_found) end end end describe 'GET /projects/:id/deployments/:deployment_id' do let(:project) { deployment.environment.project } let!(:deployment) { create(:deployment, :success) } context 'as a member of the project' do it 'returns the projects deployment' do get api("/projects/#{project.id}/deployments/#{deployment.id}", user) expect(response).to have_gitlab_http_status(:ok) expect(json_response['sha']).to match /\A\h{40}\z/ expect(json_response['id']).to eq(deployment.id) end end context 'as non member' do it 'returns a 404 status code' do get api("/projects/#{project.id}/deployments/#{deployment.id}", non_member) expect(response).to have_gitlab_http_status(:not_found) end end end describe 'POST /projects/:id/deployments' do let!(:project) { create(:project, :repository) } # * ddd0f15ae83993f5cb66a927a28673882e99100b (HEAD -> master, origin/master, origin/HEAD) Merge branch 'po-fix-test-en # |\ # | * 2d1db523e11e777e49377cfb22d368deec3f0793 Correct test_env.rb path for adding branch # |/ # * 1e292f8fedd741b75372e19097c76d327140c312 Merge branch 'cherry-pick-ce369011' into 'master' let_it_be(:sha) { 'ddd0f15ae83993f5cb66a927a28673882e99100b' } let_it_be(:first_deployment_sha) { '1e292f8fedd741b75372e19097c76d327140c312' } before do # Creating the first deployment is an edge-case that is already covered by unit testing, # here we want to see the behavior of a running system so we create a first deployment post( api("/projects/#{project.id}/deployments", user), params: { environment: 'production', sha: first_deployment_sha, ref: 'master', tag: false, status: 'success' } ) end context 'as a maintainer' do it 'creates a new deployment' do post( api("/projects/#{project.id}/deployments", user), params: { environment: 'production', sha: sha, ref: 'master', tag: false, status: 'success' } ) expect(response).to have_gitlab_http_status(:created) expect(json_response['sha']).to eq(sha) expect(json_response['ref']).to eq('master') expect(json_response['environment']['name']).to eq('production') end it 'errors when creating a deployment with an invalid name' do post( api("/projects/#{project.id}/deployments", user), params: { environment: 'a' * 300, sha: sha, ref: 'master', tag: false, status: 'success' } ) expect(response).to have_gitlab_http_status(:bad_request) end it 'links any merged merge requests to the deployment', :sidekiq_inline do mr = create( :merge_request, :merged, merge_commit_sha: sha, target_project: project, source_project: project, target_branch: 'master', source_branch: 'foo' ) post( api("/projects/#{project.id}/deployments", user), params: { environment: 'production', sha: sha, ref: 'master', tag: false, status: 'success' } ) deploy = project.deployments.last expect(deploy.merge_requests).to eq([mr]) end end context 'as a developer' do let(:developer) { create(:user) } before do project.add_developer(developer) end it 'creates a new deployment' do post( api("/projects/#{project.id}/deployments", developer), params: { environment: 'production', sha: sha, ref: 'master', tag: false, status: 'success' } ) expect(response).to have_gitlab_http_status(:created) expect(json_response['sha']).to eq(sha) expect(json_response['ref']).to eq('master') end it 'links any merged merge requests to the deployment', :sidekiq_inline do mr = create( :merge_request, :merged, merge_commit_sha: sha, target_project: project, source_project: project, target_branch: 'master', source_branch: 'foo' ) post( api("/projects/#{project.id}/deployments", developer), params: { environment: 'production', sha: sha, ref: 'master', tag: false, status: 'success' } ) deploy = project.deployments.last expect(deploy.merge_requests).to eq([mr]) end it 'links any picked merge requests to the deployment', :sidekiq_inline do mr = create( :merge_request, :merged, merge_commit_sha: sha, target_project: project, source_project: project, target_branch: 'master', source_branch: 'foo' ) # we branch from the previous deployment and cherry-pick mr into the new branch branch = project.repository.add_branch(developer, 'stable', first_deployment_sha) expect(branch).not_to be_nil result = ::Commits::CherryPickService .new(project, developer, commit: mr.merge_commit, start_branch: 'stable', branch_name: 'stable') .execute expect(result[:status]).to eq(:success), result[:message] pick_sha = result[:result] post( api("/projects/#{project.id}/deployments", developer), params: { environment: 'production', sha: pick_sha, ref: 'stable', tag: false, status: 'success' } ) deploy = project.deployments.last expect(deploy.merge_requests).to eq([mr]) end end context 'as non member' do it 'returns a 404 status code' do post( api("/projects/#{project.id}/deployments", non_member), params: { environment: 'production', sha: '123', ref: 'master', tag: false, status: 'success' } ) expect(response).to have_gitlab_http_status(:not_found) end end end describe 'PUT /projects/:id/deployments/:deployment_id' do let(:project) { create(:project, :repository) } let(:build) { create(:ci_build, :failed, project: project) } let(:environment) { create(:environment, project: project) } let(:deploy) do create( :deployment, :failed, project: project, environment: environment, deployable: nil, sha: project.commit.sha ) end context 'as a maintainer' do it 'returns a 403 when updating a deployment with a build' do deploy.update!(deployable: build) put( api("/projects/#{project.id}/deployments/#{deploy.id}", user), params: { status: 'success' } ) expect(response).to have_gitlab_http_status(:forbidden) end it 'updates a deployment without an associated build' do put( api("/projects/#{project.id}/deployments/#{deploy.id}", user), params: { status: 'success' } ) expect(response).to have_gitlab_http_status(:ok) expect(json_response['status']).to eq('success') end it 'returns an error when an invalid status transition is detected' do put( api("/projects/#{project.id}/deployments/#{deploy.id}", user), params: { status: 'running' } ) expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['message']['status']).to include(%Q{cannot transition via \"run\"}) end it 'links merge requests when the deployment status changes to success', :sidekiq_inline do mr = create( :merge_request, :merged, target_project: project, source_project: project, target_branch: 'master', source_branch: 'foo' ) put( api("/projects/#{project.id}/deployments/#{deploy.id}", user), params: { status: 'success' } ) deploy = project.deployments.last expect(deploy.merge_requests).to eq([mr]) end end context 'as a developer' do let(:developer) { create(:user) } before do project.add_developer(developer) end it 'returns a 403 when updating a deployment with a build' do deploy.update!(deployable: build) put( api("/projects/#{project.id}/deployments/#{deploy.id}", developer), params: { status: 'success' } ) expect(response).to have_gitlab_http_status(:forbidden) end it 'updates a deployment without an associated build' do put( api("/projects/#{project.id}/deployments/#{deploy.id}", developer), params: { status: 'success' } ) expect(response).to have_gitlab_http_status(:ok) expect(json_response['status']).to eq('success') end end context 'as non member' do it 'returns a 404 status code' do put( api("/projects/#{project.id}/deployments/#{deploy.id}", non_member), params: { status: 'success' } ) expect(response).to have_gitlab_http_status(:not_found) end end end describe 'DELETE /projects/:id/deployments/:deployment_id' do let(:project) { create(:project, :repository) } let(:environment) { create(:environment, project: project) } let(:commits) { project.repository.commits(nil, { limit: 3 }) } let!(:deploy) do create( :deployment, :success, project: project, environment: environment, deployable: nil, sha: commits[1].sha ) end let!(:old_deploy) do create( :deployment, :success, project: project, environment: environment, deployable: nil, sha: commits[0].sha, finished_at: 1.year.ago ) end let!(:running_deploy) do create( :deployment, :running, project: project, environment: environment, deployable: nil, sha: commits[2].sha ) end context 'as an maintainer' do it 'deletes a deployment' do delete api("/projects/#{project.id}/deployments/#{old_deploy.id}", user) expect(response).to have_gitlab_http_status(:no_content) end it 'will not delete a running deployment' do delete api("/projects/#{project.id}/deployments/#{running_deploy.id}", user) expect(response).to have_gitlab_http_status(:bad_request) expect(response.body).to include("Cannot destroy running deployment") end end context 'as a developer' do let(:developer) { create(:user) } before do project.add_developer(developer) end it 'is forbidden' do delete api("/projects/#{project.id}/deployments/#{deploy.id}", developer) expect(response).to have_gitlab_http_status(:forbidden) end end context 'as non member' do it 'is not found' do delete api("/projects/#{project.id}/deployments/#{deploy.id}", non_member) expect(response).to have_gitlab_http_status(:not_found) end end context 'for non-existent deployment' do it 'is not found' do delete api("/projects/#{project.id}/deployments/#{non_existing_record_id}", project.first_owner) expect(response).to have_gitlab_http_status(:not_found) end end end describe 'GET /projects/:id/deployments/:deployment_id/merge_requests' do let(:project) { create(:project, :repository) } let!(:deployment) { create(:deployment, :success, project: project) } subject { get api("/projects/#{project.id}/deployments/#{deployment.id}/merge_requests", user) } context 'when a user is not a member of the deployment project' do let(:user) { build(:user) } it 'returns a 404 status code' do subject expect(response).to have_gitlab_http_status(:not_found) end end context 'when a user member of the deployment project' do let_it_be(:project2) { create(:project) } let!(:merge_request1) { create(:merge_request, source_project: project, target_project: project) } let!(:merge_request2) { create(:merge_request, source_project: project, target_project: project, state: 'closed') } let!(:merge_request3) { create(:merge_request, source_project: project2, target_project: project2) } it 'returns the relevant merge requests linked to a deployment for a project' do deployment.link_merge_requests(MergeRequest.where(id: [merge_request1.id, merge_request2.id])) subject expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response.map { |d| d['id'] }).to contain_exactly(merge_request1.id, merge_request2.id) end context 'when a deployment is not associated to any existing merge requests' do it 'returns an empty array' do subject expect(response).to have_gitlab_http_status(:ok) expect(json_response).to eq([]) end end end end context 'prevent N + 1 queries' do context 'when the endpoint returns multiple records' do let(:project) { create(:project, :repository) } let!(:deployment) { create(:deployment, :success, project: project) } subject { get api("/projects/#{project.id}/deployments?order_by=id&sort=asc", user) } it 'succeeds', :aggregate_failures do subject expect(response).to have_gitlab_http_status(:ok) expect(json_response.size).to eq(1) end context 'with 10 more records' do it 'does not increase the query count', :aggregate_failures do create_list(:deployment, 10, :success, project: project) expect { subject }.not_to be_n_plus_1_query expect(json_response.size).to eq(11) end end end end end