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

3 4 5
require 'spec_helper'

describe Projects::EnvironmentsController do
6 7
  include MetricsDashboardHelpers

8 9
  let_it_be(:user) { create(:user) }
  let_it_be(:project) { create(:project) }
10

11
  let_it_be(:environment) do
12
    create(:environment, name: 'production', project: project)
13
  end
14 15

  before do
16
    project.add_maintainer(user)
17 18 19 20

    sign_in(user)
  end

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

26
        expect(response).to have_gitlab_http_status(:ok)
27
      end
28 29 30 31 32

      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
33
        get :index, params: environment_params
34
      end
35 36
    end

37 38
    context 'when requesting JSON response for folders' do
      before do
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
        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'] }

54 55 56 57 58 59 60
      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
61 62 63
          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')
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
          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

88 89
      context 'when requesting available environments scope' do
        before do
90
          get :index, params: environment_params(format: :json, nested: true, scope: :available)
91 92 93 94 95 96 97 98 99 100 101 102 103 104
        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
105 106
      end

107 108
      context 'when requesting stopped environments scope' do
        before do
109
          get :index, params: environment_params(format: :json, nested: true, scope: :stopped)
110 111 112 113 114 115 116 117
        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
118

119 120 121 122
        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
123 124 125 126
      end
    end
  end

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

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

        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
151
      it 'sorts the subfolders lexicographically' do
blackst0ne's avatar
blackst0ne committed
152 153 154 155 156
        get :folder, params: {
                       namespace_id: project.namespace,
                       project_id: project,
                       id: 'staging-1.0'
                     },
157 158 159 160 161
                     format: :json

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

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

        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
180 181
        params = environment_params
        params[:id] = 12345
blackst0ne's avatar
blackst0ne committed
182
        get :show, params: params
183

184
        expect(response).to have_gitlab_http_status(404)
185 186 187 188 189 190
      end
    end
  end

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

      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
199
      patch_params = environment_params.merge(environment: { external_url: 'https://git.gitlab.com' })
blackst0ne's avatar
blackst0ne committed
200
      patch :update, params: patch_params
201

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

Fatih Acet's avatar
Fatih Acet committed
206 207 208 209 210
  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
211
        patch :stop, params: environment_params(format: :json)
Fatih Acet's avatar
Fatih Acet committed
212

213
        expect(response).to have_gitlab_http_status(404)
Fatih Acet's avatar
Fatih Acet committed
214 215 216 217 218 219 220 221 222 223
      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
224
        patch :stop, params: environment_params(format: :json)
Fatih Acet's avatar
Fatih Acet committed
225

226
        expect(response).to have_gitlab_http_status(200)
Fatih Acet's avatar
Fatih Acet committed
227 228
        expect(json_response).to eq(
          { 'redirect_url' =>
229
              project_job_url(project, action) })
Fatih Acet's avatar
Fatih Acet committed
230 231 232 233 234 235 236 237
      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
238
        patch :stop, params: environment_params(format: :json)
Fatih Acet's avatar
Fatih Acet committed
239

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

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

253
        expect(response).to have_gitlab_http_status(200)
254 255
      end

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

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

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

270
        expect(response).to have_gitlab_http_status(404)
271 272 273 274 275 276 277 278 279 280 281 282
      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
283 284
          # In EE we have to stub EE::Environment since it overwrites the
          # "terminals" method.
285
          expect_any_instance_of(Gitlab.ee? ? EE::Environment : Environment)
286 287 288 289
            .to receive(:terminals)
            .and_return([:fake_terminal])

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

blackst0ne's avatar
blackst0ne committed
294
          get :terminal_websocket_authorize, params: environment_params
295

296
          expect(response).to have_gitlab_http_status(200)
297 298 299 300 301 302 303
          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
304
          get :terminal_websocket_authorize, params: environment_params(id: 666)
305

306
          expect(response).to have_gitlab_http_status(404)
307 308 309 310 311 312 313 314
        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
315
        expect { get :terminal_websocket_authorize, params: environment_params }.to raise_error(JWT::DecodeError)
