# frozen_string_literal: true require 'spec_helper' RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_category: :continuous_delivery do include ReactiveCachingHelpers using RSpec::Parameterized::TableSyntax include RepoHelpers include StubENV include CreateEnvironmentsHelpers let(:project) { create(:project, :repository) } subject(:environment) { create(:environment, project: project) } it { is_expected.to be_kind_of(ReactiveCaching) } it { is_expected.to nullify_if_blank(:external_url) } it { is_expected.to belong_to(:project).required } it { is_expected.to belong_to(:merge_request).optional } it { is_expected.to have_many(:deployments) } it { is_expected.to have_many(:metrics_dashboard_annotations) } it { is_expected.to have_many(:alert_management_alerts) } it { is_expected.to have_one(:upcoming_deployment) } it { is_expected.to have_one(:latest_opened_most_severe_alert) } it { is_expected.to delegate_method(:manual_actions).to(:last_deployment) } it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) } it { is_expected.to validate_length_of(:name).is_at_most(255) } it { is_expected.to validate_uniqueness_of(:slug).scoped_to(:project_id) } it { is_expected.to validate_length_of(:slug).is_at_most(24) } it { is_expected.to validate_length_of(:external_url).is_at_most(255) } describe 'validation' do it 'does not become invalid record when external_url is empty' do environment = build(:environment, external_url: nil) expect(environment).to be_valid end context 'does not allow changes to merge_request' do let(:merge_request) { create(:merge_request, source_project: project) } it 'for an environment that has no merge request associated' do environment = create(:environment) environment.merge_request = merge_request expect(environment).not_to be_valid end it 'for an environment that has a merge request associated' do environment = create(:environment, merge_request: merge_request) environment.merge_request = nil expect(environment).not_to be_valid end end context 'tier' do let!(:env) { build(:environment, tier: nil) } before do # Disable `before_validation: :ensure_environment_tier` since it always set tier and interfere with tests. # See: https://github.com/thoughtbot/shoulda/issues/178#issuecomment-1654014 allow_any_instance_of(described_class).to receive(:ensure_environment_tier).and_return(env) end context 'presence is checked' do it 'during create and update' do expect(env).to validate_presence_of(:tier).on(:create) expect(env).to validate_presence_of(:tier).on(:update) end end context 'when FF is disabled' do before do stub_feature_flags(validate_environment_tier_presence: false) end it { expect(env).to validate_presence_of(:tier).on(:create) } it { expect(env).not_to validate_presence_of(:tier).on(:update) } end end end describe 'validate and sanitize external url' do let_it_be_with_refind(:environment) { create(:environment) } where(:source_external_url, :expected_error_message) do nil | nil 'http://example.com' | nil 'example.com' | nil 'www.example.io' | nil 'http://$URL' | nil 'http://$(URL)' | nil 'custom://example.com' | nil '1.1.1.1' | nil '$BASE_URL/${CI_COMMIT_REF_NAME}' | nil '$ENVIRONMENT_URL' | nil 'https://$SUB.$MAIN' | nil 'https://$SUB-$REGION.$MAIN' | nil 'https://example.com?param={()}' | nil 'http://XSS?x=' | nil 'https://user:${VARIABLE}@example.io' | nil 'https://example.com/test?param={data}' | nil 'http://${URL}' | 'URI is invalid' 'https://${URL}.example/test' | 'URI is invalid' 'http://test${CI_MERGE_REQUEST_IID}.example.com' | 'URI is invalid' 'javascript:alert("hello")' | 'javascript scheme is not allowed' end with_them do it 'sets an external URL or an error' do environment.external_url = source_external_url environment.valid? if expected_error_message expect(environment.errors[:external_url].first).to eq(expected_error_message) else expect(environment.errors[:external_url]).to be_empty, "There were unexpected errors: #{environment.errors.full_messages}" expect(environment.external_url).to eq(source_external_url) end end end end describe '.before_save' do it 'ensures environment tier when a new object is created' do environment = build(:environment, name: 'gprd', tier: nil) expect { environment.save! }.to change { environment.tier }.from(nil).to('production') end it 'ensures environment tier when an existing object is updated' do environment = create(:environment, name: 'gprd') environment.update_column(:tier, nil) expect { environment.save! }.to change { environment.reload.tier }.from(nil).to('production') end it 'does not overwrite the existing environment tier' do environment = create(:environment, name: 'gprd', tier: :production) expect { environment.update!(name: 'gstg') }.not_to change { environment.reload.tier } end end describe '.order_by_last_deployed_at' do let!(:environment1) { create(:environment, project: project) } let!(:environment2) { create(:environment, project: project) } let!(:environment3) { create(:environment, project: project) } let!(:deployment1) { create(:deployment, environment: environment1) } let!(:deployment2) { create(:deployment, environment: environment2) } let!(:deployment3) { create(:deployment, environment: environment1) } it 'returns the environments in ascending order of having been last deployed' do expect(project.environments.order_by_last_deployed_at.to_a).to eq([environment3, environment2, environment1]) end it 'returns the environments in descending order of having been last deployed' do expect(project.environments.order_by_last_deployed_at_desc.to_a).to eq([environment1, environment2, environment3]) end end describe ".stopped_review_apps" do let_it_be(:project) { create(:project, :repository) } let_it_be(:old_stopped_review_env) { create(:environment, :with_review_app, :stopped, created_at: 31.days.ago, project: project) } let_it_be(:new_stopped_review_env) { create(:environment, :with_review_app, :stopped, project: project) } let_it_be(:old_active_review_env) { create(:environment, :with_review_app, :available, created_at: 31.days.ago, project: project) } let_it_be(:old_stopped_other_env) { create(:environment, :stopped, created_at: 31.days.ago, project: project) } let_it_be(:new_stopped_other_env) { create(:environment, :stopped, project: project) } let_it_be(:old_active_other_env) { create(:environment, :available, created_at: 31.days.ago, project: project) } let(:before) { 30.days.ago } let(:limit) { 1000 } subject { project.environments.stopped_review_apps(before, limit) } # rubocop: disable RSpec/SingleLineHook it { is_expected.to contain_exactly(old_stopped_review_env) } context "current timestamp" do let(:before) { Time.zone.now } it { is_expected.to contain_exactly(old_stopped_review_env, new_stopped_review_env) } end end describe "scheduled deletion" do let_it_be(:deletable_environment) { create(:environment, auto_delete_at: Time.zone.now) } let_it_be(:undeletable_environment) { create(:environment, auto_delete_at: nil) } describe ".scheduled_for_deletion" do subject { described_class.scheduled_for_deletion } it { is_expected.to contain_exactly(deletable_environment) } end describe ".not_scheduled_for_deletion" do subject { described_class.not_scheduled_for_deletion } it { is_expected.to contain_exactly(undeletable_environment) } end describe ".schedule_to_delete" do subject { described_class.id_in(deletable_environment).schedule_to_delete } it "schedules the record for deletion" do freeze_time do subject deletable_environment.reload undeletable_environment.reload expect(deletable_environment.auto_delete_at).to eq(1.week.from_now) expect(undeletable_environment.auto_delete_at).to be_nil end end end end describe 'state machine' do it 'invalidates the cache after a change' do expect(environment).to receive(:expire_etag_cache) environment.stop end context 'when environment has auto stop period' do let!(:environment) { create(:environment, :available, :auto_stoppable, project: project) } it 'clears auto stop period when the environment has stopped' do environment.stop! expect(environment.auto_stop_at).to be_nil end it 'does not clear auto stop period when the environment has not stopped' do expect(environment.auto_stop_at).to be_present end end end describe '.for_name_like' do subject { project.environments.for_name_like(query, limit: limit) } let!(:environment) { create(:environment, name: 'production', project: project) } let(:query) { 'pro' } let(:limit) { 5 } it 'returns a found name' do is_expected.to include(environment) end context 'when query is production' do let(:query) { 'production' } it 'returns a found name' do is_expected.to include(environment) end end context 'when query is productionA' do let(:query) { 'productionA' } it 'returns empty array' do is_expected.to be_empty end end context 'when query is empty' do let(:query) { '' } it 'returns a found name' do is_expected.to include(environment) end end context 'when query is nil' do let(:query) {} it 'raises an error' do expect { subject }.to raise_error(NoMethodError) end end context 'when query is partially matched in the middle of environment name' do let(:query) { 'duction' } it 'returns empty array' do is_expected.to be_empty end end context 'when query contains a wildcard character' do let(:query) { 'produc%' } it 'prevents wildcard injection' do is_expected.to be_empty end end end describe '.for_name_like_within_folder' do subject { project.environments.for_name_like_within_folder(query, limit: limit) } let!(:environment) { create(:environment, name: 'review/test-app', project: project) } let!(:environment_a) { create(:environment, name: 'test-app', project: project) } let(:query) { 'test' } let(:limit) { 5 } it 'returns a found name' do is_expected.to contain_exactly(environment) end it 'does not return environment without folder' do is_expected.not_to include(environment_a) end context 'when query is test-app' do let(:query) { 'test-app' } it 'returns a found name' do is_expected.to include(environment) end end context 'when query is test-app-a' do let(:query) { 'test-app-a' } it 'returns empty array' do is_expected.to be_empty end end context 'when query is empty string' do let(:query) { '' } let!(:environment_b) { create(:environment, name: 'review/test-app-1', project: project) } it 'returns only the foldered environments' do is_expected.to contain_exactly(environment, environment_b) end end context 'when query is nil' do let(:query) {} it 'raises an error' do expect { subject }.to raise_error(NoMethodError) end end context 'when query is partially matched in the middle of environment name' do let(:query) { 'app' } it 'returns empty array' do is_expected.to be_empty end end context 'when query contains a wildcard character' do let(:query) { 'test%' } it 'prevents wildcard injection' do is_expected.to be_empty end end end describe '.auto_stoppable' do subject { described_class.auto_stoppable(limit) } let(:limit) { 100 } context 'when environment is auto-stoppable' do let!(:environment) { create(:environment, :auto_stoppable) } it { is_expected.to eq([environment]) } end context 'when environment is not auto-stoppable' do let!(:environment) { create(:environment) } it { is_expected.to be_empty } end end describe '.auto_deletable' do subject { described_class.auto_deletable(limit) } let(:limit) { 100 } context 'when environment is auto-deletable' do let!(:environment) { create(:environment, :auto_deletable) } it { is_expected.to eq([environment]) } end context 'when environment is not auto-deletable' do let!(:environment) { create(:environment) } it { is_expected.to be_empty } end end describe '.pluck_names' do subject { described_class.pluck_names } let!(:environment) { create(:environment, name: 'production', project: project) } it 'plucks names' do is_expected.to eq(%w[production]) end end describe '.for_tier' do let_it_be(:environment) { create(:environment, :production) } it 'returns the production environment when searching for production tier' do expect(described_class.for_tier(:production)).to eq([environment]) end it 'returns nothing when searching for staging tier' do expect(described_class.for_tier(:staging)).to be_empty end end describe '.for_type' do it 'filters by type' do create(:environment) create(:environment, name: 'type1/prod') env = create(:environment, name: 'type2/prod') expect(described_class.for_type('type2')).to contain_exactly(env) end end describe '#guess_tier' do using RSpec::Parameterized::TableSyntax subject { environment.send(:guess_tier) } let(:environment) { build(:environment, name: name) } where(:name, :tier) do 'review/feature' | described_class.tiers[:development] 'review/product' | described_class.tiers[:development] 'DEV' | described_class.tiers[:development] 'development' | described_class.tiers[:development] 'trunk' | described_class.tiers[:development] 'dev' | described_class.tiers[:development] 'review/app' | described_class.tiers[:development] 'PRODUCTION' | described_class.tiers[:production] 'prod' | described_class.tiers[:production] 'prod-east-2' | described_class.tiers[:production] 'us-prod-east' | described_class.tiers[:production] 'fe-production' | described_class.tiers[:production] 'test' | described_class.tiers[:testing] 'TEST' | described_class.tiers[:testing] 'testing' | described_class.tiers[:testing] 'testing-prd' | described_class.tiers[:testing] 'acceptance-testing' | described_class.tiers[:testing] 'production-test' | described_class.tiers[:testing] 'test-production' | described_class.tiers[:testing] 'QC' | described_class.tiers[:testing] 'qa-env-2' | described_class.tiers[:testing] 'gstg' | described_class.tiers[:staging] 'staging' | described_class.tiers[:staging] 'stage' | described_class.tiers[:staging] 'Model' | described_class.tiers[:staging] 'MODL' | described_class.tiers[:staging] 'Pre-production' | described_class.tiers[:staging] 'pre' | described_class.tiers[:staging] 'Demo' | described_class.tiers[:staging] 'staging' | described_class.tiers[:staging] 'pre-prod' | described_class.tiers[:staging] 'blue-kit-stage' | described_class.tiers[:staging] 'nonprod' | described_class.tiers[:staging] 'nonlive' | described_class.tiers[:staging] 'non-prod' | described_class.tiers[:staging] 'non-live' | described_class.tiers[:staging] 'gprd' | described_class.tiers[:production] 'gprd-cny' | described_class.tiers[:production] 'production' | described_class.tiers[:production] 'Production' | described_class.tiers[:production] 'PRODUCTION' | described_class.tiers[:production] 'Production/eu' | described_class.tiers[:production] 'production/eu' | described_class.tiers[:production] 'PRODUCTION/EU' | described_class.tiers[:production] 'productioneu' | described_class.tiers[:production] 'store-produce' | described_class.tiers[:production] 'unproductive' | described_class.tiers[:production] 'production/www.gitlab.com' | described_class.tiers[:production] 'prod' | described_class.tiers[:production] 'PROD' | described_class.tiers[:production] 'Live' | described_class.tiers[:production] 'canary' | described_class.tiers[:other] 'other' | described_class.tiers[:other] 'EXP' | described_class.tiers[:other] 'something-else' | described_class.tiers[:other] end with_them do it { is_expected.to eq(tier) } end end describe '#expire_etag_cache' do let(:store) { Gitlab::EtagCaching::Store.new } it 'changes the cached value' do old_value = store.get(environment.etag_cache_key) environment.stop expect(store.get(environment.etag_cache_key)).not_to eq(old_value) end end describe '.with_deployment' do subject { described_class.with_deployment(sha, status: status) } let(:environment) { create(:environment, project: project) } let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' } let(:status) { nil } context 'when deployment has the specified sha' do let!(:deployment) { create(:deployment, environment: environment, sha: sha) } it { is_expected.to eq([environment]) } context 'with success status filter' do let(:status) { :success } it { is_expected.to be_empty } end context 'with created status filter' do let(:status) { :created } it { is_expected.to contain_exactly(environment) } end end context 'when deployment does not have the specified sha' do let!(:deployment) { create(:deployment, environment: environment, sha: 'ddd0f15ae83993f5cb66a927a28673882e99100b') } it { is_expected.to be_empty } end end describe '#folder_name' do context 'when it is inside a folder' do subject(:environment) do create(:environment, name: 'staging/review-1', project: project) end it 'returns a top-level folder name' do expect(environment.folder_name).to eq 'staging' end end context 'when the environment if a top-level item itself' do subject(:environment) do create(:environment, name: 'production') end it 'returns an environment name' do expect(environment.folder_name).to eq 'production' end end end describe '#name_without_type' do context 'when it is inside a folder' do subject(:environment) do create(:environment, name: 'staging/review-1') end it 'returns name without folder' do expect(environment.name_without_type).to eq 'review-1' end end context 'when the environment if a top-level item itself' do subject(:environment) do create(:environment, name: 'production') end it 'returns full name' do expect(environment.name_without_type).to eq 'production' end end end describe '#includes_commit?' do let(:project) { create(:project, :repository) } context 'without a last deployment' do it "returns false" do expect(environment.includes_commit?('HEAD')).to be false end end context 'with a last deployment' do let!(:deployment) do create(:deployment, :success, environment: environment, sha: project.commit('master').id) end context 'in the same branch' do it 'returns true' do expect(environment.includes_commit?(RepoHelpers.sample_commit.id)).to be true end end context 'not in the same branch' do before do deployment.update!(sha: project.commit('feature').id) end it 'returns false' do expect(environment.includes_commit?(RepoHelpers.sample_commit.id)).to be false end end end end describe '#environment_type' do subject { environment.environment_type } it 'sets a environment type if name has multiple segments' do environment.update!(name: 'production/worker.gitlab.com') is_expected.to eq('production') end it 'nullifies a type if it\'s a simple name' do environment.update!(name: 'production') is_expected.to be_nil end end describe '#stop_actions_available?' do subject { environment.stop_actions_available? } context 'when no other actions' do it { is_expected.to be_falsey } end context 'when matching action is defined' do let(:build) { create(:ci_build, :success) } let!(:deployment) do create( :deployment, :success, environment: environment, deployable: build, on_stop: 'close_app' ) end let!(:close_action) do create(:ci_build, :manual, pipeline: build.pipeline, name: 'close_app') end context 'when environment is available' do before do environment.start end it { is_expected.to be_truthy } end context 'when environment is stopped' do before do environment.stop end it { is_expected.to be_falsey } end end end describe '#stop_with_actions!' do let(:user) { create(:user) } subject { environment.stop_with_actions!(user) } before do expect(environment).to receive(:available?).and_call_original end context 'when no other actions' do context 'environment is available' do before do environment.update!(state: :available) end it do actions = subject expect(environment).to be_stopped expect(actions).to match_array([]) end end context 'environment is already stopped' do before do environment.update!(state: :stopped) end it do subject expect(environment).to be_stopped end end end context 'when matching action is defined' do let(:pipeline) { create(:ci_pipeline, project: project) } let(:build_a) { create(:ci_build, :success, pipeline: pipeline) } before do create(:deployment, :success, environment: environment, deployable: build_a, on_stop: 'close_app_a') end context 'when user is not allowed to stop environment' do let!(:close_action) do create(:ci_build, :manual, pipeline: pipeline, name: 'close_app_a') end it 'raises an exception' do expect { subject }.to raise_error(Gitlab::Access::AccessDeniedError) end end context 'when user is allowed to stop environment' do before do project.add_developer(user) create(:protected_branch, :developers_can_merge, name: 'master', project: project) end context 'when action did not yet finish' do let!(:close_action) do create(:ci_build, :manual, pipeline: pipeline, name: 'close_app_a') end it 'returns the same action' do action = subject.first expect(action).to eq(close_action) expect(action.user).to eq(user) end it 'environment is not stopped' do subject expect(environment).not_to be_stopped end end context 'if action did finish' do let!(:close_action) do create(:ci_build, :manual, :success, pipeline: pipeline, name: 'close_app_a') end it 'returns a new action of the same type' do action = subject.first expect(action).to be_persisted expect(action.name).to eq(close_action.name) expect(action.user).to eq(user) end end context 'close action does not raise ActiveRecord::StaleObjectError' do let!(:close_action) do create(:ci_build, :manual, pipeline: pipeline, name: 'close_app_a') end before do # preload the build environment.stop_actions # Update record as the other process. This makes `environment.stop_action` stale. close_action.drop! end it 'successfully plays the build even if the build was a stale object' do # Since build is droped. expect(close_action.processed).to be_falsey # it encounters the StaleObjectError at first, but reloads the object and runs `build.play` expect { subject }.not_to raise_error # Now the build should be processed. expect(close_action.reload.processed).to be_truthy end end end end context 'when there are more then one stop action for the environment' do let(:pipeline) { create(:ci_pipeline, project: project) } let(:build_a) { create(:ci_build, :success, pipeline: pipeline) } let(:build_b) { create(:ci_build, :success, pipeline: pipeline) } let!(:close_actions) do [ create(:ci_build, :manual, pipeline: pipeline, name: 'close_app_a'), create(:ci_build, :manual, pipeline: pipeline, name: 'close_app_b') ] end before do project.add_developer(user) create(:deployment, :success, environment: environment, deployable: build_a, finished_at: 5.minutes.ago, on_stop: 'close_app_a') create(:deployment, :success, environment: environment, deployable: build_b, finished_at: 1.second.ago, on_stop: 'close_app_b') end it 'returns the same actions' do actions = subject expect(actions.count).to eq(close_actions.count) expect(actions.pluck(:id)).to match_array(close_actions.pluck(:id)) expect(actions.pluck(:user)).to match_array(close_actions.pluck(:user)) end context 'when there are failed builds' do before do create(:ci_build, :failed, pipeline: pipeline, name: 'close_app_c') create(:deployment, :failed, environment: environment, deployable: create(:ci_build, pipeline: pipeline), on_stop: 'close_app_c') end it 'returns only stop actions from successful builds' do actions = subject expect(actions).to match_array(close_actions) expect(actions.count).to eq(pipeline.latest_successful_builds.count) end end end end describe '#stop_actions' do subject { environment.stop_actions } context 'when there are no deployments and builds' do it 'returns empty array' do is_expected.to match_array([]) end end context 'when there are multiple deployments with actions' do let(:pipeline) { create(:ci_pipeline, project: project) } let(:ci_build_a) { create(:ci_build, :success, project: project, pipeline: pipeline) } let(:ci_build_b) { create(:ci_build, :success, project: project, pipeline: pipeline) } let!(:ci_build_c) { create(:ci_build, :manual, project: project, pipeline: pipeline, name: 'close_app_a') } let!(:ci_build_d) { create(:ci_build, :manual, project: project, pipeline: pipeline, name: 'close_app_b') } let!(:deployment_a) do create(:deployment, :success, project: project, environment: environment, deployable: ci_build_a, on_stop: 'close_app_a') end let!(:deployment_b) do create(:deployment, :success, project: project, environment: environment, deployable: ci_build_b, on_stop: 'close_app_b') end before do # Create failed deployment without stop_action. build = create(:ci_build, :failed, project: project, pipeline: pipeline) create(:deployment, :failed, project: project, environment: environment, deployable: build) end it 'returns only the stop actions' do expect(subject.pluck(:id)).to contain_exactly(ci_build_c.id, ci_build_d.id) end end end describe '#last_deployment_group' do subject { environment.last_deployment_group } context 'when there are no deployments and builds' do it do is_expected.to eq(Deployment.none) end end context 'when there are deployments for multiple pipelines' do let(:pipeline_a) { create(:ci_pipeline, project: project) } let(:pipeline_b) { create(:ci_pipeline, project: project) } let(:ci_build_a) { create(:ci_build, :success, project: project, pipeline: pipeline_a) } let(:ci_build_b) { create(:ci_build, :failed, project: project, pipeline: pipeline_b) } let(:ci_build_c) { create(:ci_build, :success, project: project, pipeline: pipeline_a) } let(:ci_build_d) { create(:ci_build, :failed, project: project, pipeline: pipeline_a) } # Successful deployments for pipeline_a let!(:deployment_a) { create(:deployment, :success, project: project, environment: environment, deployable: ci_build_a) } let!(:deployment_b) { create(:deployment, :success, project: project, environment: environment, deployable: ci_build_c) } before do # Failed deployment for pipeline_a create(:deployment, :failed, project: project, environment: environment, deployable: ci_build_d) # Failed deployment for pipeline_b create(:deployment, :failed, project: project, environment: environment, deployable: ci_build_b) end it 'returns the successful deployment jobs for the last deployment pipeline' do expect(subject.pluck(:id)).to contain_exactly(deployment_a.id, deployment_b.id) end end end describe 'recently_updated_on_branch?' do subject { environment.recently_updated_on_branch?('feature') } context 'when last deployment to environment is the most recent one' do before do create(:deployment, :success, environment: environment, ref: 'feature') end it { is_expected.to be true } end context 'when last deployment to environment is not the most recent' do before do create(:deployment, :success, environment: environment, ref: 'feature') create(:deployment, :success, environment: environment, ref: 'master') end it { is_expected.to be false } end end describe '#reset_auto_stop' do subject { environment.reset_auto_stop } let(:environment) { create(:environment, :auto_stoppable) } it 'nullifies the auto_stop_at' do expect { subject }.to change(environment, :auto_stop_at).from(Time).to(nil) end end describe '#actions_for' do let(:deployment) { create(:deployment, :success, environment: environment) } let(:pipeline) { deployment.deployable.pipeline } let!(:review_action) { create(:ci_build, :manual, name: 'review-apps', pipeline: pipeline, environment: 'review/$CI_COMMIT_REF_NAME') } let!(:production_action) { create(:ci_build, :manual, name: 'production', pipeline: pipeline, environment: 'production') } it 'returns a list of actions with matching environment' do expect(environment.actions_for('review/master')).to contain_exactly(review_action) end end describe '.deployments' do subject { environment.deployments } context 'when there is a deployment record with created status' do let(:deployment) { create(:deployment, :created, environment: environment) } it 'does not return the record' do is_expected.to be_empty end end context 'when there is a deployment record with running status' do let(:deployment) { create(:deployment, :running, environment: environment) } it 'does not return the record' do is_expected.to be_empty end end context 'when there is a deployment record with success status' do let(:deployment) { create(:deployment, :success, environment: environment) } it 'returns the record' do is_expected.to eq([deployment]) end end end describe '.last_deployment' do subject { environment.last_deployment } before do allow_next_instance_of(Deployment) do |instance| allow(instance).to receive(:create_ref) end end context 'when there is an old deployment record' do let!(:previous_deployment) { create(:deployment, :success, environment: environment) } context 'when there is a deployment record with created status' do let!(:deployment) { create(:deployment, environment: environment) } it 'returns the previous deployment' do is_expected.to eq(previous_deployment) end end context 'when there is a deployment record with running status' do let!(:deployment) { create(:deployment, :running, environment: environment) } it 'returns the previous deployment' do is_expected.to eq(previous_deployment) end end context 'when there is a deployment record with failed status' do let!(:deployment) { create(:deployment, :failed, environment: environment) } it 'returns the previous deployment' do is_expected.to eq(previous_deployment) end end context 'when there is a deployment record with success status' do let!(:deployment) { create(:deployment, :success, environment: environment) } let!(:old_deployment) { create(:deployment, :success, environment: environment, finished_at: 2.days.ago) } it 'returns the latest successful deployment' do is_expected.to eq(deployment) end it 'returns the deployment with the latest finished_at' do expect(old_deployment.finished_at < deployment.finished_at).to be_truthy is_expected.to eq(deployment) end end end end describe 'Last deployment relations' do Deployment::FINISHED_STATUSES.each do |status| it "returns the last #{status} deployment" do create(:deployment, status.to_sym, environment: environment, finished_at: 1.day.ago) expected = create(:deployment, status.to_sym, environment: environment, finished_at: Time.current) expect(environment.public_send(:"last_#{status}_deployment")).to eq(expected) end end Deployment::UPCOMING_STATUSES.each do |status| it "returns the last #{status} deployment" do create(:deployment, status.to_sym, environment: environment) expected = create(:deployment, status.to_sym, environment: environment) expect(environment.public_send(:"last_#{status}_deployment")).to eq(expected) end end end describe '#last_deployable' do subject { environment.last_deployable } context 'does not join across databases' do let(:pipeline_a) { create(:ci_pipeline, project: project) } let(:pipeline_b) { create(:ci_pipeline, project: project) } let(:ci_build_a) { create(:ci_build, project: project, pipeline: pipeline_a) } let(:ci_build_b) { create(:ci_build, project: project, pipeline: pipeline_b) } before do create(:deployment, :success, project: project, environment: environment, deployable: ci_build_a) create(:deployment, :failed, project: project, environment: environment, deployable: ci_build_b) end it 'when called' do with_cross_joins_prevented do expect(subject.id).to eq(ci_build_a.id) end end end end describe '#last_deployment_pipeline' do subject { environment.last_deployment_pipeline } let(:pipeline_a) { create(:ci_pipeline, project: project) } let(:pipeline_b) { create(:ci_pipeline, project: project) } let(:ci_build_a) { create(:ci_build, project: project, pipeline: pipeline_a) } let(:ci_build_b) { create(:ci_build, project: project, pipeline: pipeline_b) } before do create(:deployment, :success, project: project, environment: environment, deployable: ci_build_a) create(:deployment, :failed, project: project, environment: environment, deployable: ci_build_b) end it 'does not join across databases' do with_cross_joins_prevented do expect(subject.id).to eq(pipeline_a.id) end end end describe '#last_visible_deployment' do subject { environment.last_visible_deployment } before do allow_any_instance_of(Deployment).to receive(:create_ref) end context 'when there is an old deployment record' do let!(:previous_deployment) { create(:deployment, :success, environment: environment) } context 'when there is a deployment record with created status' do let!(:deployment) { create(:deployment, environment: environment) } it { is_expected.to eq(previous_deployment) } end context 'when there is a deployment record with running status' do let!(:deployment) { create(:deployment, :running, environment: environment) } it { is_expected.to eq(deployment) } end context 'when there is a deployment record with success status' do let!(:deployment) { create(:deployment, :success, environment: environment) } it { is_expected.to eq(deployment) } end context 'when there is a deployment record with failed status' do let!(:deployment) { create(:deployment, :failed, environment: environment) } it { is_expected.to eq(deployment) } end context 'when there is a deployment record with canceled status' do let!(:deployment) { create(:deployment, :canceled, environment: environment) } it { is_expected.to eq(deployment) } end end end describe '#last_visible_deployable' do subject { environment.last_visible_deployable } let!(:deployment) do create(:deployment, :success, project: project, environment: environment, deployable: deployable) end let!(:deployable) { create(:ci_build, :success, project: project) } it 'fetches the deployable through the last visible deployment' do is_expected.to eq(deployable) end end describe '#last_visible_pipeline' do subject { environment.last_visible_pipeline } let!(:deployment) do create(:deployment, :success, project: project, environment: environment, deployable: deployable) end let!(:deployable) { create(:ci_build, :success, project: project, pipeline: pipeline) } let!(:pipeline) { create(:ci_pipeline, :success, project: project) } it 'fetches the pipeline through the last visible deployment' do is_expected.to eq(pipeline) end end describe '#upcoming_deployment' do subject { environment.upcoming_deployment } context 'when environment has a successful deployment' do let!(:deployment) { create(:deployment, :success, environment: environment, project: project) } it { is_expected.to be_nil } end context 'when environment has a running deployment' do let!(:deployment) { create(:deployment, :running, environment: environment, project: project) } it { is_expected.to eq(deployment) } end context 'when environment has a blocked deployment' do let!(:deployment) { create(:deployment, :blocked, environment: environment, project: project) } it { is_expected.to eq(deployment) } end end describe '#has_terminals?' do subject { environment.has_terminals? } context 'when the environment is available' do context 'with a deployment service' do context 'when user configured kubernetes from CI/CD > Clusters' do let!(:cluster) { create(:cluster, :project, :provided_by_gcp, projects: [project]) } context 'with deployment' do let!(:deployment) { create(:deployment, :success, environment: environment) } it { is_expected.to be_truthy } end context 'without deployments' do it { is_expected.to be_falsy } end end end context 'without a deployment service' do it { is_expected.to be_falsy } end end context 'when the environment is unavailable' do before do environment.stop end it { is_expected.to be_falsy } end end describe '#deployment_platform' do context 'when there is a deployment platform for environment' do let!(:cluster) do create(:cluster, :provided_by_gcp, environment_scope: '*', projects: [project]) end it 'finds a deployment platform' do expect(environment.deployment_platform).to eq cluster.platform end end context 'when there is no deployment platform for environment' do it 'returns nil' do expect(environment.deployment_platform).to be_nil end end it 'checks deployment platforms associated with a project' do expect(project).to receive(:deployment_platform) .with(environment: environment.name) environment.deployment_platform end end describe '#deployment_namespace' do let(:environment) { create(:environment) } subject { environment.deployment_namespace } before do allow(environment).to receive(:deployment_platform).and_return(deployment_platform) end context 'no deployment platform available' do let(:deployment_platform) { nil } it { is_expected.to be_nil } end context 'deployment platform is available' do let(:cluster) { create(:cluster, :provided_by_user, :project, projects: [environment.project]) } let(:deployment_platform) { cluster.platform } it 'retrieves a namespace from the cluster' do expect(cluster).to receive(:kubernetes_namespace_for) .with(environment).and_return('mock-namespace') expect(subject).to eq 'mock-namespace' end end end describe '#terminals' do subject { environment.terminals } before do allow(environment).to receive(:deployment_platform).and_return(double) end context 'reactive cache configuration' do it 'does not continue to spawn jobs' do expect(described_class.reactive_cache_lifetime).to be < described_class.reactive_cache_refresh_interval end end context 'reactive cache is empty' do before do stub_reactive_cache(environment, nil) end it { is_expected.to be_nil } end context 'reactive cache has pod data' do let(:cache_data) { Hash(pods: %w(pod1 pod2)) } before do stub_reactive_cache(environment, cache_data) end it 'retrieves terminals from the deployment platform' do expect(environment.deployment_platform) .to receive(:terminals).with(environment, cache_data) .and_return(:fake_terminals) is_expected.to eq(:fake_terminals) end end end describe '#calculate_reactive_cache' do let!(:cluster) { create(:cluster, :project, :provided_by_user, projects: [project]) } let!(:environment) { create(:environment, project: project) } let!(:deployment) { create(:deployment, :success, environment: environment, project: project) } subject { environment.calculate_reactive_cache } it 'overrides default reactive_cache_hard_limit to 10 Mb' do expect(described_class.reactive_cache_hard_limit).to eq(10.megabyte) end it 'returns cache data from the deployment platform' do expect(environment.deployment_platform).to receive(:calculate_reactive_cache_for) .with(environment).and_return(pods: %w(pod1 pod2)) is_expected.to eq(pods: %w(pod1 pod2)) end context 'environment does not have terminals available' do before do allow(environment).to receive(:has_terminals?).and_return(false) end it { is_expected.to be_nil } end context 'project is pending deletion' do before do allow(environment.project).to receive(:pending_delete?).and_return(true) end it { is_expected.to be_nil } end end describe '#has_metrics?' do subject { environment.has_metrics? } context 'when the environment is available' do context 'with a deployment service' do let_it_be(:project) { create(:project, :with_prometheus_integration, :repository) } context 'and a deployment' do let!(:deployment) { create(:deployment, environment: environment) } it { is_expected.to be_truthy } end context 'and no deployments' do it { is_expected.to be_truthy } end context 'and the prometheus adapter is not configured' do before do allow(environment.prometheus_adapter).to receive(:configured?).and_return(false) end it { is_expected.to be_falsy } end end context 'without a monitoring service' do it { is_expected.to be_falsy } end context 'when sample metrics are enabled' do before do stub_env('USE_SAMPLE_METRICS', 'true') end context 'with no prometheus adapter configured' do before do allow(environment.prometheus_adapter).to receive(:configured?).and_return(false) end it { is_expected.to be_truthy } end end end describe '#has_sample_metrics?' do subject { environment.has_metrics? } let(:project) { create(:project) } context 'when sample metrics are enabled' do before do stub_env('USE_SAMPLE_METRICS', 'true') end context 'with no prometheus adapter configured' do before do allow(environment.prometheus_adapter).to receive(:configured?).and_return(false) end it { is_expected.to be_truthy } end context 'with the environment stopped' do before do environment.stop end it { is_expected.to be_falsy } end end context 'when sample metrics are not enabled' do it { is_expected.to be_falsy } end end context 'when the environment is unavailable' do let_it_be(:project) { create(:project, :with_prometheus_integration) } before do environment.stop end it { is_expected.to be_falsy } end end describe '#has_running_deployments?' do subject { environment.has_running_deployments? } it 'return false when no deployments exist' do is_expected.to eq(false) end context 'when deployment is running on the environment' do let!(:deployment) { create(:deployment, :running, environment: environment) } it 'return true' do is_expected.to eq(true) end end end describe '#metrics' do let_it_be(:project) { create(:project, :with_prometheus_integration) } subject { environment.metrics } context 'when the environment has metrics' do before do allow(environment).to receive(:has_metrics?).and_return(true) end it 'returns the metrics from the deployment service' do expect(environment.prometheus_adapter) .to receive(:query).with(:environment, environment) .and_return(:fake_metrics) is_expected.to eq(:fake_metrics) end context 'and the prometheus client is not present' do before do allow(environment.prometheus_adapter).to receive(:promethus_client).and_return(nil) end it { is_expected.to be_nil } end end context 'when the environment does not have metrics' do before do allow(environment).to receive(:has_metrics?).and_return(false) end it { is_expected.to be_nil } end end describe '#additional_metrics' do let_it_be(:project) { create(:project, :with_prometheus_integration) } let(:metric_params) { [] } subject { environment.additional_metrics(*metric_params) } context 'when the environment has additional metrics' do before do allow(environment).to receive(:has_metrics?).and_return(true) end it 'returns the additional metrics from the deployment service' do expect(environment.prometheus_adapter) .to receive(:query) .with(:additional_metrics_environment, environment) .and_return(:fake_metrics) is_expected.to eq(:fake_metrics) end context 'when time window arguments are provided' do let(:metric_params) { [1552642245.067, Time.current] } it 'queries with the expected parameters' do expect(environment.prometheus_adapter) .to receive(:query) .with(:additional_metrics_environment, environment, *metric_params.map(&:to_f)) .and_return(:fake_metrics) is_expected.to eq(:fake_metrics) end end end context 'when the environment does not have metrics' do before do allow(environment).to receive(:has_metrics?).and_return(false) end it { is_expected.to be_nil } end end describe '#slug' do it "is automatically generated" do expect(environment.slug).not_to be_nil end it "is not regenerated if name changes" do original_slug = environment.slug environment.update!(name: environment.name.reverse) expect(environment.slug).to eq(original_slug) end it "regenerates the slug if nil" do environment = build(:environment, slug: nil) new_slug = environment.slug expect(new_slug).not_to be_nil expect(environment.slug).to eq(new_slug) end end describe '#ref_path' do subject(:environment) do create(:environment, name: 'staging / review-1') end it 'returns a path that uses the slug and does not have spaces' do expect(environment.ref_path).to start_with('refs/environments/staging-review-1-') end it "doesn't change when the slug is nil initially" do environment.slug = nil expect(environment.ref_path).to eq(environment.ref_path) end end describe '#external_url_for' do let(:source_path) { 'source/file.html' } let(:sha) { RepoHelpers.sample_commit.id } context 'when the public path is not known' do before do environment.external_url = 'http://example.com' allow(project).to receive(:public_path_for_source_path).with(source_path, sha).and_return(nil) end it 'returns nil' do expect(environment.external_url_for(source_path, sha)).to be_nil end end context 'when the public path is known' do where(:external_url, :public_path, :full_url) do 'http://example.com' | 'file.html' | 'http://example.com/file.html' 'http://example.com/' | 'file.html' | 'http://example.com/file.html' 'http://example.com' | '/file.html' | 'http://example.com/file.html' 'http://example.com/' | '/file.html' | 'http://example.com/file.html' 'http://example.com/subpath' | 'public/file.html' | 'http://example.com/subpath/public/file.html' 'http://example.com/subpath/' | 'public/file.html' | 'http://example.com/subpath/public/file.html' 'http://example.com/subpath' | '/public/file.html' | 'http://example.com/subpath/public/file.html' 'http://example.com/subpath/' | '/public/file.html' | 'http://example.com/subpath/public/file.html' end with_them do it 'returns the full external URL' do environment.external_url = external_url allow(project).to receive(:public_path_for_source_path).with(source_path, sha).and_return(public_path) expect(environment.external_url_for(source_path, sha)).to eq(full_url) end end end end describe '#prometheus_adapter' do it 'calls prometheus adapter service' do expect_next_instance_of(Gitlab::Prometheus::Adapter) do |instance| expect(instance).to receive(:prometheus_adapter) end subject.prometheus_adapter end end describe '#knative_services_finder' do let(:environment) { create(:environment) } subject { environment.knative_services_finder } context 'environment has no deployments' do it { is_expected.to be_nil } end context 'environment has a deployment' do let!(:deployment) { create(:deployment, :success, environment: environment, cluster: cluster) } context 'with no cluster associated' do let(:cluster) { nil } it { is_expected.to be_nil } end context 'with a cluster associated' do let(:cluster) { create(:cluster) } it 'calls the service finder' do expect(Clusters::KnativeServicesFinder).to receive(:new) .with(cluster, environment).and_return(:finder) is_expected.to eq :finder end end end end describe '#auto_stop_in' do subject { environment.auto_stop_in } context 'when environment will be expired' do let(:environment) { build(:environment, :will_auto_stop) } it 'returns when it will expire' do freeze_time { is_expected.to eq(1.day.to_i) } end end context 'when environment is not expired' do let(:environment) { build(:environment) } it { is_expected.to be_nil } end end describe '#auto_stop_in=' do subject { environment.auto_stop_in = value } let(:environment) { build(:environment) } where(:value, :expected_result) do '2 days' | 2.days.to_i '1 week' | 1.week.to_i '2h20min' | 2.hours.to_i + 20.minutes.to_i 'abcdef' | ChronicDuration::DurationParseError '' | nil nil | nil 'never' | nil end with_them do shared_examples 'for given values expected result is set' do it do freeze_time do if expected_result.is_a?(Integer) || expected_result.nil? subject expect(environment.auto_stop_in).to eq(expected_result) else expect { subject }.to raise_error(expected_result) end end end end context 'new assignment sets correct auto_stop_in' do include_examples 'for given values expected result is set' end context 'resets older value' do let(:environment) { create(:environment, auto_stop_at: 1.day.since.round) } include_examples 'for given values expected result is set' end end end describe '.for_id_and_slug' do subject { described_class.for_id_and_slug(environment.id, environment.slug) } let(:environment) { create(:environment) } it { is_expected.not_to be_nil } end describe '.find_or_create_by_name' do it 'finds an existing environment if it exists' do env = create(:environment) expect(described_class.find_or_create_by_name(env.name)).to eq(env) end it 'creates an environment if it does not exist' do env = project.environments.find_or_create_by_name('kittens') expect(env).to be_an_instance_of(described_class) expect(env).to be_persisted end end describe '#destroy' do it 'remove the deployment refs from gitaly' do deployment = create(:deployment, :success, environment: environment, project: project) deployment.create_ref expect { environment.destroy! }.to change { project.commit(deployment.ref_path) }.to(nil) end end describe '.count_by_state' do context 'when environments are not empty' do let!(:environment1) { create(:environment, project: project, state: 'stopped') } let!(:environment2) { create(:environment, project: project, state: 'available') } let!(:environment3) { create(:environment, project: project, state: 'stopped') } it 'returns the environments count grouped by state' do expect(project.environments.count_by_state).to eq({ stopped: 2, available: 1, stopping: 0 }) end it 'returns the environments count grouped by state with zero value' do environment2.update!(state: 'stopped') expect(project.environments.count_by_state).to eq({ stopped: 3, available: 0, stopping: 0 }) end end it 'returns zero state counts when environments are empty' do expect(project.environments.count_by_state).to eq({ stopped: 0, available: 0, stopping: 0 }) end end describe '#has_opened_alert?' do subject { environment.has_opened_alert? } let_it_be(:project) { create(:project) } let_it_be(:environment, reload: true) { create(:environment, project: project) } context 'when environment has an triggered alert' do let!(:alert) { create(:alert_management_alert, :triggered, project: project, environment: environment) } it { is_expected.to be(true) } end context 'when environment has an resolved alert' do let!(:alert) { create(:alert_management_alert, :resolved, project: project, environment: environment) } it { is_expected.to be(false) } end context 'when environment does not have an alert' do it { is_expected.to be(false) } end end describe '#cancel_deployment_jobs!' do subject { environment.cancel_deployment_jobs! } let_it_be(:project) { create(:project, :repository) } let_it_be(:environment, reload: true) { create(:environment, project: project) } let!(:deployment) { create(:deployment, project: project, environment: environment, deployable: build) } let!(:build) { create(:ci_build, :running, project: project, environment: environment) } it 'cancels an active deployment job' do subject expect(build.reset).to be_canceled end context 'when deployable does not exist' do before do deployment.update_column(:deployable_id, non_existing_record_id) end it 'does not raise an error' do expect { subject }.not_to raise_error expect(build.reset).to be_running end end end describe '#rollout_status' do let!(:cluster) { create(:cluster, :project, :provided_by_user, projects: [project]) } let!(:environment) { create(:environment, project: project) } let!(:deployment) { create(:deployment, :success, environment: environment, project: project) } subject { environment.rollout_status } context 'environment does not have a deployment board available' do before do allow(environment).to receive(:has_terminals?).and_return(false) end it { is_expected.to be_nil } end context 'cached rollout status is present' do let(:pods) { %w(pod1 pod2) } let(:deployments) { %w(deployment1 deployment2) } before do stub_reactive_cache(environment, pods: pods, deployments: deployments) end it 'fetches the rollout status from the deployment platform' do expect(environment.deployment_platform).to receive(:rollout_status) .with(environment, { pods: pods, deployments: deployments }) .and_return(:mock_rollout_status) is_expected.to eq(:mock_rollout_status) end end context 'cached rollout status is not present yet' do before do stub_reactive_cache(environment, nil) end it 'falls back to a loading status' do expect(::Gitlab::Kubernetes::RolloutStatus).to receive(:loading).and_return(:mock_loading_status) is_expected.to eq(:mock_loading_status) end end end describe '#ingresses' do subject { environment.ingresses } let(:deployment_platform) { double(:deployment_platform) } let(:deployment_namespace) { 'production' } before do allow(environment).to receive(:deployment_platform) { deployment_platform } allow(environment).to receive(:deployment_namespace) { deployment_namespace } end context 'when rollout status is available' do before do allow(environment).to receive(:rollout_status_available?) { true } end it 'fetches ingresses from the deployment platform' do expect(deployment_platform).to receive(:ingresses).with(deployment_namespace) subject end end context 'when rollout status is not available' do before do allow(environment).to receive(:rollout_status_available?) { false } end it 'does nothing' do expect(deployment_platform).not_to receive(:ingresses) subject end end end describe '#patch_ingress' do subject { environment.patch_ingress(ingress, data) } let(:ingress) { double(:ingress) } let(:data) { double(:data) } let(:deployment_platform) { double(:deployment_platform) } let(:deployment_namespace) { 'production' } before do allow(environment).to receive(:deployment_platform) { deployment_platform } allow(environment).to receive(:deployment_namespace) { deployment_namespace } end context 'when rollout status is available' do before do allow(environment).to receive(:rollout_status_available?) { true } end it 'fetches ingresses from the deployment platform' do expect(deployment_platform).to receive(:patch_ingress).with(deployment_namespace, ingress, data) subject end end context 'when rollout status is not available' do before do allow(environment).to receive(:rollout_status_available?) { false } end it 'does nothing' do expect(deployment_platform).not_to receive(:patch_ingress) subject end end end describe '#clear_all_caches' do subject { environment.clear_all_caches } it 'clears all caches on the environment' do expect_next_instance_of(Gitlab::EtagCaching::Store) do |store| expect(store).to receive(:touch).with(environment.etag_cache_key) end expect(environment).to receive(:clear_reactive_cache!) subject end end describe '#should_link_to_merge_requests?' do subject { environment.should_link_to_merge_requests? } context 'when environment is foldered' do context 'when environment is production tier' do let(:environment) { create(:environment, project: project, name: 'production/aws') } it { is_expected.to eq(true) } end context 'when environment is development tier' do let(:environment) { create(:environment, project: project, name: 'review/feature') } it { is_expected.to eq(false) } end end context 'when environment is unfoldered' do context 'when environment is production tier' do let(:environment) { create(:environment, project: project, name: 'production') } it { is_expected.to eq(true) } end context 'when environment is development tier' do let(:environment) { create(:environment, project: project, name: 'development') } it { is_expected.to eq(true) } end end end describe '#deploy_freezes' do let(:environment) { create(:environment, project: project, name: 'staging') } let(:freeze_period) { create(:ci_freeze_period, project: project) } subject { environment.deploy_freezes } it 'returns the freeze periods of the associated project' do expect(subject).to contain_exactly(freeze_period) end it 'caches the freeze periods' do expect(Gitlab::SafeRequestStore).to receive(:fetch) .at_least(:once) .and_return([freeze_period]) subject end end describe '#deployed_and_updated_before' do subject do described_class.deployed_and_updated_before(project_id, before) end let(:project_id) { project.id } let(:before) { 1.week.ago.to_date.to_s } let(:environment) { create(:environment, project: project, updated_at: 2.weeks.ago) } let!(:stale_deployment) { create(:deployment, environment: environment, updated_at: 2.weeks.ago) } it 'excludes environments with recent deployments' do create(:deployment, environment: environment, updated_at: Date.current) is_expected.to match_array([]) end it 'includes environments with no deployments' do environment1 = create(:environment, project: project, updated_at: 2.weeks.ago) is_expected.to match_array([environment, environment1]) end it 'excludes environments that have been recently updated with no deployments' do create(:environment, project: project) is_expected.to match_array([environment]) end it 'excludes environments that have been recently updated with stale deployments' do environment1 = create(:environment, project: project) create(:deployment, environment: environment1, updated_at: 2.weeks.ago) is_expected.to match_array([environment]) end end end