diff options
Diffstat (limited to 'spec/requests/api/terraform/state_spec.rb')
-rw-r--r-- | spec/requests/api/terraform/state_spec.rb | 321 |
1 files changed, 264 insertions, 57 deletions
diff --git a/spec/requests/api/terraform/state_spec.rb b/spec/requests/api/terraform/state_spec.rb index 38b08b4e214..fd34345d814 100644 --- a/spec/requests/api/terraform/state_spec.rb +++ b/spec/requests/api/terraform/state_spec.rb @@ -2,20 +2,22 @@ require 'spec_helper' -RSpec.describe API::Terraform::State, :snowplow do +RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructure_as_code do include HttpBasicAuthHelpers let_it_be(:project) { create(:project) } let_it_be(:developer) { create(:user, developer_projects: [project]) } let_it_be(:maintainer) { create(:user, maintainer_projects: [project]) } - let!(:state) { create(:terraform_state, :with_version, project: project) } - let(:current_user) { maintainer } let(:auth_header) { user_basic_auth_header(current_user) } let(:project_id) { project.id } - let(:state_name) { state.name } + + let(:state_name) { "some-state" } let(:state_path) { "/projects/#{project_id}/terraform/state/#{state_name}" } + let!(:state) do + create(:terraform_state, :with_version, project: project, name: URI.decode_www_form_component(state_name)) + end before do stub_terraform_state_object_storage @@ -91,15 +93,35 @@ RSpec.describe API::Terraform::State, :snowplow do end end - context 'personal acceess token authentication' do + shared_examples 'can access terraform state' do + it 'returns terraform state of a project of given state name' do + request + + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).to eq(state.reload.latest_file.read) + end + end + + context 'personal access token authentication' do context 'with maintainer permissions' do let(:current_user) { maintainer } - it 'returns terraform state belonging to a project of given state name' do - request + where(given_state_name: %w[test-state test.state test%2Ffoo]) + with_them do + it_behaves_like 'can access terraform state' do + let(:state_name) { given_state_name } + end + end - expect(response).to have_gitlab_http_status(:ok) - expect(response.body).to eq(state.reload.latest_file.read) + context 'allow_dots_on_tf_state_names is disabled, and the state name contains a dot' do + let(:state_name) { 'state-name-with-dot' } + let(:state_path) { "/projects/#{project_id}/terraform/state/#{state_name}.tfstate" } + + before do + stub_feature_flags(allow_dots_on_tf_state_names: false) + end + + it_behaves_like 'can access terraform state' end context 'for a project that does not exist' do @@ -112,18 +134,23 @@ RSpec.describe API::Terraform::State, :snowplow do end end + context 'with invalid state name' do + let(:state_name) { 'foo/bar' } + + it 'returns a 404 error' do + request + + expect(response).to have_gitlab_http_status(:not_found) + end + end + it_behaves_like 'cannot access a state that is scheduled for deletion' end context 'with developer permissions' do let(:current_user) { developer } - it 'returns terraform state belonging to a project of given state name' do - request - - expect(response).to have_gitlab_http_status(:ok) - expect(response.body).to eq(state.reload.latest_file.read) - end + it_behaves_like 'can access terraform state' end end @@ -133,12 +160,7 @@ RSpec.describe API::Terraform::State, :snowplow do context 'with maintainer permissions' do let(:job) { create(:ci_build, status: :running, project: project, user: maintainer) } - it 'returns terraform state belonging to a project of given state name' do - request - - expect(response).to have_gitlab_http_status(:ok) - expect(response.body).to eq(state.reload.latest_file.read) - end + it_behaves_like 'can access terraform state' it 'returns unauthorized if the the job is not running' do job.update!(status: :failed) @@ -161,12 +183,7 @@ RSpec.describe API::Terraform::State, :snowplow do context 'with developer permissions' do let(:job) { create(:ci_build, status: :running, project: project, user: developer) } - it 'returns terraform state belonging to a project of given state name' do - request - - expect(response).to have_gitlab_http_status(:ok) - expect(response.body).to eq(state.reload.latest_file.read) - end + it_behaves_like 'can access terraform state' end end end @@ -182,11 +199,26 @@ RSpec.describe API::Terraform::State, :snowplow do context 'with maintainer permissions' do let(:current_user) { maintainer } - it 'updates the state' do - expect { request }.to change { Terraform::State.count }.by(0) + where(given_state_name: %w[test-state test.state test%2Ffoo]) + with_them do + let(:state_name) { given_state_name } - expect(response).to have_gitlab_http_status(:ok) - expect(Gitlab::Json.parse(response.body)).to be_empty + it 'updates the state' do + expect { request }.to change { Terraform::State.count }.by(0) + + expect(response).to have_gitlab_http_status(:ok) + expect(Gitlab::Json.parse(response.body)).to be_empty + end + end + + context 'with invalid state name' do + let(:state_name) { 'foo/bar' } + + it 'returns a 404 error' do + request + + expect(response).to have_gitlab_http_status(:not_found) + end end context 'when serial already exists' do @@ -224,16 +256,39 @@ RSpec.describe API::Terraform::State, :snowplow do end context 'when there is no terraform state of a given name' do - let(:state_name) { 'example2' } + let(:non_existing_state_name) { 'non-existing-state' } + let(:non_existing_state_path) { "/projects/#{project_id}/terraform/state/#{non_existing_state_name}" } + + subject(:request) { post api(non_existing_state_path), headers: auth_header, as: :json, params: params } context 'with maintainer permissions' do let(:current_user) { maintainer } - it 'creates a new state' do - expect { request }.to change { Terraform::State.count }.by(1) + where(given_state_name: %w[test-state test.state test%2Ffoo]) + with_them do + let(:state_name) { given_state_name } - expect(response).to have_gitlab_http_status(:ok) - expect(Gitlab::Json.parse(response.body)).to be_empty + it 'creates a new state' do + expect { request }.to change { Terraform::State.count }.by(1) + + expect(response).to have_gitlab_http_status(:ok) + expect(Gitlab::Json.parse(response.body)).to be_empty + end + end + + context 'allow_dots_on_tf_state_names is disabled, and the state name contains a dot' do + let(:non_existing_state_name) { 'state-name-with-dot.tfstate' } + + before do + stub_feature_flags(allow_dots_on_tf_state_names: false) + end + + it 'strips characters after the dot' do + expect { request }.to change { Terraform::State.count }.by(1) + + expect(response).to have_gitlab_http_status(:ok) + expect(Terraform::State.last.name).to eq('state-name-with-dot') + end end end @@ -269,6 +324,48 @@ RSpec.describe API::Terraform::State, :snowplow do expect(state.reload_latest_version.build).to eq(job) end end + + describe 'response depending on the max allowed state size' do + let(:current_user) { maintainer } + + before do + stub_application_setting(max_terraform_state_size_bytes: max_allowed_state_size) + + request + end + + context 'when the max allowed state size is unlimited (set as 0)' do + let(:max_allowed_state_size) { 0 } + + it 'returns a success response' do + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when the max allowed state size is greater than the request state size' do + let(:max_allowed_state_size) { params.to_json.size + 1 } + + it 'returns a success response' do + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when the max allowed state size is equal to the request state size' do + let(:max_allowed_state_size) { params.to_json.size } + + it 'returns a success response' do + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when the max allowed state size is less than the request state size' do + let(:max_allowed_state_size) { params.to_json.size - 1 } + + it "returns a 'payload too large' response" do + expect(response).to have_gitlab_http_status(:payload_too_large) + end + end + end end describe 'DELETE /projects/:id/terraform/state/:name' do @@ -276,11 +373,8 @@ RSpec.describe API::Terraform::State, :snowplow do it_behaves_like 'endpoint with unique user tracking' - context 'with maintainer permissions' do - let(:current_user) { maintainer } - let(:deletion_service) { instance_double(Terraform::States::TriggerDestroyService) } - - it 'schedules the state for deletion and returns empty body' do + shared_examples 'schedules the state for deletion' do + it 'returns empty body' do expect(Terraform::States::TriggerDestroyService).to receive(:new).and_return(deletion_service) expect(deletion_service).to receive(:execute).once @@ -289,6 +383,40 @@ RSpec.describe API::Terraform::State, :snowplow do expect(response).to have_gitlab_http_status(:ok) expect(Gitlab::Json.parse(response.body)).to be_empty end + end + + context 'with maintainer permissions' do + let(:current_user) { maintainer } + let(:deletion_service) { instance_double(Terraform::States::TriggerDestroyService) } + + where(given_state_name: %w[test-state test.state test%2Ffoo]) + with_them do + let(:state_name) { given_state_name } + + it_behaves_like 'schedules the state for deletion' + end + + context 'allow_dots_on_tf_state_names is disabled, and the state name contains a dot' do + let(:state_name) { 'state-name-with-dot' } + let(:state_name_with_dot) { "#{state_name}.tfstate" } + let(:state_path) { "/projects/#{project_id}/terraform/state/#{state_name_with_dot}" } + + before do + stub_feature_flags(allow_dots_on_tf_state_names: false) + end + + it_behaves_like 'schedules the state for deletion' + end + + context 'with invalid state name' do + let(:state_name) { 'foo/bar' } + + it 'returns a 404 error' do + request + + expect(response).to have_gitlab_http_status(:not_found) + end + end it_behaves_like 'cannot access a state that is scheduled for deletion' end @@ -304,7 +432,7 @@ RSpec.describe API::Terraform::State, :snowplow do end end - describe 'PUT /projects/:id/terraform/state/:name/lock' do + describe 'POST /projects/:id/terraform/state/:name/lock' do let(:params) do { ID: '123-456', @@ -322,10 +450,14 @@ RSpec.describe API::Terraform::State, :snowplow do it_behaves_like 'endpoint with unique user tracking' it_behaves_like 'cannot access a state that is scheduled for deletion' - it 'locks the terraform state' do - request + context 'with invalid state name' do + let(:state_name) { 'foo/bar' } - expect(response).to have_gitlab_http_status(:ok) + it 'returns a 404 error' do + request + + expect(response).to have_gitlab_http_status(:not_found) + end end context 'state is already locked' do @@ -349,6 +481,47 @@ RSpec.describe API::Terraform::State, :snowplow do expect(response).to have_gitlab_http_status(:forbidden) end end + + where(given_state_name: %w[test-state test%2Ffoo]) + with_them do + let(:state_name) { given_state_name } + + it 'locks the terraform state' do + request + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'with a dot in the state name' do + let(:state_name) { 'test.state' } + + context 'with allow_dots_on_tf_state_names ff enabled' do + before do + stub_feature_flags(allow_dots_on_tf_state_names: true) + end + + let(:state_name) { 'test.state' } + + it 'locks the terraform state' do + request + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'with allow_dots_on_tf_state_names ff disabled' do + before do + stub_feature_flags(allow_dots_on_tf_state_names: false) + end + + it 'returns 404' do + request + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end end describe 'DELETE /projects/:id/terraform/state/:name/lock' do @@ -365,8 +538,9 @@ RSpec.describe API::Terraform::State, :snowplow do end before do - state.lock_xid = '123-456' + state.lock_xid = '123.456' state.save! + stub_feature_flags(allow_dots_on_tf_state_names: true) end subject(:request) { delete api("#{state_path}/lock"), headers: auth_header, params: params } @@ -379,28 +553,61 @@ RSpec.describe API::Terraform::State, :snowplow do let(:lock_id) { 'irrelevant to this test, just needs to be present' } end - context 'with the correct lock id' do - let(:lock_id) { '123-456' } + where(given_state_name: %w[test-state test.state test%2Ffoo]) + with_them do + let(:state_name) { given_state_name } - it 'removes the terraform state lock' do - request + context 'with the correct lock id' do + let(:lock_id) { '123.456' } - expect(response).to have_gitlab_http_status(:ok) + it 'removes the terraform state lock' do + request + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'with allow_dots_on_tf_state_names ff disabled' do + before do + stub_feature_flags(allow_dots_on_tf_state_names: false) + end + + context 'with dots in the state name' do + let(:lock_id) { '123.456' } + let(:state_name) { 'test.state' } + + it 'returns 404' do + request + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'with no lock id (force-unlock)' do + let(:params) { {} } + + it 'removes the terraform state lock' do + request + + expect(response).to have_gitlab_http_status(:ok) + end end end - context 'with no lock id (force-unlock)' do - let(:params) { {} } + context 'with invalid state name' do + let(:lock_id) { '123.456' } + let(:state_name) { 'foo/bar' } - it 'removes the terraform state lock' do + it 'returns a 404 error' do request - expect(response).to have_gitlab_http_status(:ok) + expect(response).to have_gitlab_http_status(:not_found) end end context 'with an incorrect lock id' do - let(:lock_id) { '456-789' } + let(:lock_id) { '456.789' } it 'returns an error' do request @@ -420,7 +627,7 @@ RSpec.describe API::Terraform::State, :snowplow do end context 'user does not have permission to unlock the state' do - let(:lock_id) { '123-456' } + let(:lock_id) { '123.456' } let(:current_user) { developer } it 'returns an error' do |