316 317 318 319 320 321
        # controller tests don't set the response status correctly. It's enough
        # to check that the action raised an exception
      end
    end
  end

322 323 324 325 326 327
  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
328
      get :metrics_redirect, params: { namespace_id: project.namespace, project_id: project }
329 330 331 332 333

      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
334
      get :metrics_redirect, params: { namespace_id: project.namespace, project_id: project }
335

336 337
      expect(response).to be_ok
      expect(response).to render_template 'empty'
338 339 340
    end
  end

341 342 343 344 345 346 347
  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
348 349
        expect(environment).not_to receive(:metrics)

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

        expect(response).to be_ok
      end

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

blackst0ne's avatar
blackst0ne committed
359
          get :metrics, params: environment_params(format: :json)
360

361
          expect(response).to have_gitlab_http_status(204)
362 363 364 365 366 367 368 369 370 371 372 373 374 375 376
          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
377
        get :metrics, params: environment_params(format: :json)
378 379 380 381 382 383 384 385 386

        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

387
  describe 'GET #additional_metrics' do
388 389
    let(:window_params) { { start: '1554702993.5398998', end: '1554717396.996232' } }

390 391 392 393 394 395 396 397 398 399 400
    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
401
          additional_metrics(window_params)
402

403
          expect(response).to have_gitlab_http_status(204)
404 405 406 407 408 409 410
          expect(json_response).to eq({})
        end
      end
    end

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

      it 'returns a metrics JSON document' do
421
        additional_metrics(window_params)
422 423 424 425 426 427

        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
428
    end
429

430 431 432 433
    context 'when time params are missing' do
      it 'raises an error when window params are missing' do
        expect { additional_metrics }
        .to raise_error(ActionController::ParameterMissing)
434
      end
435
    end
436 437 438

    context 'when only one time param is provided' do
      it 'raises an error when start is missing' do
439
        expect { additional_metrics(end: '1552647300.651094') }
440 441 442 443 444 445 446 447
          .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
448 449
  end

450 451 452 453
  describe 'GET #metrics_dashboard' do
    shared_examples_for 'correctly formatted response' do |status_code|
      it 'returns a json object with the correct keys' do
        get :metrics_dashboard, params: environment_params(dashboard_params)
454

455 456
        # Exlcude `all_dashboards` to handle separately.
        found_keys = json_response.keys - ['all_dashboards']
457

458 459
        expect(response).to have_gitlab_http_status(status_code)
        expect(found_keys).to contain_exactly(*expected_keys)
460 461 462
      end
    end

463
    shared_examples_for '200 response' do
464 465
      let(:expected_keys) { %w(dashboard status) }

466
      it_behaves_like 'correctly formatted response', :ok
467 468
    end

469
    shared_examples_for 'error response' do |status_code|
470 471
      let(:expected_keys) { %w(message status) }

472 473
      it_behaves_like 'correctly formatted response', status_code
    end
474

475 476
    shared_examples_for 'includes all dashboards' do
      it 'includes info for all findable dashboard' do
477 478
        get :metrics_dashboard, params: environment_params(dashboard_params)

479 480 481
        expect(json_response).to have_key('all_dashboards')
        expect(json_response['all_dashboards']).to be_an_instance_of(Array)
        expect(json_response['all_dashboards']).to all( include('path', 'default', 'display_name') )
482 483 484
      end
    end

485 486
    shared_examples_for 'the default dashboard' do
      it_behaves_like '200 response'
487
      it_behaves_like 'includes all dashboards'
488 489

      it 'is the default dashboard' do
490 491
        get :metrics_dashboard, params: environment_params(dashboard_params)

492
        expect(json_response['dashboard']['dashboard']).to eq('Environment metrics')
493 494 495
      end
    end

496 497 498
    shared_examples_for 'the specified dashboard' do |expected_dashboard|
      it_behaves_like '200 response'
      it_behaves_like 'includes all dashboards'
499

500 501
      it 'has the correct name' do
        get :metrics_dashboard, params: environment_params(dashboard_params)
502

503
        dashboard_name = json_response['dashboard']['dashboard']
