647 lines
21 KiB
Ruby
647 lines
21 KiB
Ruby
# 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
|