environments_controller_spec.rb 21.1 KB
Newer Older
1 2
# frozen_string_literal: true

3 4 5
require 'spec_helper'

describe Projects::EnvironmentsController do
Z.J. van de Weg's avatar
Z.J. van de Weg committed
6
  set(:user) { create(:user) }
7
  set(:project) { create(:project) }
8

Z.J. van de Weg's avatar
Z.J. van de Weg committed
9
  set(:environment) do
10
    create(:environment, name: 'production', project: project)
11
  end
12 13

  before do
14
    project.add_maintainer(user)
15 16 17 18

    sign_in(user)
  end

19
  describe 'GET index' do
Z.J. van de Weg's avatar
Z.J. van de Weg committed
20
    context 'when a request for the HTML is made' do
21
      it 'responds with status code 200' do
blackst0ne's avatar
blackst0ne committed
22
        get :index, params: environment_params
23

24
        expect(response).to have_gitlab_http_status(:ok)
25
      end
26 27 28 29 30

      it 'expires etag cache to force reload environments list' do
        expect_any_instance_of(Gitlab::EtagCaching::Store)
          .to receive(:touch).with(project_environments_path(project, format: :json))

blackst0ne's avatar
blackst0ne committed
31
        get :index, params: environment_params
32
      end
33 34
    end

35 36
    context 'when requesting JSON response for folders' do
      before do
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
        create(:environment, project: project,
                             name: 'staging/review-1',
                             state: :available)

        create(:environment, project: project,
                             name: 'staging/review-2',
                             state: :available)

        create(:environment, project: project,
                             name: 'staging/review-3',
                             state: :stopped)
      end

      let(:environments) { json_response['environments'] }

52 53 54 55 56 57 58
      context 'with default parameters' do
        before do
          get :index, params: environment_params(format: :json)
        end

        it 'responds with a flat payload describing available environments' do
          expect(environments.count).to eq 3
59 60 61
          expect(environments.first).to include('name' => 'production', 'name_without_type' => 'production')
          expect(environments.second).to include('name' => 'staging/review-1', 'name_without_type' => 'review-1')
          expect(environments.third).to include('name' => 'staging/review-2', 'name_without_type' => 'review-2')
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
          expect(json_response['available_count']).to eq 3
          expect(json_response['stopped_count']).to eq 1
        end

        it 'sets the polling interval header' do
          expect(response).to have_gitlab_http_status(:ok)
          expect(response.headers['Poll-Interval']).to eq("3000")
        end
      end

      context 'when a folder-based nested structure is requested' do
        before do
          get :index, params: environment_params(format: :json, nested: true)
        end

        it 'responds with a payload containing the latest environment for each folder' do
          expect(environments.count).to eq 2
          expect(environments.first['name']).to eq 'production'
          expect(environments.second['name']).to eq 'staging'
          expect(environments.second['size']).to eq 2
          expect(environments.second['latest']['name']).to eq 'staging/review-2'
        end
      end

86 87
      context 'when requesting available environments scope' do
        before do
88
          get :index, params: environment_params(format: :json, nested: true, scope: :available)
89 90 91 92 93 94 95 96 97 98 99 100 101 102
        end

        it 'responds with a payload describing available environments' do
          expect(environments.count).to eq 2
          expect(environments.first['name']).to eq 'production'
          expect(environments.second['name']).to eq 'staging'
          expect(environments.second['size']).to eq 2
          expect(environments.second['latest']['name']).to eq 'staging/review-2'
        end

        it 'contains values describing environment scopes sizes' do
          expect(json_response['available_count']).to eq 3
          expect(json_response['stopped_count']).to eq 1
        end
103 104
      end

105 106
      context 'when requesting stopped environments scope' do
        before do
107
          get :index, params: environment_params(format: :json, nested: true, scope: :stopped)
108 109 110 111 112 113 114 115
        end

        it 'responds with a payload describing stopped environments' do
          expect(environments.count).to eq 1
          expect(environments.first['name']).to eq 'staging'
          expect(environments.first['size']).to eq 1
          expect(environments.first['latest']['name']).to eq 'staging/review-3'
        end
116

117 118 119 120
        it 'contains values describing environment scopes sizes' do
          expect(json_response['available_count']).to eq 3
          expect(json_response['stopped_count']).to eq 1
        end
121 122 123 124
      end
    end
  end

125 126 127 128 129
  describe 'GET folder' do
    before do
      create(:environment, project: project,
                           name: 'staging-1.0/review',
                           state: :available)