504

505 506 507 508 509 510
        # 'Environment metrics' is the default dashboard.
        expect(dashboard_name).not_to eq('Environment metrics')
        expect(dashboard_name).to eq(expected_dashboard)
      end

      context 'when the dashboard cannot not be processed' do
511
        before do
512
          allow(YAML).to receive(:safe_load).and_return({})
513 514
        end

515 516 517 518
        it_behaves_like 'error response', :unprocessable_entity
      end
    end

519
    shared_examples_for 'specified dashboard embed' do |expected_titles|
520
      it_behaves_like '200 response'
521

522
      it 'contains only the specified charts' do
523
        get :metrics_dashboard, params: environment_params(dashboard_params)
524

525 526 527
        dashboard = json_response['dashboard']
        panel_group = dashboard['panel_groups'].first
        titles = panel_group['panels'].map { |panel| panel['title'] }
528

529 530 531
        expect(dashboard['dashboard']).to be_nil
        expect(dashboard['panel_groups'].length).to eq 1
        expect(panel_group['group']).to be_nil
532
        expect(titles).to eq expected_titles
533
      end
534
    end
535

536 537 538 539
    shared_examples_for 'the default dynamic dashboard' do
      it_behaves_like 'specified dashboard embed', ['Memory Usage (Total)', 'Core Usage (Total)']
    end

540 541 542 543 544 545
    shared_examples_for 'dashboard can be specified' do
      context 'when dashboard is specified' do
        let(:dashboard_path) { '.gitlab/dashboards/test.yml' }
        let(:dashboard_params) { { format: :json, dashboard: dashboard_path } }

        it_behaves_like 'error response', :not_found
546

547
        context 'when the project dashboard is available' do
548
          let(:dashboard_yml) { fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') }
549
          let(:project) { project_with_dashboard(dashboard_path, dashboard_yml) }
550 551
          let(:environment) { create(:environment, name: 'production', project: project) }

552
          it_behaves_like 'the specified dashboard', 'Test Dashboard'
553
        end
554

555
        context 'when the specified dashboard is the default dashboard' do
556
          let(:dashboard_path) { system_dashboard_path }
557 558

          it_behaves_like 'the default dashboard'
559 560
        end
      end
561
    end
562

563 564
    shared_examples_for 'dashboard can be embedded' do
      context 'when the embedded flag is included' do
565 566
        let(:dashboard_params) { { format: :json, embedded: true } }

567
        it_behaves_like 'the default dynamic dashboard'
568

569 570 571 572 573 574 575 576 577
        context 'when incomplete dashboard params are provided' do
          let(:dashboard_params) { { format: :json, embedded: true, title: 'Title' } }

          # The title param should be ignored.
          it_behaves_like 'the default dynamic dashboard'
        end

        context 'when invalid params are provided' do
          let(:dashboard_params) { { format: :json, embedded: true, metric_id: 16 } }
578

579
          # The superfluous param should be ignored.
580
          it_behaves_like 'the default dynamic dashboard'
581
        end
582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602

        context 'when the dashboard is correctly specified' do
          let(:dashboard_params) do
            {
              format: :json,
              embedded: true,
              dashboard: system_dashboard_path,
              group: business_metric_title,
              title: 'title',
              y_label: 'y_label'
            }
          end

          it_behaves_like 'error response', :not_found

          context 'and exists' do
            let!(:metric) { create(:prometheus_metric, project: project) }

            it_behaves_like 'specified dashboard embed', ['title']
          end
        end
603
      end
604
    end
605 606 607 608 609 610 611 612 613 614 615 616 617 618

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

        it_behaves_like 'the default dashboard'
      end
    end

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

    it_behaves_like 'the default dashboard'
    it_behaves_like 'dashboard can be specified'
    it_behaves_like 'dashboard can be embedded'
619 620
  end

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 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691
  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
692 693 694 695 696 697 698 699 700 701

    context 'when query matches case insensitively' do
      let(:query) { 'Prod' }

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

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

704 705 706 707
  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
708
  end
709 710 711 712

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