130
      create(:environment, project: project,
131
                           name: 'staging-1.0/zzz',
132
                           state: :available)
133 134 135 136
    end

    context 'when using default format' do
      it 'responds with HTML' do
blackst0ne's avatar
blackst0ne committed
137 138 139 140 141
        get :folder, params: {
                       namespace_id: project.namespace,
                       project_id: project,
                       id: 'staging-1.0'
                     }
142 143 144 145 146 147 148

        expect(response).to be_ok
        expect(response).to render_template 'folder'
      end
    end

    context 'when using JSON format' do
Z.J. van de Weg's avatar
Z.J. van de Weg committed
149
      it 'sorts the subfolders lexicographically' do
blackst0ne's avatar
blackst0ne committed
150 151 152 153 154
        get :folder, params: {
                       namespace_id: project.namespace,
                       project_id: project,
                       id: 'staging-1.0'
                     },
155 156 157 158 159
                     format: :json

        expect(response).to be_ok
        expect(response).not_to render_template 'folder'
        expect(json_response['environments'][0])
160
          .to include('name' => 'staging-1.0/review', 'name_without_type' => 'review')
161
        expect(json_response['environments'][1])
162
          .to include('name' => 'staging-1.0/zzz', 'name_without_type' => 'zzz')
163 164 165 166
      end
    end
  end

167 168 169
  describe 'GET show' do
    context 'with valid id' do
      it 'responds with a status code 200' do
blackst0ne's avatar
blackst0ne committed
170
        get :show, params: environment_params
171 172 173 174 175 176 177

        expect(response).to be_ok
      end
    end

    context 'with invalid id' do
      it 'responds with a status code 404' do
Z.J. van de Weg's avatar
Z.J. van de Weg committed
178 179
        params = environment_params
        params[:id] = 12345
blackst0ne's avatar
blackst0ne committed
180
        get :show, params: params
181

182
        expect(response).to have_gitlab_http_status(404)
183 184 185 186 187 188
      end
    end
  end

  describe 'GET edit' do
    it 'responds with a status code 200' do
blackst0ne's avatar
blackst0ne committed
189
      get :edit, params: environment_params
190 191 192 193 194 195 196

      expect(response).to be_ok
    end
  end

  describe 'PATCH #update' do
    it 'responds with a 302' do
Z.J. van de Weg's avatar
Z.J. van de Weg committed
197
      patch_params = environment_params.merge(environment: { external_url: 'https://git.gitlab.com' })
blackst0ne's avatar
blackst0ne committed
198
      patch :update, params: patch_params
199

200
      expect(response).to have_gitlab_http_status(302)
201 202
    end
  end
Z.J. van de Weg's avatar
Z.J. van de Weg committed
203

Fatih Acet's avatar
Fatih Acet committed
204 205 206 207 208
  describe 'PATCH #stop' do
    context 'when env not available' do
      it 'returns 404' do
        allow_any_instance_of(Environment).to receive(:available?) { false }

blackst0ne's avatar
blackst0ne committed
209
        patch :stop, params: environment_params(format: :json)
Fatih Acet's avatar
Fatih Acet committed
210

211
        expect(response).to have_gitlab_http_status(404)
Fatih Acet's avatar
Fatih Acet committed
212 213 214 215 216 217 218 219 220 221
      end
    end

    context 'when stop action' do
      it 'returns action url' do
        action = create(:ci_build, :manual)

        allow_any_instance_of(Environment)
          .to receive_messages(available?: true, stop_with_action!: action)

blackst0ne's avatar
blackst0ne committed
222
        patch :stop, params: environment_params(format: :json)
Fatih Acet's avatar
Fatih Acet committed
223

224
        expect(response).to have_gitlab_http_status(200)
Fatih Acet's avatar
Fatih Acet committed
225 226
        expect(json_response).to eq(
          { 'redirect_url' =>
227
              project_job_url(project, action) })
Fatih Acet's avatar
Fatih Acet committed
228 229 230 231 232 233 234 235
      end
    end

    context 'when no stop action' do
      it 'returns env url' do
        allow_any_instance_of(Environment)
          .to receive_messages(available?: true, stop_with_action!: nil)

blackst0ne's avatar
blackst0ne committed
236
        patch :stop, params: environment_params(format: :json)
Fatih Acet's avatar
Fatih Acet committed
237

238
        expect(response).to have_gitlab_http_status(200)
Fatih Acet's avatar
Fatih Acet committed
239 240
        expect(json_response).to eq(
          { 'redirect_url' =>
241
              project_environment_url(project, environment) })
Fatih Acet's avatar
Fatih Acet committed
242 243 244 245
      end
    end
  end

246 247 248
  describe 'GET #terminal' do
    context 'with valid id' do
      it 'responds with a status code 200' do
blackst0ne's avatar
blackst0ne committed
249
        get :terminal, params: environment_params
250

251
        expect(response).to have_gitlab_http_status(200)
252 253
      end

254
      it 'loads the terminals for the environment' do
255 256 257 258
        # In EE we have to stub EE::Environment since it overwrites the
        # "terminals" method.
        expect_any_instance_of(defined?(EE) ? EE::Environment : Environment)
          .to receive(:terminals)
259

blackst0ne's avatar
blackst0ne committed
260
        get :terminal, params: environment_params
261 262 263 264 265
      end
    end

    context 'with invalid id' do
      it 'responds with a status code 404' do
blackst0ne's avatar
blackst0ne committed
266
        get :terminal, params: environment_params(id: 666)
267

268
        expect(response).to have_gitlab_http_status(404)
269 270 271 272 273 274 275 276 277 278 279 280
      end
    end
  end

  describe 'GET #terminal_websocket_authorize' do
    context 'with valid workhorse signature' do
      before do
        allow(Gitlab::Workhorse).to receive(:verify_api_request!).and_return(nil)
      end

      context 'and valid id' do
        it 'returns the first terminal for the environment' do
281 282 283
          # In EE we have to stub EE::Environment since it overwrites the
          # "terminals" method.
          expect_any_instance_of(defined?(EE) ? EE::Environment : Environment)
284 285 286 287
            .to receive(:terminals)
            .and_return([:fake_terminal])

          expect(Gitlab::Workhorse)
288
            .to receive(:channel_websocket)
289 290
            .with(:fake_terminal)
            .and_return(workhorse: :response)
291

blackst0ne's avatar
blackst0ne committed
292
          get :terminal_websocket_authorize, params: environment_params
293

294
          expect(response).to have_gitlab_http_status(200)
295 296 297 298 299 300 301
          expect(response.headers["Content-Type"]).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
          expect(response.body).to eq('{"workhorse":"response"}')
        end
      end

      context 'and invalid id' do
        it 'returns 404' do
blackst0ne's avatar
blackst0ne committed
302
          get :terminal_websocket_authorize, params: environment_params(id: 666)
303

304
          expect(response).to have_gitlab_http_status(404)
305 306 307 308 309 310 311 312
        end
      end
    end

    context 'with invalid workhorse signature' do
      it 'aborts with an exception' do
        allow(Gitlab::Workhorse).to receive(:verify_api_request!).and_raise(JWT::DecodeError)

blackst0ne's avatar
blackst0ne committed
313
        expect { get :terminal_websocket_authorize, params: environment_params }.to raise_error(JWT::DecodeError)
314 315 316 317 318 319
        # controller tests don't set the response status correctly. It's enough
        # to check that the action raised an exception
      end
    end
  end

320 321 322 323 324 325
  describe 'GET #metrics_redirect' do
    let(:project) { create(:project) }

    it 'redirects to environment if it exists' do
      environment = create(:environment, name: 'production', project: project)

blackst0ne's avatar
blackst0ne committed
326
      get :metrics_redirect, params: { namespace_id: project.namespace, project_id: project }
327 328 329 330 331

      expect(response).to redirect_to(environment_metrics_path(environment))
    end

    it 'redirects to empty page if no environment exists' do
blackst0ne's avatar
blackst0ne committed
332
      get :metrics_redirect, params: { namespace_id: project.namespace, project_id: project }
333

334 335
      expect(response).to be_ok
      expect(response).to render_template 'empty'
336 337 338
    end
  end

339 340 341 342 343 344 345
  describe 'GET #metrics' do
    before do
      allow(controller).to receive(:environment).and_return(environment)
    end

    context 'when environment has no metrics' do
      it 'returns a metrics page' do
346 347
        expect(environment).not_to receive(:metrics)

blackst0ne's avatar
blackst0ne committed
348
        get :metrics, params: environment_params
349 350 351 352 353 354

        expect(response).to be_ok
      end

      context 'when requesting metrics as JSON' do
        it 'returns a metrics JSON document' do
355 356
          expect(environment).to receive(:metrics).and_return(nil)

blackst0ne's avatar
blackst0ne committed
357
          get :metrics, params: environment_params(format: :json)
358

359
          expect(response).to have_gitlab_http_status(204)
360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
          expect(json_response).to eq({})
        end
      end
    end

    context 'when environment has some metrics' do
      before do
        expect(environment).to receive(:metrics).and_return({
          success: true,
          metrics: {},
          last_update: 42
        })
      end

      it 'returns a metrics JSON document' do
blackst0ne's avatar
blackst0ne committed
375
        get :metrics, params: environment_params(format: :json)
376 377 378 379 380 381 382 383 384

        expect(response).to be_ok
        expect(json_response['success']).to be(true)
        expect(json_response['metrics']).to eq({})
        expect(json_response['last_update']).to eq(42)
      end
    end
  end

385 386 387 388 389 390 391 392 393 394 395 396
  describe 'GET #additional_metrics' do
    before do
      allow(controller).to receive(:environment).and_return(environment)
    end

    context 'when environment has no metrics' do
      before do
        expect(environment).to receive(:additional_metrics).and_return(nil)
      end

      context 'when requesting metrics as JSON' do
        it 'returns a metrics JSON document' do
397
          additional_metrics
398

399
          expect(response).to have_gitlab_http_status(204)
400 401 402 403 404 405 406
          expect(json_response).to eq({})
        end
      end
    end

    context 'when environment has some metrics' do
      before do
Pawel Chojnacki's avatar
Pawel Chojnacki committed
407 408 409 410 411 412 413
        expect(environment)
          .to receive(:additional_metrics)
                .and_return({
                              success: true,
                              data: {},
                              last_update: 42
                            })
414 415 416
      end

      it 'returns a metrics JSON document' do
417
        additional_metrics
418 419 420 421 422 423

        expect(response).to be_ok
        expect(json_response['success']).to be(true)
        expect(json_response['data']).to eq({})
        expect(json_response['last_update']).to eq(42)
      end
424 425 426 427 428 429 430 431 432 433 434

      context 'when time params are provided' do
        it 'returns a metrics JSON document' do
          additional_metrics(start: '1554702993.5398998', end: '1554717396.996232')

          expect(response).to be_ok
          expect(json_response['success']).to be(true)
          expect(json_response['data']).to eq({})
          expect(json_response['last_update']).to eq(42)
        end
      end
435
    end
436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461

    context 'when only one time param is provided' do
      context 'when :metrics_time_window feature flag is disabled' do
        before do
          stub_feature_flags(metrics_time_window: false)
          expect(environment).to receive(:additional_metrics).with(no_args).and_return(nil)
        end

        it 'returns a time-window agnostic response' do
          additional_metrics(start: '1552647300.651094')

          expect(response).to have_gitlab_http_status(204)
          expect(json_response).to eq({})
        end
      end

      it 'raises an error when start is missing' do
        expect { additional_metrics(start: '1552647300.651094') }
          .to raise_error(ActionController::ParameterMissing)
      end

      it 'raises an error when end is missing' do
        expect { additional_metrics(start: '1552647300.651094') }
          .to raise_error(ActionController::ParameterMissing)
      end
    end
462 463
  end

464 465 466 467 468 469 470 471 472 473 474 475 476
  describe 'metrics_dashboard' do
    context 'when prometheus endpoint is disabled' do
      before do
        stub_feature_flags(environment_metrics_use_prometheus_endpoint: false)
      end

      it 'responds with status code 403' do
        get :metrics_dashboard, params: environment_params(format: :json)

        expect(response).to have_gitlab_http_status(:forbidden)
      end
    end

477 478 479 480 481 482 483
    shared_examples_for '200 response' do |contains_all_dashboards: false|
      let(:expected_keys) { %w(dashboard status) }

      before do
        expected_keys << 'all_dashboards' if contains_all_dashboards
      end

484
      it 'returns a json representation of the environment dashboard' do
485
        get :metrics_dashboard, params: environment_params(dashboard_params)
486 487

        expect(response).to have_gitlab_http_status(:ok)
488
        expect(json_response.keys).to contain_exactly(*expected_keys)
rpereira2's avatar
rpereira2 committed
489
        expect(json_response['dashboard']).to be_an_instance_of(Hash)
490
      end
491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525
    end

    shared_examples_for 'error response' do |status_code, contains_all_dashboards: false|
      let(:expected_keys) { %w(message status) }

      before do
        expected_keys << 'all_dashboards' if contains_all_dashboards
      end

      it 'returns an error response' do
        get :metrics_dashboard, params: environment_params(dashboard_params)

        expect(response).to have_gitlab_http_status(status_code)
        expect(json_response.keys).to contain_exactly(*expected_keys)
      end
    end

    shared_examples_for 'has all dashboards' do
      it 'includes an index of all available dashboards' do
        get :metrics_dashboard, params: environment_params(dashboard_params)

        expect(json_response.keys).to include('all_dashboards')
        expect(json_response['all_dashboards']).to be_an_instance_of(Array)
        expect(json_response['all_dashboards']).to all( include('path', 'default') )
      end
    end

    context 'when multiple dashboards is disabled' do
      before do
        stub_feature_flags(environment_metrics_show_multiple_dashboards: false)
      end

      let(:dashboard_params) { { format: :json } }

      it_behaves_like '200 response'
526 527 528

      context 'when the dashboard could not be provided' do
        before do
529
          allow(YAML).to receive(:safe_load).and_return({})
530 531
        end

532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568
        it_behaves_like 'error response', :unprocessable_entity
      end

      context 'when a dashboard param is specified' do
        let(:dashboard_params) { { format: :json, dashboard: '.gitlab/dashboards/not_there_dashboard.yml' } }

        it_behaves_like '200 response'
      end
    end

    context 'when multiple dashboards is enabled' do
      let(:dashboard_params) { { format: :json } }

      it_behaves_like '200 response', contains_all_dashboards: true
      it_behaves_like 'has all dashboards'

      context 'when a dashboard could not be provided' do
        before do
          allow(YAML).to receive(:safe_load).and_return({})
        end

        it_behaves_like 'error response', :unprocessable_entity, contains_all_dashboards: true
        it_behaves_like 'has all dashboards'
      end

      context 'when a dashboard param is specified' do
        let(:dashboard_params) { { format: :json, dashboard: '.gitlab/dashboards/test.yml' } }

        context 'when the dashboard is available' do
          let(:dashboard_yml) { fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') }
          let(:dashboard_file) { { '.gitlab/dashboards/test.yml' => dashboard_yml } }
          let(:project) { create(:project, :custom_repo, files: dashboard_file) }
          let(:environment) { create(:environment, name: 'production', project: project) }

          it_behaves_like '200 response', contains_all_dashboards: true
          it_behaves_like 'has all dashboards'
        end
569

570 571 572
        context 'when the dashboard does not exist' do
          it_behaves_like 'error response', :not_found, contains_all_dashboards: true
          it_behaves_like 'has all dashboards'
573 574
        end
      end
575 576 577
    end
  end

578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650
  describe 'GET #search' do
    before do
      create(:environment, name: 'staging', project: project)
      create(:environment, name: 'review/patch-1', project: project)
      create(:environment, name: 'review/patch-2', project: project)
    end

    let(:query) { 'pro' }

    it 'responds with status code 200' do
      get :search, params: environment_params(format: :json, query: query)

      expect(response).to have_gitlab_http_status(:ok)
    end

    it 'returns matched results' do
      get :search, params: environment_params(format: :json, query: query)

      expect(json_response).to contain_exactly('production')
    end

    context 'when query is review' do
      let(:query) { 'review' }

      it 'returns matched results' do
        get :search, params: environment_params(format: :json, query: query)

        expect(json_response).to contain_exactly('review/patch-1', 'review/patch-2')
      end
    end

    context 'when query is empty' do
      let(:query) { '' }

      it 'returns matched results' do
        get :search, params: environment_params(format: :json, query: query)

        expect(json_response)
          .to contain_exactly('production', 'staging', 'review/patch-1', 'review/patch-2')
      end
    end

    context 'when query is review/patch-3' do
      let(:query) { 'review/patch-3' }

      it 'responds with status code 204' do
        get :search, params: environment_params(format: :json, query: query)

        expect(response).to have_gitlab_http_status(:no_content)
      end
    end

    context 'when query is partially matched in the middle of environment name' do
      let(:query) { 'patch' }

      it 'responds with status code 204' do
        get :search, params: environment_params(format: :json, query: query)

        expect(response).to have_gitlab_http_status(:no_content)
      end
    end

    context 'when query contains a wildcard character' do
      let(:query) { 'review%' }

      it 'prevents wildcard injection' do
        get :search, params: environment_params(format: :json, query: query)

        expect(response).to have_gitlab_http_status(:no_content)
      end
    end
  end

651 652 653 654
  def environment_params(opts = {})
    opts.reverse_merge(namespace_id: project.namespace,
                       project_id: project,
                       id: environment.id)
Z.J. van de Weg's avatar
Z.J. van de Weg committed
655
  end
656 657 658 659

  def additional_metrics(opts = {})
    get :additional_metrics, params: environment_params(format: :json, **opts)
  end
660
end