diff options
Diffstat (limited to 'spec/controllers')
70 files changed, 3976 insertions, 1293 deletions
diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb index 4f223811be8..f71f859a704 100644 --- a/spec/controllers/admin/application_settings_controller_spec.rb +++ b/spec/controllers/admin/application_settings_controller_spec.rb @@ -15,6 +15,37 @@ RSpec.describe Admin::ApplicationSettingsController do stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') end + describe 'GET #integrations' do + before do + sign_in(admin) + end + + context 'when GitLab.com' do + before do + allow(::Gitlab).to receive(:com?) { true } + end + + it 'returns 404' do + get :integrations + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when not GitLab.com' do + before do + allow(::Gitlab).to receive(:com?) { false } + end + + it 'renders correct template' do + get :integrations + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template('admin/application_settings/integrations') + end + end + end + describe 'GET #usage_data with no access' do before do stub_usage_data_connections @@ -56,6 +87,13 @@ RSpec.describe Admin::ApplicationSettingsController do sign_in(admin) end + it 'updates the require_admin_approval_after_user_signup setting' do + put :update, params: { application_setting: { require_admin_approval_after_user_signup: true } } + + expect(response).to redirect_to(general_admin_application_settings_path) + expect(ApplicationSetting.current.require_admin_approval_after_user_signup).to eq(true) + end + it 'updates the password_authentication_enabled_for_git setting' do put :update, params: { application_setting: { password_authentication_enabled_for_git: "0" } } diff --git a/spec/controllers/admin/clusters_controller_spec.rb b/spec/controllers/admin/clusters_controller_spec.rb index d2a569a9d48..69bdc79c5f5 100644 --- a/spec/controllers/admin/clusters_controller_spec.rb +++ b/spec/controllers/admin/clusters_controller_spec.rb @@ -416,6 +416,7 @@ RSpec.describe Admin::ClustersController do expect(cluster).to be_user expect(cluster).to be_kubernetes expect(cluster).to be_platform_kubernetes_rbac + expect(cluster).to be_namespace_per_environment end end end @@ -585,6 +586,7 @@ RSpec.describe Admin::ClustersController do enabled: false, name: 'my-new-cluster-name', managed: false, + namespace_per_environment: false, base_domain: domain } } @@ -599,6 +601,7 @@ RSpec.describe Admin::ClustersController do expect(cluster.enabled).to be_falsey expect(cluster.name).to eq('my-new-cluster-name') expect(cluster).not_to be_managed + expect(cluster).not_to be_namespace_per_environment expect(cluster.domain).to eq('test-domain.com') end @@ -624,6 +627,7 @@ RSpec.describe Admin::ClustersController do enabled: false, name: 'my-new-cluster-name', managed: false, + namespace_per_environment: false, domain: domain } } @@ -637,6 +641,7 @@ RSpec.describe Admin::ClustersController do expect(cluster.enabled).to be_falsey expect(cluster.name).to eq('my-new-cluster-name') expect(cluster).not_to be_managed + expect(cluster).not_to be_namespace_per_environment end end diff --git a/spec/controllers/admin/hooks_controller_spec.rb b/spec/controllers/admin/hooks_controller_spec.rb index 8975f746dd7..17c4222530d 100644 --- a/spec/controllers/admin/hooks_controller_spec.rb +++ b/spec/controllers/admin/hooks_controller_spec.rb @@ -29,4 +29,12 @@ RSpec.describe Admin::HooksController do expect(SystemHook.first).to have_attributes(hook_params) end end + + describe 'DELETE #destroy' do + let!(:hook) { create(:system_hook) } + let!(:log) { create(:web_hook_log, web_hook: hook) } + let(:params) { { id: hook } } + + it_behaves_like 'Web hook destroyer' + end end diff --git a/spec/controllers/admin/instance_review_controller_spec.rb b/spec/controllers/admin/instance_review_controller_spec.rb new file mode 100644 index 00000000000..d15894eeb5d --- /dev/null +++ b/spec/controllers/admin/instance_review_controller_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Admin::InstanceReviewController do + include UsageDataHelpers + + let(:admin) { create(:admin) } + let(:subscriptions_url) { ::Gitlab::SubscriptionPortal::SUBSCRIPTIONS_URL } + + before do + sign_in(admin) + end + + context 'GET #index' do + let!(:group) { create(:group) } + let!(:projects) { create_list(:project, 2, group: group) } + + subject { post :index } + + context 'with usage ping enabled' do + before do + stub_application_setting(usage_ping_enabled: true) + stub_usage_data_connections + ::Gitlab::UsageData.data(force_refresh: true) + subject + end + + it 'redirects to the customers app with correct params' do + params = { instance_review: { + email: admin.email, + last_name: admin.name, + version: ::Gitlab::VERSION, + users_count: 5, + projects_count: 2, + groups_count: 1, + issues_count: 0, + merge_requests_count: 0, + internal_pipelines_count: 0, + external_pipelines_count: 0, + labels_count: 0, + milestones_count: 0, + snippets_count: 0, + notes_count: 0 + } }.to_query + + expect(response).to redirect_to("#{subscriptions_url}/instance_review?#{params}") + end + end + + context 'with usage ping disabled' do + before do + stub_application_setting(usage_ping_enabled: false) + subject + end + + it 'redirects to the customers app with correct params' do + params = { instance_review: { + email: admin.email, + last_name: admin.name, + version: ::Gitlab::VERSION + } }.to_query + + expect(response).to redirect_to("#{subscriptions_url}/instance_review?#{params}") + end + end + end +end diff --git a/spec/controllers/admin/integrations_controller_spec.rb b/spec/controllers/admin/integrations_controller_spec.rb index 4b1806a43d2..1a13d016b73 100644 --- a/spec/controllers/admin/integrations_controller_spec.rb +++ b/spec/controllers/admin/integrations_controller_spec.rb @@ -20,6 +20,18 @@ RSpec.describe Admin::IntegrationsController do end end end + + context 'when GitLab.com' do + before do + allow(::Gitlab).to receive(:com?) { true } + end + + it 'returns 404' do + get :edit, params: { id: Service.available_services_names.sample } + + expect(response).to have_gitlab_http_status(:not_found) + end + end end describe '#update' do @@ -43,7 +55,7 @@ RSpec.describe Admin::IntegrationsController do end it 'calls to PropagateIntegrationWorker' do - expect(PropagateIntegrationWorker).to have_received(:perform_async).with(integration.id, false) + expect(PropagateIntegrationWorker).to have_received(:perform_async).with(integration.id) end end diff --git a/spec/controllers/admin/runners_controller_spec.rb b/spec/controllers/admin/runners_controller_spec.rb index 013eee19409..3fffc50475c 100644 --- a/spec/controllers/admin/runners_controller_spec.rb +++ b/spec/controllers/admin/runners_controller_spec.rb @@ -151,4 +151,21 @@ RSpec.describe Admin::RunnersController do expect(runner.active).to eq(false) end end + + describe 'GET #runner_setup_scripts' do + it 'renders the setup scripts' do + get :runner_setup_scripts, params: { os: 'linux', arch: 'amd64' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to have_key("install") + expect(json_response).to have_key("register") + end + + it 'renders errors if they occur' do + get :runner_setup_scripts, params: { os: 'foo', arch: 'bar' } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response).to have_key("errors") + end + end end diff --git a/spec/controllers/admin/sessions_controller_spec.rb b/spec/controllers/admin/sessions_controller_spec.rb index 35982e57034..5fa7a7f278d 100644 --- a/spec/controllers/admin/sessions_controller_spec.rb +++ b/spec/controllers/admin/sessions_controller_spec.rb @@ -109,7 +109,7 @@ RSpec.describe Admin::SessionsController, :do_not_mock_admin_mode do # triggering the auth form will request admin mode get :new - Timecop.freeze(Gitlab::Auth::CurrentUserMode::ADMIN_MODE_REQUESTED_GRACE_PERIOD.from_now) do + travel_to(Gitlab::Auth::CurrentUserMode::ADMIN_MODE_REQUESTED_GRACE_PERIOD.from_now) do post :create, params: { user: { password: user.password } } expect(response).to redirect_to(new_admin_session_path) diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 6301da74f4a..5312a0db7f5 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -23,6 +23,12 @@ RSpec.describe Admin::UsersController do expect(assigns(:users)).to eq([admin]) end + + it 'eager loads authorized projects association' do + get :index + + expect(assigns(:users).first.association(:authorized_projects)).to be_loaded + end end describe 'GET :id' do @@ -96,6 +102,58 @@ RSpec.describe Admin::UsersController do end end + describe 'PUT #approve' do + let(:user) { create(:user, :blocked_pending_approval) } + + subject { put :approve, params: { id: user.username } } + + context 'when feature is disabled' do + before do + stub_feature_flags(admin_approval_for_new_user_signups: false) + end + + it 'responds with access denied' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when feature is enabled' do + before do + stub_feature_flags(admin_approval_for_new_user_signups: true) + end + + context 'when successful' do + it 'activates the user' do + subject + + user.reload + + expect(user).to be_active + expect(flash[:notice]).to eq('Successfully approved') + end + end + + context 'when unsuccessful' do + let(:user) { create(:user, :blocked) } + + it 'displays the error' do + subject + + expect(flash[:alert]).to eq('The user you are trying to approve is not pending an approval') + end + + it 'does not activate the user' do + subject + + user.reload + expect(user).not_to be_active + end + end + end + end + describe 'PUT #activate' do shared_examples 'a request that activates the user' do it 'activates the user' do @@ -184,6 +242,17 @@ RSpec.describe Admin::UsersController do expect(flash[:notice]).to eq('Error occurred. A blocked user cannot be deactivated') end end + + context 'for an internal user' do + it 'does not deactivate the user' do + internal_user = User.alert_bot + + put :deactivate, params: { id: internal_user.username } + + expect(internal_user.reload.deactivated?).to be_falsey + expect(flash[:notice]).to eq('Internal users cannot be deactivated') + end + end end describe 'PUT block/:id' do @@ -321,7 +390,7 @@ RSpec.describe Admin::UsersController do describe 'POST update' do context 'when the password has changed' do - def update_password(user, password = User.random_password, password_confirmation = password) + def update_password(user, password = User.random_password, password_confirmation = password, format = :html) params = { id: user.to_param, user: { @@ -330,7 +399,7 @@ RSpec.describe Admin::UsersController do } } - post :update, params: params + post :update, params: params, format: format end context 'when admin changes their own password' do @@ -429,6 +498,23 @@ RSpec.describe Admin::UsersController do .not_to change { user.reload.encrypted_password } end end + + context 'when the update fails' do + let(:password) { User.random_password } + + before do + expect_next_instance_of(Users::UpdateService) do |service| + allow(service).to receive(:execute).and_return({ message: 'failed', status: :error }) + end + end + + it 'returns a 500 error' do + expect { update_password(admin, password, password, :json) } + .not_to change { admin.reload.password_expired? } + + expect(response).to have_gitlab_http_status(:error) + end + end end context 'admin notes' do diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 188a4cb04af..d95aac2f386 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -416,13 +416,13 @@ RSpec.describe ApplicationController do end it 'returns false if the grace period has expired' do - Timecop.freeze(3.hours.from_now) do + travel_to(3.hours.from_now) do expect(subject).to be_falsey end end it 'returns true if the grace period is still active' do - Timecop.freeze(1.hour.from_now) do + travel_to(1.hour.from_now) do expect(subject).to be_truthy end end @@ -844,6 +844,8 @@ RSpec.describe ApplicationController do describe '#set_current_context' do controller(described_class) do + feature_category :issue_tracking + def index Labkit::Context.with_context do |context| render json: context.to_h @@ -893,6 +895,12 @@ RSpec.describe ApplicationController do expect(json_response['meta.caller_id']).to eq('AnonymousController#index') end + it 'sets the feature_category as defined in the controller' do + get :index, format: :json + + expect(json_response['meta.feature_category']).to eq('issue_tracking') + end + it 'assigns the context to a variable for logging' do get :index, format: :json diff --git a/spec/controllers/boards/lists_controller_spec.rb b/spec/controllers/boards/lists_controller_spec.rb index c72d9e5053a..9b09f46d17e 100644 --- a/spec/controllers/boards/lists_controller_spec.rb +++ b/spec/controllers/boards/lists_controller_spec.rb @@ -260,6 +260,17 @@ RSpec.describe Boards::ListsController do end end + context 'with an error service response' do + it 'returns an unprocessable entity response' do + allow(Boards::Lists::DestroyService).to receive(:new) + .and_return(double(execute: ServiceResponse.error(message: 'error'))) + + remove_board_list user: user, board: board, list: planning + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + end + def remove_board_list(user:, board:, list:) sign_in(user) diff --git a/spec/controllers/concerns/controller_with_feature_category/config_spec.rb b/spec/controllers/concerns/controller_with_feature_category/config_spec.rb deleted file mode 100644 index 9b8ffd2baab..00000000000 --- a/spec/controllers/concerns/controller_with_feature_category/config_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -require "fast_spec_helper" -require "rspec-parameterized" -require_relative "../../../../app/controllers/concerns/controller_with_feature_category/config" - -RSpec.describe ControllerWithFeatureCategory::Config do - describe "#matches?" do - using RSpec::Parameterized::TableSyntax - - where(:only_actions, :except_actions, :if_proc, :unless_proc, :test_action, :expected) do - nil | nil | nil | nil | "action" | true - [:included] | nil | nil | nil | "action" | false - [:included] | nil | nil | nil | "included" | true - nil | [:excluded] | nil | nil | "excluded" | false - nil | nil | true | nil | "action" | true - [:included] | nil | true | nil | "action" | false - [:included] | nil | true | nil | "included" | true - nil | [:excluded] | true | nil | "excluded" | false - nil | nil | false | nil | "action" | false - [:included] | nil | false | nil | "action" | false - [:included] | nil | false | nil | "included" | false - nil | [:excluded] | false | nil | "excluded" | false - nil | nil | nil | true | "action" | false - [:included] | nil | nil | true | "action" | false - [:included] | nil | nil | true | "included" | false - nil | [:excluded] | nil | true | "excluded" | false - nil | nil | nil | false | "action" | true - [:included] | nil | nil | false | "action" | false - [:included] | nil | nil | false | "included" | true - nil | [:excluded] | nil | false | "excluded" | false - nil | nil | true | false | "action" | true - [:included] | nil | true | false | "action" | false - [:included] | nil | true | false | "included" | true - nil | [:excluded] | true | false | "excluded" | false - nil | nil | false | true | "action" | false - [:included] | nil | false | true | "action" | false - [:included] | nil | false | true | "included" | false - nil | [:excluded] | false | true | "excluded" | false - end - - with_them do - let(:config) do - if_to_proc = if_proc.nil? ? nil : -> (_) { if_proc } - unless_to_proc = unless_proc.nil? ? nil : -> (_) { unless_proc } - - described_class.new(:category, only_actions, except_actions, if_to_proc, unless_to_proc) - end - - specify { expect(config.matches?(test_action)).to be(expected) } - end - end -end diff --git a/spec/controllers/concerns/controller_with_feature_category_spec.rb b/spec/controllers/concerns/controller_with_feature_category_spec.rb index e603a7d14c4..55e84755f5c 100644 --- a/spec/controllers/concerns/controller_with_feature_category_spec.rb +++ b/spec/controllers/concerns/controller_with_feature_category_spec.rb @@ -2,7 +2,6 @@ require 'fast_spec_helper' require_relative "../../../app/controllers/concerns/controller_with_feature_category" -require_relative "../../../app/controllers/concerns/controller_with_feature_category/config" RSpec.describe ControllerWithFeatureCategory do describe ".feature_category_for_action" do @@ -14,17 +13,15 @@ RSpec.describe ControllerWithFeatureCategory do let(:controller) do Class.new(base_controller) do - feature_category :baz - feature_category :foo, except: %w(update edit) - feature_category :bar, only: %w(index show) - feature_category :quux, only: %w(destroy) - feature_category :quuz, only: %w(destroy) + feature_category :foo, %w(update edit) + feature_category :bar, %w(index show) + feature_category :quux, %w(destroy) end end let(:subclass) do Class.new(controller) do - feature_category :qux, only: %w(index) + feature_category :baz, %w(subclass_index) end end @@ -33,34 +30,31 @@ RSpec.describe ControllerWithFeatureCategory do end it "returns the expected category", :aggregate_failures do - expect(controller.feature_category_for_action("update")).to eq(:baz) - expect(controller.feature_category_for_action("hello")).to eq(:foo) + expect(controller.feature_category_for_action("update")).to eq(:foo) expect(controller.feature_category_for_action("index")).to eq(:bar) + expect(controller.feature_category_for_action("destroy")).to eq(:quux) end - it "returns the closest match for categories defined in subclasses" do - expect(subclass.feature_category_for_action("index")).to eq(:qux) - expect(subclass.feature_category_for_action("show")).to eq(:bar) + it "returns the expected category for categories defined in subclasses" do + expect(subclass.feature_category_for_action("subclass_index")).to eq(:baz) end - it "returns the last defined feature category when multiple match" do - expect(controller.feature_category_for_action("destroy")).to eq(:quuz) - end - - it "raises an error when using including and excluding the same action" do + it "raises an error when defining for the controller and for individual actions" do expect do Class.new(base_controller) do - feature_category :hello, only: [:world], except: [:world] + feature_category :hello + feature_category :goodbye, [:world] end - end.to raise_error(%r(cannot configure both `only` and `except`)) + end.to raise_error(ArgumentError, "hello is defined for all actions, but other categories are set") end - it "raises an error when using unknown arguments" do + it "raises an error when multiple calls define the same action" do expect do Class.new(base_controller) do - feature_category :hello, hello: :world + feature_category :hello, [:world] + feature_category :goodbye, ["world"] end - end.to raise_error(%r(unknown arguments)) + end.to raise_error(ArgumentError, "Actions have multiple feature categories: world") end end end diff --git a/spec/controllers/concerns/issuable_collections_spec.rb b/spec/controllers/concerns/issuable_collections_spec.rb index befdd760965..6fa273bf3d7 100644 --- a/spec/controllers/concerns/issuable_collections_spec.rb +++ b/spec/controllers/concerns/issuable_collections_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe IssuableCollections do + using RSpec::Parameterized::TableSyntax + let(:user) { create(:user) } let(:controller) do @@ -25,13 +27,35 @@ RSpec.describe IssuableCollections do end describe '#page_count_for_relation' do - let(:params) { { state: 'opened' } } + let(:relation) { double(:relation, limit_value: 20) } + + context 'row count is known' do + let(:params) { { state: 'opened' } } + + it 'returns the number of pages' do + pages = controller.send(:page_count_for_relation, relation, 28) + + expect(pages).to eq(2) + end + end + + context 'row_count is unknown' do + where(:page_param, :expected) do + nil | 2 + 1 | 2 + '1' | 2 + 2 | 3 + end - it 'returns the number of pages' do - relation = double(:relation, limit_value: 20) - pages = controller.send(:page_count_for_relation, relation, 28) + with_them do + let(:params) { { state: 'opened', page: page_param } } - expect(pages).to eq(2) + it 'returns current page + 1 if the row count is unknown' do + pages = controller.send(:page_count_for_relation, relation, -1) + + expect(pages).to eq(expected) + end + end end end diff --git a/spec/controllers/concerns/redis_tracking_spec.rb b/spec/controllers/concerns/redis_tracking_spec.rb index 3795fca5576..831f5ad7bb1 100644 --- a/spec/controllers/concerns/redis_tracking_spec.rb +++ b/spec/controllers/concerns/redis_tracking_spec.rb @@ -3,15 +3,19 @@ require "spec_helper" RSpec.describe RedisTracking do - let(:event_name) { 'g_compliance_dashboard' } - let(:feature) { 'g_compliance_dashboard_feature' } + let(:feature) { 'approval_rule' } let(:user) { create(:user) } + before do + skip_feature_flags_yaml_validation + end + controller(ApplicationController) do include RedisTracking skip_before_action :authenticate_user!, only: :show - track_redis_hll_event :index, :show, name: 'i_analytics_dev_ops_score', feature: :g_compliance_dashboard_feature, feature_default_enabled: true + track_redis_hll_event :index, :show, name: 'g_compliance_approval_rules', feature: :approval_rule, feature_default_enabled: true, + if: [:custom_condition_one?, :custom_condition_two?] def index render html: 'index' @@ -24,51 +28,94 @@ RSpec.describe RedisTracking do def show render html: 'show' end - end - context 'with feature disabled' do - it 'does not track the event' do - stub_feature_flags(feature => false) + private - expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) + def custom_condition_one? + true + end - get :index + def custom_condition_two? + true end end - context 'with usage ping disabled' do + def expect_tracking + expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event) + .with(instance_of(String), 'g_compliance_approval_rules') + end + + def expect_no_tracking + expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) + end + + context 'with feature disabled' do it 'does not track the event' do - stub_feature_flags(feature => true) - allow(Gitlab::CurrentSettings).to receive(:usage_ping_enabled?).and_return(false) + stub_feature_flags(feature => false) - expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) + expect_no_tracking get :index end end - context 'with feature enabled and usage ping enabled' do + context 'with feature enabled' do before do stub_feature_flags(feature => true) - allow(Gitlab::CurrentSettings).to receive(:usage_ping_enabled?).and_return(true) end context 'when user is logged in' do - it 'tracks the event' do + before do sign_in(user) + end - expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event) + it 'tracks the event' do + expect_tracking get :index end it 'passes default_enabled flag' do - sign_in(user) - expect(controller).to receive(:metric_feature_enabled?).with(feature.to_sym, true) get :index end + + it 'tracks the event if DNT is not enabled' do + request.headers['DNT'] = '0' + + expect_tracking + + get :index + end + + it 'does not track the event if DNT is enabled' do + request.headers['DNT'] = '1' + + expect_no_tracking + + get :index + end + + it 'does not track the event if the format is not HTML' do + expect_no_tracking + + get :index, format: :json + end + + it 'does not track the event if a custom condition returns false' do + expect(controller).to receive(:custom_condition_two?).and_return(false) + + expect_no_tracking + + get :index + end + + it 'does not track the event for untracked actions' do + expect_no_tracking + + get :new + end end context 'when user is not logged in and there is a visitor_id' do @@ -81,26 +128,18 @@ RSpec.describe RedisTracking do it 'tracks the event' do cookies[:visitor_id] = { value: visitor_id, expires: 24.months } - expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event) + expect_tracking get :show end end context 'when user is not logged in and there is no visitor_id' do - it 'does not tracks the event' do - expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) + it 'does not track the event' do + expect_no_tracking get :index end end - - context 'for untracked action' do - it 'does not tracks the event' do - expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) - - get :new - end - end end end diff --git a/spec/controllers/dashboard/labels_controller_spec.rb b/spec/controllers/dashboard/labels_controller_spec.rb index 415cb821545..e7091664d1a 100644 --- a/spec/controllers/dashboard/labels_controller_spec.rb +++ b/spec/controllers/dashboard/labels_controller_spec.rb @@ -3,27 +3,32 @@ require 'spec_helper' RSpec.describe Dashboard::LabelsController do - let(:project) { create(:project) } - let(:user) { create(:user) } - let!(:label) { create(:label, project: project) } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:project_2) { create(:project) } + + let_it_be(:label) { create(:label, project: project, title: 'some_label') } + let_it_be(:label_with_same_title) { create(:label, project: project_2, title: 'some_label') } + let_it_be(:unrelated_label) { create(:label, project: create(:project, :public)) } + + before_all do + project.add_reporter(user) + project_2.add_reporter(user) + end before do sign_in(user) - project.add_reporter(user) end describe "#index" do - let!(:unrelated_label) { create(:label, project: create(:project, :public)) } - subject { get :index, format: :json } - it 'returns global labels for projects the user has a relationship with' do + it 'returns labels with unique titles for projects the user has a relationship with' do subject expect(json_response).to be_kind_of(Array) expect(json_response.size).to eq(1) - expect(json_response[0]["id"]).to be_nil - expect(json_response[0]["title"]).to eq(label.title) + expect(json_response[0]['title']).to eq(label.title) end it_behaves_like 'disabled when using an external authorization service' diff --git a/spec/controllers/dashboard_controller_spec.rb b/spec/controllers/dashboard_controller_spec.rb index c838affa239..9b78f841cce 100644 --- a/spec/controllers/dashboard_controller_spec.rb +++ b/spec/controllers/dashboard_controller_spec.rb @@ -15,6 +15,16 @@ RSpec.describe DashboardController do describe 'GET issues' do it_behaves_like 'issuables list meta-data', :issue, :issues it_behaves_like 'issuables requiring filter', :issues + + it 'lists only incidents and issues' do + issue = create(:incident, project: project, author: user) + incident = create(:incident, project: project, author: user) + create(:quality_test_case, project: project, author: user) + + get :issues, params: { author_id: user.id } + + expect(assigns(:issues)).to match_array([issue, incident]) + end end describe 'GET merge requests' do diff --git a/spec/controllers/every_controller_spec.rb b/spec/controllers/every_controller_spec.rb index 4785ee9ed8f..b1519c4ef1e 100644 --- a/spec/controllers/every_controller_spec.rb +++ b/spec/controllers/every_controller_spec.rb @@ -17,20 +17,20 @@ RSpec.describe "Every controller" do .compact .select { |route| route[:controller].present? && route[:action].present? } .map { |route| [constantize_controller(route[:controller]), route[:action]] } - .reject { |route| route.first.nil? || !route.first.include?(ControllerWithFeatureCategory) } + .select { |(controller, action)| controller&.include?(ControllerWithFeatureCategory) } + .reject { |(controller, action)| controller == ApplicationController || controller == Devise::UnlocksController } end let_it_be(:routes_without_category) do controller_actions.map do |controller, action| - "#{controller}##{action}" unless controller.feature_category_for_action(action) + next if controller.feature_category_for_action(action) + + "#{controller}##{action}" end.compact end it "has feature categories" do - pending("We'll work on defining categories for all controllers: "\ - "https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/463") - - expect(routes_without_category).to be_empty, "#{routes_without_category.first(10)} did not have a category" + expect(routes_without_category).to be_empty, "#{routes_without_category} did not have a category" end it "completed controllers don't get new routes without categories" do @@ -74,9 +74,9 @@ RSpec.describe "Every controller" do end def actions_defined_in_feature_category_config(controller) - feature_category_configs = controller.send(:class_attributes)[:feature_category_config] - feature_category_configs.map do |config| - Array(config.send(:only)) + Array(config.send(:except)) - end.flatten.uniq.map(&:to_s) + controller.send(:class_attributes)[:feature_category_config] + .values + .flatten + .map(&:to_s) end end diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb index 405f1eae482..e4aea688a69 100644 --- a/spec/controllers/graphql_controller_spec.rb +++ b/spec/controllers/graphql_controller_spec.rb @@ -98,6 +98,12 @@ RSpec.describe GraphqlController do expect(assigns(:context)[:is_sessionless_user]).to be false end end + + it 'includes request object in context' do + post :execute + + expect(assigns(:context)[:request]).to eq request + end end describe 'Admin Mode' do @@ -150,9 +156,11 @@ RSpec.describe GraphqlController do describe '#append_info_to_payload' do let(:graphql_query) { graphql_query_for('project', { 'fullPath' => 'foo' }, %w(id name)) } + let(:mock_store) { { graphql_logs: { foo: :bar } } } let(:log_payload) { {} } before do + allow(RequestStore).to receive(:store).and_return(mock_store) allow(controller).to receive(:append_info_to_payload).and_wrap_original do |method, *| method.call(log_payload) end @@ -162,7 +170,7 @@ RSpec.describe GraphqlController do post :execute, params: { query: graphql_query, operationName: 'Foo' } expect(controller).to have_received(:append_info_to_payload) - expect(log_payload.dig(:metadata, :graphql, :operation_name)).to eq('Foo') + expect(log_payload.dig(:metadata, :graphql)).to eq({ operation_name: 'Foo', foo: :bar }) end end end diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb index 81d5bc7770f..140b7b0f2a8 100644 --- a/spec/controllers/groups/clusters_controller_spec.rb +++ b/spec/controllers/groups/clusters_controller_spec.rb @@ -271,6 +271,7 @@ RSpec.describe Groups::ClustersController do expect(cluster).to be_kubernetes expect(cluster.provider_gcp).to be_legacy_abac expect(cluster).to be_managed + expect(cluster).to be_namespace_per_environment end context 'when legacy_abac param is false' do @@ -358,6 +359,7 @@ RSpec.describe Groups::ClustersController do expect(cluster).to be_user expect(cluster).to be_kubernetes expect(cluster).to be_managed + expect(cluster).to be_namespace_per_environment end end @@ -387,6 +389,7 @@ RSpec.describe Groups::ClustersController do expect(cluster).to be_user expect(cluster).to be_kubernetes expect(cluster).to be_platform_kubernetes_rbac + expect(cluster).to be_namespace_per_environment end end @@ -716,6 +719,7 @@ RSpec.describe Groups::ClustersController do enabled: false, name: 'my-new-cluster-name', managed: false, + namespace_per_environment: false, domain: domain } } @@ -729,6 +733,7 @@ RSpec.describe Groups::ClustersController do expect(cluster.enabled).to be_falsey expect(cluster.name).to eq('my-new-cluster-name') expect(cluster).not_to be_managed + expect(cluster).not_to be_namespace_per_environment end end diff --git a/spec/controllers/groups/group_links_controller_spec.rb b/spec/controllers/groups/group_links_controller_spec.rb index 07299382230..c411d9cfb63 100644 --- a/spec/controllers/groups/group_links_controller_spec.rb +++ b/spec/controllers/groups/group_links_controller_spec.rb @@ -15,6 +15,21 @@ RSpec.describe Groups::GroupLinksController do shared_with_group.add_developer(group_member) end + shared_examples 'placeholder is passed as `id` parameter' do |action| + it 'returns a 404' do + post( + action, + params: { + group_id: shared_group, + id: ':id' + }, + format: :json + ) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + describe '#create' do let(:shared_with_group_id) { shared_with_group.id } let(:shared_group_access) { GroupGroupLink.default_access } @@ -125,6 +140,8 @@ RSpec.describe Groups::GroupLinksController do expect(response).to have_gitlab_http_status(:not_found) end end + + include_examples 'placeholder is passed as `id` parameter', :create end describe '#update' do @@ -136,10 +153,15 @@ RSpec.describe Groups::GroupLinksController do let(:expiry_date) { 1.month.from_now.to_date } subject do - post(:update, params: { group_id: shared_group, - id: link.id, - group_link: { group_access: Gitlab::Access::GUEST, - expires_at: expiry_date } }) + post( + :update, + params: { + group_id: shared_group, + id: link.id, + group_link: { group_access: Gitlab::Access::GUEST, expires_at: expiry_date } + }, + format: :json + ) end context 'when user has admin access to the shared group' do @@ -160,6 +182,26 @@ RSpec.describe Groups::GroupLinksController do expect(link.expires_at).to eq(expiry_date) end + context 'when `expires_at` is set' do + it 'returns correct json response' do + travel_to Time.now.utc.beginning_of_day + + subject + + expect(json_response).to eq({ "expires_in" => "about 1 month", "expires_soon" => false }) + end + end + + context 'when `expires_at` is not set' do + let(:expiry_date) { nil } + + it 'returns empty json response' do + subject + + expect(json_response).to be_empty + end + end + it 'updates project permissions' do expect { subject }.to change { group_member.can?(:create_release, project) }.from(true).to(false) end @@ -172,6 +214,8 @@ RSpec.describe Groups::GroupLinksController do expect(response).to have_gitlab_http_status(:not_found) end end + + include_examples 'placeholder is passed as `id` parameter', :update end describe '#destroy' do @@ -207,5 +251,7 @@ RSpec.describe Groups::GroupLinksController do expect(response).to have_gitlab_http_status(:not_found) end end + + include_examples 'placeholder is passed as `id` parameter', :destroy end end diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb index 4b9dd3629f1..5425a437c80 100644 --- a/spec/controllers/groups/group_members_controller_spec.rb +++ b/spec/controllers/groups/group_members_controller_spec.rb @@ -233,6 +233,42 @@ RSpec.describe Groups::GroupMembersController do end end end + + context 'expiration date' do + let(:expiry_date) { 1.month.from_now.to_date } + + before do + travel_to Time.now.utc.beginning_of_day + + put( + :update, + params: { + group_member: { expires_at: expiry_date }, + group_id: group, + id: requester + }, + format: :json + ) + end + + context 'when `expires_at` is set' do + it 'returns correct json response' do + expect(json_response).to eq({ + "expires_in" => "about 1 month", + "expires_soon" => false, + "expires_at_formatted" => expiry_date.to_time.in_time_zone.to_s(:medium) + }) + end + end + + context 'when `expires_at` is not set' do + let(:expiry_date) { nil } + + it 'returns empty json response' do + expect(json_response).to be_empty + end + end + end end describe 'DELETE destroy' do @@ -441,7 +477,7 @@ RSpec.describe Groups::GroupMembersController do group_id: group, id: membership }, - format: :js + format: :json expect(response).to have_gitlab_http_status(:ok) end diff --git a/spec/controllers/groups/labels_controller_spec.rb b/spec/controllers/groups/labels_controller_spec.rb index 20ee19b01d1..33041f1af9f 100644 --- a/spec/controllers/groups/labels_controller_spec.rb +++ b/spec/controllers/groups/labels_controller_spec.rb @@ -9,6 +9,8 @@ RSpec.describe Groups::LabelsController do before do group.add_owner(user) + # by default FFs are enabled in specs so we turn it off + stub_feature_flags(show_inherited_labels: false) sign_in(user) end @@ -32,11 +34,41 @@ RSpec.describe Groups::LabelsController do subgroup.add_owner(user) end - it 'returns ancestor group labels' do - get :index, params: { group_id: subgroup, include_ancestor_groups: true, only_group_labels: true }, format: :json + RSpec.shared_examples 'returns ancestor group labels' do + it 'returns ancestor group labels' do + get :index, params: params, format: :json - label_ids = json_response.map {|label| label['title']} - expect(label_ids).to match_array([group_label_1.title, subgroup_label_1.title]) + label_ids = json_response.map {|label| label['title']} + expect(label_ids).to match_array([group_label_1.title, subgroup_label_1.title]) + end + end + + context 'when include_ancestor_groups true' do + let(:params) { { group_id: subgroup, include_ancestor_groups: true, only_group_labels: true } } + + it_behaves_like 'returns ancestor group labels' + end + + context 'when include_ancestor_groups false' do + let(:params) { { group_id: subgroup, only_group_labels: true } } + + it 'does not return ancestor group labels', :aggregate_failures do + get :index, params: params, format: :json + + label_ids = json_response.map {|label| label['title']} + expect(label_ids).to match_array([subgroup_label_1.title]) + expect(label_ids).not_to include([group_label_1.title]) + end + end + + context 'when show_inherited_labels enabled' do + let(:params) { { group_id: subgroup } } + + before do + stub_feature_flags(show_inherited_labels: true) + end + + it_behaves_like 'returns ancestor group labels' end end @@ -56,4 +88,43 @@ RSpec.describe Groups::LabelsController do expect(response).to have_gitlab_http_status(:ok) end end + + describe 'DELETE #destroy' do + context 'when current user has ability to destroy the label' do + before do + sign_in(user) + end + + it 'removes the label' do + label = create(:group_label, group: group) + delete :destroy, params: { group_id: group.to_param, id: label.to_param } + + expect { label.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + + context 'when label is succesfuly destroyed' do + it 'redirects to the group labels page' do + label = create(:group_label, group: group) + delete :destroy, params: { group_id: group.to_param, id: label.to_param } + + expect(response).to redirect_to(group_labels_path) + end + end + end + + context 'when current_user does not have ability to destroy the label' do + let(:another_user) { create(:user) } + + before do + sign_in(another_user) + end + + it 'responds with status 404' do + label = create(:group_label, group: group) + delete :destroy, params: { group_id: group.to_param, id: label.to_param } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end end diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb index 5c7b88a218a..2c85fe482e2 100644 --- a/spec/controllers/groups/milestones_controller_spec.rb +++ b/spec/controllers/groups/milestones_controller_spec.rb @@ -9,7 +9,6 @@ RSpec.describe Groups::MilestonesController do let(:user) { create(:user) } let(:title) { '肯定不是中文的问题' } let(:milestone) { create(:milestone, project: project) } - let(:milestone_path) { group_milestone_path(group, milestone.safe_title, title: milestone.title) } let(:milestone_params) do { @@ -25,6 +24,12 @@ RSpec.describe Groups::MilestonesController do project.add_maintainer(user) end + it_behaves_like 'milestone tabs' do + let(:milestone) { create(:milestone, group: group) } + let(:milestone_path) { group_milestone_path(group, milestone.iid) } + let(:request_params) { { group_id: group, id: milestone.iid } } + end + describe '#index' do describe 'as HTML' do render_views diff --git a/spec/controllers/groups/registry/repositories_controller_spec.rb b/spec/controllers/groups/registry/repositories_controller_spec.rb index ddac8fc5002..ae982b02a4f 100644 --- a/spec/controllers/groups/registry/repositories_controller_spec.rb +++ b/spec/controllers/groups/registry/repositories_controller_spec.rb @@ -87,7 +87,7 @@ RSpec.describe Groups::Registry::RepositoriesController do it_behaves_like 'with name parameter' - it_behaves_like 'a gitlab tracking event', described_class.name, 'list_repositories' + it_behaves_like 'a package tracking event', described_class.name, 'list_repositories' context 'with project in subgroup' do let_it_be(:test_group) { create(:group, parent: group ) } diff --git a/spec/controllers/groups/settings/ci_cd_controller_spec.rb b/spec/controllers/groups/settings/ci_cd_controller_spec.rb index f11bb66caab..880d5fe8951 100644 --- a/spec/controllers/groups/settings/ci_cd_controller_spec.rb +++ b/spec/controllers/groups/settings/ci_cd_controller_spec.rb @@ -225,4 +225,25 @@ RSpec.describe Groups::Settings::CiCdController do end end end + + describe 'GET #runner_setup_scripts' do + before do + group.add_owner(user) + end + + it 'renders the setup scripts' do + get :runner_setup_scripts, params: { os: 'linux', arch: 'amd64', group_id: group } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to have_key("install") + expect(json_response).to have_key("register") + end + + it 'renders errors if they occur' do + get :runner_setup_scripts, params: { os: 'foo', arch: 'bar', group_id: group } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response).to have_key("errors") + end + end end diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 35d8c0b7c6d..df7e018b35e 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -2,18 +2,18 @@ require 'spec_helper' -RSpec.describe GroupsController do +RSpec.describe GroupsController, factory_default: :keep do include ExternalAuthorizationServiceHelpers - let(:user) { create(:user) } - let(:admin) { create(:admin) } - let(:group) { create(:group, :public) } - let(:project) { create(:project, namespace: group) } - let!(:group_member) { create(:group_member, group: group, user: user) } - let!(:owner) { group.add_owner(create(:user)).user } - let!(:maintainer) { group.add_maintainer(create(:user)).user } - let!(:developer) { group.add_developer(create(:user)).user } - let!(:guest) { group.add_guest(create(:user)).user } + let_it_be_with_refind(:group) { create_default(:group, :public) } + let_it_be_with_refind(:project) { create(:project, namespace: group) } + let_it_be(:user) { create(:user) } + let_it_be(:admin) { create(:admin) } + let_it_be(:group_member) { create(:group_member, group: group, user: user) } + let_it_be(:owner) { group.add_owner(create(:user)).user } + let_it_be(:maintainer) { group.add_maintainer(create(:user)).user } + let_it_be(:developer) { group.add_developer(create(:user)).user } + let_it_be(:guest) { group.add_guest(create(:user)).user } shared_examples 'member with ability to create subgroups' do it 'renders the new page' do @@ -57,7 +57,6 @@ RSpec.describe GroupsController do describe 'GET #show' do before do sign_in(user) - project end let(:format) { :html } @@ -82,7 +81,6 @@ RSpec.describe GroupsController do describe 'GET #details' do before do sign_in(user) - project end let(:format) { :html } @@ -131,12 +129,9 @@ RSpec.describe GroupsController do end describe 'GET #activity' do - render_views - context 'as json' do before do sign_in(user) - project end it 'includes events from all projects in group and subgroups', :sidekiq_might_not_need_inline do @@ -157,10 +152,6 @@ RSpec.describe GroupsController do end context 'when user has no permission to see the event' do - let(:user) { create(:user) } - let(:group) { create(:group) } - let(:project) { create(:project, group: group) } - let(:project_with_restricted_access) do create(:project, :public, issues_access_level: ProjectFeature::PRIVATE, group: group) end @@ -398,8 +389,8 @@ RSpec.describe GroupsController do end describe 'GET #issues', :sidekiq_might_not_need_inline do - let(:issue_1) { create(:issue, project: project, title: 'foo') } - let(:issue_2) { create(:issue, project: project, title: 'bar') } + let_it_be(:issue_1) { create(:issue, project: project, title: 'foo') } + let_it_be(:issue_2) { create(:issue, project: project, title: 'bar') } before do create_list(:award_emoji, 3, awardable: issue_2) @@ -409,6 +400,15 @@ RSpec.describe GroupsController do sign_in(user) end + it 'lists only incidents and issues' do + incident = create(:incident, project: project) + create(:quality_test_case, project: project) + + get :issues, params: { id: group.to_param } + + expect(assigns(:issues)).to match_array([issue_1, issue_2, incident]) + end + context 'sorting by votes' do it 'sorts most popular issues' do get :issues, params: { id: group.to_param, sort: 'upvotes_desc' } @@ -551,9 +551,38 @@ RSpec.describe GroupsController do end end - context 'when there is a conflicting group path' do - render_views + context "updating default_branch_name" do + let(:example_branch_name) { "example_branch_name" } + + subject(:update_action) do + put :update, + params: { + id: group.to_param, + group: { default_branch_name: example_branch_name } + } + end + + it "updates the attribute" do + expect { subject } + .to change { group.namespace_settings.reload.default_branch_name } + .from(nil) + .to(example_branch_name) + + expect(response).to have_gitlab_http_status(:found) + end + context "to empty string" do + let(:example_branch_name) { '' } + + it "does not update the attribute" do + subject + + expect(group.namespace_settings.reload.default_branch_name).not_to eq('') + end + end + end + + context 'when there is a conflicting group path' do let!(:conflict_group) { create(:group, path: SecureRandom.hex(12) ) } let!(:old_name) { group.name } @@ -794,6 +823,7 @@ RSpec.describe GroupsController do context 'when transferring to a subgroup goes right' do let(:new_parent_group) { create(:group, :public) } + let(:group) { create(:group, :public) } let!(:group_member) { create(:group_member, :owner, group: group, user: user) } let!(:new_parent_group_member) { create(:group_member, :owner, group: new_parent_group, user: user) } @@ -805,11 +835,8 @@ RSpec.describe GroupsController do } end - it 'returns a notice' do + it 'returns a notice and redirects to the new path' do expect(flash[:notice]).to eq("Group '#{group.name}' was successfully transferred.") - end - - it 'redirects to the new path' do expect(response).to redirect_to("/#{new_parent_group.path}/#{group.path}") end end @@ -826,17 +853,15 @@ RSpec.describe GroupsController do } end - it 'returns a notice' do + it 'returns a notice and redirects to the new path' do expect(flash[:notice]).to eq("Group '#{group.name}' was successfully transferred.") - end - - it 'redirects to the new path' do expect(response).to redirect_to("/#{group.path}") end end context 'When the transfer goes wrong' do let(:new_parent_group) { create(:group, :public) } + let(:group) { create(:group, :public) } let!(:group_member) { create(:group_member, :owner, group: group, user: user) } let!(:new_parent_group_member) { create(:group_member, :owner, group: new_parent_group, user: user) } @@ -850,17 +875,15 @@ RSpec.describe GroupsController do } end - it 'returns an alert' do + it 'returns an alert and redirects to the current path' do expect(flash[:alert]).to eq "Transfer failed: namespace directory cannot be moved" - end - - it 'redirects to the current path' do expect(response).to redirect_to(edit_group_path(group)) end end context 'when the user is not allowed to transfer the group' do let(:new_parent_group) { create(:group, :public) } + let(:group) { create(:group, :public) } let!(:group_member) { create(:group_member, :guest, group: group, user: user) } let!(:new_parent_group_member) { create(:group_member, :guest, group: new_parent_group, user: user) } @@ -879,6 +902,7 @@ RSpec.describe GroupsController do context 'transferring when a project has container images' do let(:group) { create(:group, :public, :nested) } + let(:project) { create(:project, namespace: group) } let!(:group_member) { create(:group_member, :owner, group: group, user: user) } before do @@ -979,6 +1003,8 @@ RSpec.describe GroupsController do end context 'when there is no file available to download' do + let(:admin) { create(:admin) } + before do sign_in(admin) end @@ -1149,9 +1175,7 @@ RSpec.describe GroupsController do describe "GET #activity as JSON" do include DesignManagementTestHelpers - render_views - let(:project) { create(:project, :public, group: group) } let(:other_project) { create(:project, :public, group: group) } def get_activity diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb index 3049396dd0f..9ac42cbc3ec 100644 --- a/spec/controllers/help_controller_spec.rb +++ b/spec/controllers/help_controller_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe HelpController do + include StubVersion + let(:user) { create(:user) } before do @@ -108,8 +110,56 @@ RSpec.describe HelpController do end it 'renders HTML' do - expect(response).to render_template('show.html.haml') - expect(response.media_type).to eq 'text/html' + aggregate_failures do + expect(response).to render_template('show.html.haml') + expect(response.media_type).to eq 'text/html' + end + end + end + + context 'when a custom help_page_documentation_url is set' do + before do + stub_application_setting(help_page_documentation_base_url: documentation_base_url) + stub_version(gitlab_version, 'deadbeaf') + end + + subject { get :show, params: { path: path }, format: 'html' } + + let(:gitlab_version) { '13.4.0-ee' } + let(:documentation_base_url) { 'https://docs.gitlab.com' } + let(:path) { 'ssh/README' } + + it 'redirects user to custom documentation url with a specified version' do + is_expected.to redirect_to("#{documentation_base_url}/13.4/ee/#{path}.html") + end + + context 'when documentation url ends with a slash' do + let(:documentation_base_url) { 'https://docs.gitlab.com/' } + + it 'redirects user to custom documentation url without slash duplicates' do + is_expected.to redirect_to("https://docs.gitlab.com/13.4/ee/#{path}.html") + end + end + + context 'when it is a pre-release' do + let(:gitlab_version) { '13.4.0-pre' } + + it 'redirects user to custom documentation url without a version' do + is_expected.to redirect_to("#{documentation_base_url}/ee/#{path}.html") + end + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(help_page_documentation_redirect: false) + end + + it 'renders HTML' do + aggregate_failures do + is_expected.to render_template('show.html.haml') + expect(response.media_type).to eq 'text/html' + end + end end end @@ -129,9 +179,12 @@ RSpec.describe HelpController do path: 'user/img/markdown_logo' }, format: :png - expect(response).to be_successful - expect(response.media_type).to eq 'image/png' - expect(response.headers['Content-Disposition']).to match(/^inline;/) + + aggregate_failures do + expect(response).to be_successful + expect(response.media_type).to eq 'image/png' + expect(response.headers['Content-Disposition']).to match(/^inline;/) + end end end diff --git a/spec/controllers/import/bulk_imports_controller_spec.rb b/spec/controllers/import/bulk_imports_controller_spec.rb new file mode 100644 index 00000000000..f3850ff844e --- /dev/null +++ b/spec/controllers/import/bulk_imports_controller_spec.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Import::BulkImportsController do + let_it_be(:user) { create(:user) } + + before do + sign_in(user) + end + + context 'when user is signed in' do + context 'when bulk_import feature flag is enabled' do + before do + stub_feature_flags(bulk_import: true) + end + + describe 'POST configure' do + context 'when no params are passed in' do + it 'clears out existing session' do + post :configure + + expect(session[:bulk_import_gitlab_access_token]).to be_nil + expect(session[:bulk_import_gitlab_url]).to be_nil + + expect(response).to have_gitlab_http_status(:found) + expect(response).to redirect_to(status_import_bulk_import_url) + end + end + + it 'sets the session variables' do + token = 'token' + url = 'https://gitlab.example' + + post :configure, params: { bulk_import_gitlab_access_token: token, bulk_import_gitlab_url: url } + + expect(session[:bulk_import_gitlab_access_token]).to eq(token) + expect(session[:bulk_import_gitlab_url]).to eq(url) + expect(response).to have_gitlab_http_status(:found) + expect(response).to redirect_to(status_import_bulk_import_url) + end + + it 'strips access token with spaces' do + token = 'token' + + post :configure, params: { bulk_import_gitlab_access_token: " #{token} " } + + expect(session[:bulk_import_gitlab_access_token]).to eq(token) + expect(controller).to redirect_to(status_import_bulk_import_url) + end + end + + describe 'GET status' do + let(:client) { Gitlab::BulkImport::Client.new(uri: 'http://gitlab.example', token: 'token') } + + describe 'serialized group data' do + let(:client_response) do + [ + { 'id' => 1, 'full_name' => 'group1', 'full_path' => 'full/path/group1' }, + { 'id' => 2, 'full_name' => 'group2', 'full_path' => 'full/path/group2' } + ] + end + + before do + allow(controller).to receive(:client).and_return(client) + allow(client).to receive(:get).with('groups', top_level_only: true).and_return(client_response) + end + + it 'returns serialized group data' do + get :status, format: :json + + expect(response.parsed_body).to eq({ importable_data: client_response }.as_json) + end + end + + context 'when host url is local or not http' do + %w[https://localhost:3000 http://192.168.0.1 ftp://testing].each do |url| + before do + stub_application_setting(allow_local_requests_from_web_hooks_and_services: false) + + session[:bulk_import_gitlab_access_token] = 'test' + session[:bulk_import_gitlab_url] = url + end + + it 'denies network request' do + get :status + + expect(controller).to redirect_to(new_group_path) + expect(flash[:alert]).to eq('Specified URL cannot be used: "Only allowed schemes are http, https"') + end + end + + context 'when local requests are allowed' do + %w[https://localhost:3000 http://192.168.0.1].each do |url| + before do + stub_application_setting(allow_local_requests_from_web_hooks_and_services: true) + + session[:bulk_import_gitlab_access_token] = 'test' + session[:bulk_import_gitlab_url] = url + end + + it 'allows network request' do + get :status + + expect(response).to have_gitlab_http_status(:ok) + end + end + end + end + + context 'when connection error occurs' do + before do + allow(controller).to receive(:client).and_return(client) + allow(client).to receive(:get).and_raise(Gitlab::BulkImport::Client::ConnectionError) + end + + it 'returns 422' do + get :status, format: :json + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + + it 'clears session' do + get :status, format: :json + + expect(session[:gitlab_url]).to be_nil + expect(session[:gitlab_access_token]).to be_nil + end + end + end + end + + context 'when gitlab_api_imports feature flag is disabled' do + before do + stub_feature_flags(bulk_import: false) + end + + context 'POST configure' do + it 'returns 404' do + post :configure + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'GET status' do + it 'returns 404' do + get :status + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + end + + context 'when user is signed out' do + before do + sign_out(user) + end + + context 'POST configure' do + it 'redirects to sign in page' do + post :configure + + expect(response).to have_gitlab_http_status(:found) + expect(response).to redirect_to(new_user_session_path) + end + end + + context 'GET status' do + it 'redirects to sign in page' do + get :status + + expect(response).to have_gitlab_http_status(:found) + expect(response).to redirect_to(new_user_session_path) + end + end + end +end diff --git a/spec/controllers/import/manifest_controller_spec.rb b/spec/controllers/import/manifest_controller_spec.rb index ec8bd45b65c..6b21b45e698 100644 --- a/spec/controllers/import/manifest_controller_spec.rb +++ b/spec/controllers/import/manifest_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Import::ManifestController do +RSpec.describe Import::ManifestController, :clean_gitlab_redis_shared_state do include ImportSpecHelper let_it_be(:user) { create(:user) } @@ -16,42 +16,93 @@ RSpec.describe Import::ManifestController do sign_in(user) end - def assign_session_group - session[:manifest_import_repositories] = [] - session[:manifest_import_group_id] = group.id + describe 'POST upload' do + context 'with a valid manifest' do + it 'saves the manifest and redirects to the status page', :aggregate_failures do + post :upload, params: { + group_id: group.id, + manifest: fixture_file_upload('spec/fixtures/aosp_manifest.xml') + } + + metadata = Gitlab::ManifestImport::Metadata.new(user) + + expect(metadata.group_id).to eq(group.id) + expect(metadata.repositories.size).to eq(660) + expect(metadata.repositories.first).to include(name: 'platform/build', path: 'build/make') + + expect(response).to redirect_to(status_import_manifest_path) + end + end + + context 'with an invalid manifest' do + it 'displays an error' do + post :upload, params: { + group_id: group.id, + manifest: fixture_file_upload('spec/fixtures/invalid_manifest.xml') + } + + expect(assigns(:errors)).to be_present + end + end + + context 'when the user cannot create projects in the group' do + it 'displays an error' do + sign_in(create(:user)) + + post :upload, params: { + group_id: group.id, + manifest: fixture_file_upload('spec/fixtures/aosp_manifest.xml') + } + + expect(assigns(:errors)).to be_present + end + end end describe 'GET status' do - let(:repo1) { OpenStruct.new(id: 'test1', url: 'http://demo.host/test1') } - let(:repo2) { OpenStruct.new(id: 'test2', url: 'http://demo.host/test2') } + let(:repo1) { { id: 'test1', url: 'http://demo.host/test1' } } + let(:repo2) { { id: 'test2', url: 'http://demo.host/test2' } } let(:repos) { [repo1, repo2] } - before do - assign_session_group + shared_examples 'status action' do + it "returns variables for json request" do + project = create(:project, import_type: 'manifest', creator_id: user.id) - session[:manifest_import_repositories] = repos - end + get :status, format: :json - it "returns variables for json request" do - project = create(:project, import_type: 'manifest', creator_id: user.id) + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) + expect(json_response.dig("provider_repos", 0, "id")).to eq(repo1[:id]) + expect(json_response.dig("provider_repos", 1, "id")).to eq(repo2[:id]) + expect(json_response.dig("namespaces", 0, "id")).to eq(group.id) + end - get :status, format: :json + it "does not show already added project" do + project = create(:project, import_type: 'manifest', namespace: user.namespace, import_status: :finished, import_url: repo1[:url]) - expect(response).to have_gitlab_http_status(:ok) - expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) - expect(json_response.dig("provider_repos", 0, "id")).to eq(repo1.id) - expect(json_response.dig("provider_repos", 1, "id")).to eq(repo2.id) - expect(json_response.dig("namespaces", 0, "id")).to eq(group.id) + get :status, format: :json + + expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) + expect(json_response.dig("provider_repos").length).to eq(1) + expect(json_response.dig("provider_repos", 0, "id")).not_to eq(repo1[:id]) + end end - it "does not show already added project" do - project = create(:project, import_type: 'manifest', namespace: user.namespace, import_status: :finished, import_url: repo1.url) + context 'when the data is stored via Gitlab::ManifestImport::Metadata' do + before do + Gitlab::ManifestImport::Metadata.new(user).save(repos, group.id) + end + + include_examples 'status action' + end - get :status, format: :json + context 'when the data is stored in the user session' do + before do + session[:manifest_import_repositories] = repos + session[:manifest_import_group_id] = group.id + end - expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) - expect(json_response.dig("provider_repos").length).to eq(1) - expect(json_response.dig("provider_repos", 0, "id")).not_to eq(repo1.id) + include_examples 'status action' end end end diff --git a/spec/controllers/invites_controller_spec.rb b/spec/controllers/invites_controller_spec.rb index a083cfac981..75a972d2f95 100644 --- a/spec/controllers/invites_controller_spec.rb +++ b/spec/controllers/invites_controller_spec.rb @@ -17,8 +17,53 @@ RSpec.describe InvitesController, :snowplow do } end - before do - controller.instance_variable_set(:@member, member) + shared_examples 'invalid token' do + context 'when invite token is not valid' do + let(:params) { { id: '_bogus_token_' } } + + it 'renders the 404 page' do + request + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + shared_examples "tracks the 'accepted' event for the invitation reminders experiment" do + before do + stub_experiment(invitation_reminders: true) + allow(Gitlab::Experimentation).to receive(:enabled_for_attribute?).with(:invitation_reminders, member.invite_email).and_return(experimental_group) + end + + context 'when in the control group' do + let(:experimental_group) { false } + + it "tracks the 'accepted' event" do + request + + expect_snowplow_event( + category: 'Growth::Acquisition::Experiment::InvitationReminders', + label: md5_member_global_id, + property: 'control_group', + action: 'accepted' + ) + end + end + + context 'when in the experimental group' do + let(:experimental_group) { true } + + it "tracks the 'accepted' event" do + request + + expect_snowplow_event( + category: 'Growth::Acquisition::Experiment::InvitationReminders', + label: md5_member_global_id, + property: 'experimental_group', + action: 'accepted' + ) + end + end end describe 'GET #show' do @@ -39,7 +84,7 @@ RSpec.describe InvitesController, :snowplow do end it 'forces re-confirmation if email does not match signed in user' do - member.invite_email = 'bogus@email.com' + member.update!(invite_email: 'bogus@email.com') expect do request @@ -64,8 +109,8 @@ RSpec.describe InvitesController, :snowplow do it 'tracks the user as experiment group' do request - expect_snowplow_event(snowplow_event.merge(action: 'opened')) - expect_snowplow_event(snowplow_event.merge(action: 'accepted')) + expect_snowplow_event(**snowplow_event.merge(action: 'opened')) + expect_snowplow_event(**snowplow_event.merge(action: 'accepted')) end end @@ -76,10 +121,13 @@ RSpec.describe InvitesController, :snowplow do it 'tracks the user as control group' do request - expect_snowplow_event(snowplow_event.merge(action: 'opened')) - expect_snowplow_event(snowplow_event.merge(action: 'accepted')) + expect_snowplow_event(**snowplow_event.merge(action: 'opened')) + expect_snowplow_event(**snowplow_event.merge(action: 'accepted')) end end + + it_behaves_like "tracks the 'accepted' event for the invitation reminders experiment" + it_behaves_like 'invalid token' end context 'when not logged in' do @@ -125,7 +173,7 @@ RSpec.describe InvitesController, :snowplow do it 'tracks the user as experiment group' do request - expect_snowplow_event(snowplow_event.merge(action: 'accepted')) + expect_snowplow_event(**snowplow_event.merge(action: 'accepted')) end end @@ -136,8 +184,31 @@ RSpec.describe InvitesController, :snowplow do it 'tracks the user as control group' do request - expect_snowplow_event(snowplow_event.merge(action: 'accepted')) + expect_snowplow_event(**snowplow_event.merge(action: 'accepted')) end end + + it_behaves_like "tracks the 'accepted' event for the invitation reminders experiment" + it_behaves_like 'invalid token' + end + + describe 'POST #decline for link in UI' do + before do + sign_in(user) + end + + subject(:request) { post :decline, params: params } + + it_behaves_like 'invalid token' + end + + describe 'GET #decline for link in email' do + before do + sign_in(user) + end + + subject(:request) { get :decline, params: params } + + it_behaves_like 'invalid token' end end diff --git a/spec/controllers/jira_connect/events_controller_spec.rb b/spec/controllers/jira_connect/events_controller_spec.rb index d1a2dd6e7af..8a07f69e480 100644 --- a/spec/controllers/jira_connect/events_controller_spec.rb +++ b/spec/controllers/jira_connect/events_controller_spec.rb @@ -4,14 +4,20 @@ require 'spec_helper' RSpec.describe JiraConnect::EventsController do describe '#installed' do - subject do - post :installed, params: { - clientKey: '1234', - sharedSecret: 'secret', + let(:client_key) { '1234' } + let(:shared_secret) { 'secret' } + let(:params) do + { + clientKey: client_key, + sharedSecret: shared_secret, baseUrl: 'https://test.atlassian.net' } end + subject do + post :installed, params: params + end + it 'saves the jira installation data' do expect { subject }.to change { JiraConnectInstallation.count }.by(1) end @@ -19,15 +25,15 @@ RSpec.describe JiraConnect::EventsController do it 'saves the correct values' do subject - installation = JiraConnectInstallation.find_by_client_key('1234') + installation = JiraConnectInstallation.find_by_client_key(client_key) - expect(installation.shared_secret).to eq('secret') + expect(installation.shared_secret).to eq(shared_secret) expect(installation.base_url).to eq('https://test.atlassian.net') end context 'client key already exists' do it 'returns 422' do - create(:jira_connect_installation, client_key: '1234') + create(:jira_connect_installation, client_key: client_key) subject @@ -35,6 +41,23 @@ RSpec.describe JiraConnect::EventsController do end end + context 'when it is a version update and shared_secret is not sent' do + let(:params) do + { + clientKey: client_key, + baseUrl: 'https://test.atlassian.net' + } + end + + it 'validates the JWT token in authorization header and returns 200 without creating a new installation' do + create(:jira_connect_installation, client_key: client_key, shared_secret: shared_secret) + request.headers["Authorization"] = "Bearer #{Atlassian::Jwt.encode({ iss: client_key }, shared_secret)}" + + expect { subject }.not_to change { JiraConnectInstallation.count } + expect(response).to have_gitlab_http_status(:ok) + end + end + describe '#uninstalled' do let!(:installation) { create(:jira_connect_installation) } let(:qsh) { Atlassian::Jwt.create_query_string_hash('https://gitlab.test/events/uninstalled', 'POST', 'https://gitlab.test') } diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb index e08c92da87f..249e6322d1c 100644 --- a/spec/controllers/profiles_controller_spec.rb +++ b/spec/controllers/profiles_controller_spec.rb @@ -101,6 +101,19 @@ RSpec.describe ProfilesController, :request_store do end end + describe 'GET audit_log' do + it 'tracks search event', :snowplow do + sign_in(user) + + get :audit_log + + expect_snowplow_event( + category: 'ProfilesController', + action: 'search_audit_event' + ) + end + end + describe 'PUT update_username' do let(:namespace) { user.namespace } let(:gitlab_shell) { Gitlab::Shell.new } diff --git a/spec/controllers/projects/alert_management_controller_spec.rb b/spec/controllers/projects/alert_management_controller_spec.rb index 6a1952f949b..d80147b5c59 100644 --- a/spec/controllers/projects/alert_management_controller_spec.rb +++ b/spec/controllers/projects/alert_management_controller_spec.rb @@ -42,7 +42,7 @@ RSpec.describe Projects::AlertManagementController do let(:role) { :reporter } it 'shows 404' do - get :index, params: { namespace_id: project.namespace, project_id: project } + get :details, params: { namespace_id: project.namespace, project_id: project, id: id } expect(response).to have_gitlab_http_status(:not_found) end diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index b998dee09b2..a56425c2a22 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -349,7 +349,7 @@ RSpec.describe Projects::BlobController do end it_behaves_like 'tracking unique hll events', :track_editor_edit_actions do - subject { put :update, params: default_params, format: format } + subject(:request) { put :update, params: default_params } let(:target_id) { 'g_edit_by_sfe' } let(:expected_type) { instance_of(Integer) } @@ -465,7 +465,7 @@ RSpec.describe Projects::BlobController do end it_behaves_like 'tracking unique hll events', :track_editor_edit_actions do - subject { post :create, params: default_params, format: format } + subject(:request) { post :create, params: default_params } let(:target_id) { 'g_edit_by_sfe' } let(:expected_type) { instance_of(Integer) } diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb index 51a451570c5..52cd6869b04 100644 --- a/spec/controllers/projects/clusters_controller_spec.rb +++ b/spec/controllers/projects/clusters_controller_spec.rb @@ -251,6 +251,7 @@ RSpec.describe Projects::ClustersController do cluster: { name: 'new-cluster', managed: '1', + namespace_per_environment: '0', provider_gcp_attributes: { gcp_project_id: 'gcp-project-12345', legacy_abac: legacy_abac_param @@ -278,6 +279,7 @@ RSpec.describe Projects::ClustersController do expect(project.clusters.first).to be_kubernetes expect(project.clusters.first.provider_gcp).to be_legacy_abac expect(project.clusters.first.managed?).to be_truthy + expect(project.clusters.first.namespace_per_environment?).to be_falsy end context 'when legacy_abac param is false' do @@ -369,6 +371,7 @@ RSpec.describe Projects::ClustersController do expect(project.clusters.first).to be_user expect(project.clusters.first).to be_kubernetes + expect(project.clusters.first).to be_namespace_per_environment end end @@ -400,6 +403,7 @@ RSpec.describe Projects::ClustersController do expect(cluster).to be_user expect(cluster).to be_kubernetes expect(cluster).to be_platform_kubernetes_rbac + expect(cluster).to be_namespace_per_environment end end @@ -726,6 +730,7 @@ RSpec.describe Projects::ClustersController do enabled: false, name: 'my-new-cluster-name', managed: false, + namespace_per_environment: false, platform_kubernetes_attributes: { namespace: 'my-namespace' } @@ -742,6 +747,7 @@ RSpec.describe Projects::ClustersController do expect(cluster.enabled).to be_falsey expect(cluster.name).to eq('my-new-cluster-name') expect(cluster).not_to be_managed + expect(cluster).not_to be_namespace_per_environment expect(cluster.platform_kubernetes.namespace).to eq('my-namespace') end diff --git a/spec/controllers/projects/feature_flags_clients_controller_spec.rb b/spec/controllers/projects/feature_flags_clients_controller_spec.rb new file mode 100644 index 00000000000..f527d2ba430 --- /dev/null +++ b/spec/controllers/projects/feature_flags_clients_controller_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::FeatureFlagsClientsController do + include Gitlab::Routing + + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + describe 'POST reset_token.json' do + subject(:reset_token) do + post :reset_token, + params: { namespace_id: project.namespace, project_id: project }, + format: :json + end + + before do + sign_in(user) + end + + context 'when user is a project maintainer' do + before do + project.add_maintainer(user) + end + + context 'and feature flags client exist' do + it 'regenerates feature flags client token' do + project.create_operations_feature_flags_client! + expect { reset_token }.to change { project.reload.feature_flags_client_token } + + expect(json_response['token']).to eq(project.feature_flags_client_token) + end + end + + context 'but feature flags client does not exist' do + it 'returns 404' do + reset_token + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'when user is not a project maintainer' do + before do + project.add_developer(user) + end + + it 'returns 404' do + reset_token + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end +end diff --git a/spec/controllers/projects/feature_flags_controller_spec.rb b/spec/controllers/projects/feature_flags_controller_spec.rb new file mode 100644 index 00000000000..96eeb6f239f --- /dev/null +++ b/spec/controllers/projects/feature_flags_controller_spec.rb @@ -0,0 +1,1604 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::FeatureFlagsController do + include Gitlab::Routing + include FeatureFlagHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:developer) { create(:user) } + let_it_be(:reporter) { create(:user) } + let(:user) { developer } + + before_all do + project.add_developer(developer) + project.add_reporter(reporter) + end + + before do + sign_in(user) + end + + describe 'GET index' do + render_views + + subject { get(:index, params: view_params) } + + context 'when there is no feature flags' do + it 'responds with success' do + is_expected.to have_gitlab_http_status(:ok) + end + end + + context 'for a list of feature flags' do + let!(:feature_flags) { create_list(:operations_feature_flag, 50, project: project) } + + it 'responds with success' do + is_expected.to have_gitlab_http_status(:ok) + end + end + + context 'when the user is a reporter' do + let(:user) { reporter } + + it 'responds with not found' do + is_expected.to have_gitlab_http_status(:not_found) + end + end + end + + describe 'GET #index.json' do + subject { get(:index, params: view_params, format: :json) } + + let!(:feature_flag_active) do + create(:operations_feature_flag, project: project, active: true, name: 'feature_flag_a') + end + + let!(:feature_flag_inactive) do + create(:operations_feature_flag, project: project, active: false, name: 'feature_flag_b') + end + + it 'returns all feature flags as json response' do + subject + + expect(json_response['feature_flags'].count).to eq(2) + expect(json_response['feature_flags'].first['name']).to eq(feature_flag_active.name) + expect(json_response['feature_flags'].second['name']).to eq(feature_flag_inactive.name) + end + + it 'returns CRUD paths' do + subject + + expected_edit_path = edit_project_feature_flag_path(project, feature_flag_active) + expected_update_path = project_feature_flag_path(project, feature_flag_active) + expected_destroy_path = project_feature_flag_path(project, feature_flag_active) + + feature_flag_json = json_response['feature_flags'].first + + expect(feature_flag_json['edit_path']).to eq(expected_edit_path) + expect(feature_flag_json['update_path']).to eq(expected_update_path) + expect(feature_flag_json['destroy_path']).to eq(expected_destroy_path) + end + + it 'returns the summary of feature flags' do + subject + + expect(json_response['count']['all']).to eq(2) + expect(json_response['count']['enabled']).to eq(1) + expect(json_response['count']['disabled']).to eq(1) + end + + it 'matches json schema' do + is_expected.to match_response_schema('feature_flags') + end + + it 'returns false for active when the feature flag is inactive even if it has an active scope' do + create(:operations_feature_flag_scope, + feature_flag: feature_flag_inactive, + environment_scope: 'production', + active: true) + + subject + + expect(response).to have_gitlab_http_status(:ok) + feature_flag_json = json_response['feature_flags'].second + + expect(feature_flag_json['active']).to eq(false) + end + + it 'returns the feature flag iid' do + subject + + feature_flag_json = json_response['feature_flags'].first + + expect(feature_flag_json['iid']).to eq(feature_flag_active.iid) + end + + context 'when scope is specified' do + let(:view_params) do + { namespace_id: project.namespace, project_id: project, scope: scope } + end + + context 'when all feature flags are requested' do + let(:scope) { 'all' } + + it 'returns all feature flags' do + subject + + expect(json_response['feature_flags'].count).to eq(2) + end + end + + context 'when enabled feature flags are requested' do + let(:scope) { 'enabled' } + + it 'returns enabled feature flags' do + subject + + expect(json_response['feature_flags'].count).to eq(1) + expect(json_response['feature_flags'].first['active']).to be_truthy + end + end + + context 'when disabled feature flags are requested' do + let(:scope) { 'disabled' } + + it 'returns disabled feature flags' do + subject + + expect(json_response['feature_flags'].count).to eq(1) + expect(json_response['feature_flags'].first['active']).to be_falsy + end + end + end + + context 'when feature flags have additional scopes' do + let!(:feature_flag_active_scope) do + create(:operations_feature_flag_scope, + feature_flag: feature_flag_active, + environment_scope: 'production', + active: false) + end + + let!(:feature_flag_inactive_scope) do + create(:operations_feature_flag_scope, + feature_flag: feature_flag_inactive, + environment_scope: 'staging', + active: false) + end + + it 'returns a correct summary' do + subject + + expect(json_response['count']['all']).to eq(2) + expect(json_response['count']['enabled']).to eq(1) + expect(json_response['count']['disabled']).to eq(1) + end + + it 'recognizes feature flag 1 as active' do + subject + + expect(json_response['feature_flags'].first['active']).to be_truthy + end + + it 'recognizes feature flag 2 as inactive' do + subject + + expect(json_response['feature_flags'].second['active']).to be_falsy + end + + it 'has ordered scopes' do + subject + + expect(json_response['feature_flags'][0]['scopes'][0]['id']) + .to be < json_response['feature_flags'][0]['scopes'][1]['id'] + expect(json_response['feature_flags'][1]['scopes'][0]['id']) + .to be < json_response['feature_flags'][1]['scopes'][1]['id'] + end + + it 'does not have N+1 problem' do + recorded = ActiveRecord::QueryRecorder.new { subject } + + related_count = recorded.log + .count { |query| query.include?('operations_feature_flag') } + + expect(related_count).to be_within(5).of(2) + end + end + + context 'with version 1 and 2 feature flags' do + let!(:new_version_feature_flag) do + create(:operations_feature_flag, :new_version_flag, project: project, name: 'feature_flag_c') + end + + it 'returns all feature flags as json response' do + subject + + expect(json_response['feature_flags'].count).to eq(3) + end + + it 'returns only version 1 flags when new version flags are disabled' do + stub_feature_flags(feature_flags_new_version: false) + + subject + + expected = [feature_flag_active.name, feature_flag_inactive.name].sort + expect(json_response['feature_flags'].map { |f| f['name'] }.sort).to eq(expected) + end + end + end + + describe 'GET new' do + render_views + + subject { get(:new, params: view_params) } + + it 'renders the form' do + is_expected.to have_gitlab_http_status(:ok) + end + end + + describe 'GET #show.json' do + subject { get(:show, params: params, format: :json) } + + let!(:feature_flag) do + create(:operations_feature_flag, project: project) + end + + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + iid: feature_flag.iid + } + end + + it 'returns the feature flag as json response' do + subject + + expect(json_response['name']).to eq(feature_flag.name) + expect(json_response['active']).to eq(feature_flag.active) + expect(json_response['version']).to eq('legacy_flag') + end + + it 'matches json schema' do + is_expected.to match_response_schema('feature_flag') + end + + it 'routes based on iid' do + other_project = create(:project) + other_project.add_developer(user) + other_feature_flag = create(:operations_feature_flag, project: other_project, + name: 'other_flag') + params = { + namespace_id: other_project.namespace, + project_id: other_project, + iid: other_feature_flag.iid + } + + get(:show, params: params, format: :json) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['name']).to eq(other_feature_flag.name) + end + + it 'routes based on iid when new version flags are disabled' do + stub_feature_flags(feature_flags_new_version: false) + other_project = create(:project) + other_project.add_developer(user) + other_feature_flag = create(:operations_feature_flag, project: other_project, + name: 'other_flag') + params = { + namespace_id: other_project.namespace, + project_id: other_project, + iid: other_feature_flag.iid + } + + get(:show, params: params, format: :json) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['name']).to eq(other_feature_flag.name) + end + + context 'when feature flag is not found' do + let!(:feature_flag) { } + + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + iid: 1 + } + end + + it 'returns 404' do + is_expected.to have_gitlab_http_status(:not_found) + end + end + + context 'when user is reporter' do + let(:user) { reporter } + + it 'returns 404' do + is_expected.to have_gitlab_http_status(:not_found) + end + end + + context 'when feature flags have additional scopes' do + context 'when there is at least one active scope' do + let!(:feature_flag) do + create(:operations_feature_flag, project: project, active: false) + end + + let!(:feature_flag_scope_production) do + create(:operations_feature_flag_scope, + feature_flag: feature_flag, + environment_scope: 'review/*', + active: true) + end + + it 'returns false for active' do + subject + + expect(json_response['active']).to eq(false) + end + end + + context 'when all scopes are inactive' do + let!(:feature_flag) do + create(:operations_feature_flag, project: project, active: false) + end + + let!(:feature_flag_scope_production) do + create(:operations_feature_flag_scope, + feature_flag: feature_flag, + environment_scope: 'production', + active: false) + end + + it 'recognizes the feature flag as inactive' do + subject + + expect(json_response['active']).to be_falsy + end + end + end + + context 'with a version 2 feature flag' do + let!(:new_version_feature_flag) do + create(:operations_feature_flag, :new_version_flag, project: project) + end + + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + iid: new_version_feature_flag.iid + } + end + + it 'returns the feature flag' do + subject + + expect(json_response['name']).to eq(new_version_feature_flag.name) + expect(json_response['active']).to eq(new_version_feature_flag.active) + expect(json_response['version']).to eq('new_version_flag') + end + + it 'returns a 404 when new version flags are disabled' do + stub_feature_flags(feature_flags_new_version: false) + + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns strategies ordered by id' do + first_strategy = create(:operations_strategy, feature_flag: new_version_feature_flag) + second_strategy = create(:operations_strategy, feature_flag: new_version_feature_flag) + + subject + + expect(json_response['strategies'].map { |s| s['id'] }).to eq([first_strategy.id, second_strategy.id]) + end + end + end + + describe 'POST create.json' do + subject { post(:create, params: params, format: :json) } + + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + operations_feature_flag: { + name: 'my_feature_flag', + active: true + } + } + end + + it 'returns 200' do + is_expected.to have_gitlab_http_status(:ok) + end + + it 'creates a new feature flag' do + subject + + expect(json_response['name']).to eq('my_feature_flag') + expect(json_response['active']).to be_truthy + end + + it 'creates a default scope' do + subject + + expect(json_response['scopes'].count).to eq(1) + expect(json_response['scopes'].first['environment_scope']).to eq('*') + expect(json_response['scopes'].first['active']).to be_truthy + end + + it 'matches json schema' do + is_expected.to match_response_schema('feature_flag') + end + + context 'when the same named feature flag has already existed' do + before do + create(:operations_feature_flag, name: 'my_feature_flag', project: project) + end + + it 'returns 400' do + is_expected.to have_gitlab_http_status(:bad_request) + end + + it 'returns an error message' do + subject + + expect(json_response['message']).to include('Name has already been taken') + end + end + + context 'without the active parameter' do + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + operations_feature_flag: { + name: 'my_feature_flag' + } + } + end + + it 'creates a flag with active set to true' do + expect { subject }.to change { Operations::FeatureFlag.count }.by(1) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('feature_flag') + expect(json_response['active']).to eq(true) + expect(Operations::FeatureFlag.last.active).to eq(true) + end + end + + context 'when user is reporter' do + let(:user) { reporter } + + it 'returns 404' do + is_expected.to have_gitlab_http_status(:not_found) + end + end + + context 'when creates additional scope' do + let(:params) do + view_params.merge({ + operations_feature_flag: { + name: 'my_feature_flag', + active: true, + scopes_attributes: [{ environment_scope: '*', active: true }, + { environment_scope: 'production', active: false }] + } + }) + end + + it 'creates feature flag scopes successfully' do + expect { subject }.to change { Operations::FeatureFlagScope.count }.by(2) + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'creates feature flag scopes in a correct order' do + subject + + expect(json_response['scopes'].first['environment_scope']).to eq('*') + expect(json_response['scopes'].second['environment_scope']).to eq('production') + end + + context 'when default scope is not placed first' do + let(:params) do + view_params.merge({ + operations_feature_flag: { + name: 'my_feature_flag', + active: true, + scopes_attributes: [{ environment_scope: 'production', active: false }, + { environment_scope: '*', active: true }] + } + }) + end + + it 'returns 400' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']) + .to include('Default scope has to be the first element') + end + end + end + + context 'when creates additional scope with a percentage rollout' do + it 'creates a strategy for the scope' do + params = view_params.merge({ + operations_feature_flag: { + name: 'my_feature_flag', + active: true, + scopes_attributes: [{ environment_scope: '*', active: true }, + { environment_scope: 'production', active: false, + strategies: [{ name: 'gradualRolloutUserId', + parameters: { groupId: 'default', percentage: '42' } }] }] + } + }) + + post(:create, params: params, format: :json) + + expect(response).to have_gitlab_http_status(:ok) + production_strategies_json = json_response['scopes'].second['strategies'] + expect(production_strategies_json).to eq([{ + 'name' => 'gradualRolloutUserId', + 'parameters' => { "groupId" => "default", "percentage" => "42" } + }]) + end + end + + context 'when creates additional scope with a userWithId strategy' do + it 'creates a strategy for the scope' do + params = view_params.merge({ + operations_feature_flag: { + name: 'my_feature_flag', + active: true, + scopes_attributes: [{ environment_scope: '*', active: true }, + { environment_scope: 'production', active: false, + strategies: [{ name: 'userWithId', + parameters: { userIds: '123,4,6722' } }] }] + } + }) + + post(:create, params: params, format: :json) + + expect(response).to have_gitlab_http_status(:ok) + production_strategies_json = json_response['scopes'].second['strategies'] + expect(production_strategies_json).to eq([{ + 'name' => 'userWithId', + 'parameters' => { "userIds" => "123,4,6722" } + }]) + end + end + + context 'when creates an additional scope without a strategy' do + it 'creates a default strategy' do + params = view_params.merge({ + operations_feature_flag: { + name: 'my_feature_flag', + active: true, + scopes_attributes: [{ environment_scope: '*', active: true }] + } + }) + + post(:create, params: params, format: :json) + + expect(response).to have_gitlab_http_status(:ok) + default_strategies_json = json_response['scopes'].first['strategies'] + expect(default_strategies_json).to eq([{ "name" => "default", "parameters" => {} }]) + end + end + + context 'when creating a version 2 feature flag' do + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + operations_feature_flag: { + name: 'my_feature_flag', + active: true, + version: 'new_version_flag' + } + } + end + + it 'creates a new feature flag' do + subject + + expect(json_response['name']).to eq('my_feature_flag') + expect(json_response['active']).to be_truthy + expect(json_response['version']).to eq('new_version_flag') + end + end + + context 'when creating a version 2 feature flag with strategies and scopes' do + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + operations_feature_flag: { + name: 'my_feature_flag', + active: true, + version: 'new_version_flag', + strategies_attributes: [{ + name: 'userWithId', + parameters: { userIds: 'user1' }, + scopes_attributes: [{ environment_scope: '*' }] + }] + } + } + end + + it 'creates a new feature flag with the strategies and scopes' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['name']).to eq('my_feature_flag') + expect(json_response['active']).to eq(true) + expect(json_response['strategies'].count).to eq(1) + + strategy_json = json_response['strategies'].first + expect(strategy_json).to have_key('id') + expect(strategy_json['name']).to eq('userWithId') + expect(strategy_json['parameters']).to eq({ 'userIds' => 'user1' }) + expect(strategy_json['scopes'].count).to eq(1) + + scope_json = strategy_json['scopes'].first + expect(scope_json).to have_key('id') + expect(scope_json['environment_scope']).to eq('*') + end + end + + context 'when creating a version 2 feature flag with a gradualRolloutUserId strategy' do + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + operations_feature_flag: { + name: 'my_feature_flag', + active: true, + version: 'new_version_flag', + strategies_attributes: [{ + name: 'gradualRolloutUserId', + parameters: { groupId: 'default', percentage: '15' }, + scopes_attributes: [{ environment_scope: 'production' }] + }] + } + } + end + + it 'creates the new strategy' do + subject + + expect(response).to have_gitlab_http_status(:ok) + + strategy_json = json_response['strategies'].first + expect(strategy_json['name']).to eq('gradualRolloutUserId') + expect(strategy_json['parameters']).to eq({ 'groupId' => 'default', 'percentage' => '15' }) + expect(strategy_json['scopes'].count).to eq(1) + + scope_json = strategy_json['scopes'].first + expect(scope_json['environment_scope']).to eq('production') + end + end + + context 'when creating a version 2 feature flag with a flexibleRollout strategy' do + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + operations_feature_flag: { + name: 'my_feature_flag', + active: true, + version: 'new_version_flag', + strategies_attributes: [{ + name: 'flexibleRollout', + parameters: { groupId: 'default', rollout: '15', stickiness: 'DEFAULT' }, + scopes_attributes: [{ environment_scope: 'production' }] + }] + } + } + end + + it 'creates the new strategy' do + subject + + expect(response).to have_gitlab_http_status(:ok) + + strategy_json = json_response['strategies'].first + expect(strategy_json['name']).to eq('flexibleRollout') + expect(strategy_json['parameters']).to eq({ 'groupId' => 'default', 'rollout' => '15', 'stickiness' => 'DEFAULT' }) + expect(strategy_json['scopes'].count).to eq(1) + + scope_json = strategy_json['scopes'].first + expect(scope_json['environment_scope']).to eq('production') + end + end + + context 'when creating a version 2 feature flag with a gitlabUserList strategy' do + let!(:user_list) do + create(:operations_feature_flag_user_list, project: project, + name: 'My List', user_xids: 'user1,user2') + end + + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + operations_feature_flag: { + name: 'my_feature_flag', + active: true, + version: 'new_version_flag', + strategies_attributes: [{ + name: 'gitlabUserList', + parameters: {}, + user_list_id: user_list.id, + scopes_attributes: [{ environment_scope: 'production' }] + }] + } + } + end + + it 'creates the new strategy' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['strategies']).to match([a_hash_including({ + 'name' => 'gitlabUserList', + 'parameters' => {}, + 'user_list' => { + 'id' => user_list.id, + 'iid' => user_list.iid, + 'name' => 'My List', + 'user_xids' => 'user1,user2' + }, + 'scopes' => [a_hash_including({ + 'environment_scope' => 'production' + })] + })]) + end + end + + context 'when version parameter is invalid' do + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + operations_feature_flag: { + name: 'my_feature_flag', + active: true, + version: 'bad_version' + } + } + end + + it 'returns a 400' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response).to eq({ 'message' => 'Version is invalid' }) + expect(Operations::FeatureFlag.count).to eq(0) + end + end + + context 'when version 2 flags are disabled' do + context 'and attempting to create a version 2 flag' do + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + operations_feature_flag: { + name: 'my_feature_flag', + active: true, + version: 'new_version_flag' + } + } + end + + it 'returns a 400' do + stub_feature_flags(feature_flags_new_version: false) + + subject + + expect(response).to have_gitlab_http_status(:bad_request) + expect(Operations::FeatureFlag.count).to eq(0) + end + end + + context 'and attempting to create a version 1 flag' do + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + operations_feature_flag: { + name: 'my_feature_flag', + active: true + } + } + end + + it 'creates the flag' do + stub_feature_flags(feature_flags_new_version: false) + + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(Operations::FeatureFlag.count).to eq(1) + expect(json_response['version']).to eq('legacy_flag') + end + end + end + end + + describe 'DELETE destroy.json' do + subject { delete(:destroy, params: params, format: :json) } + + let!(:feature_flag) { create(:operations_feature_flag, project: project) } + + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + iid: feature_flag.iid + } + end + + it 'returns 200' do + is_expected.to have_gitlab_http_status(:ok) + end + + it 'deletes one feature flag' do + expect { subject }.to change { Operations::FeatureFlag.count }.by(-1) + end + + it 'destroys the default scope' do + expect { subject }.to change { Operations::FeatureFlagScope.count }.by(-1) + end + + it 'matches json schema' do + is_expected.to match_response_schema('feature_flag') + end + + context 'when user is reporter' do + let(:user) { reporter } + + it 'returns 404' do + is_expected.to have_gitlab_http_status(:not_found) + end + end + + context 'when the feature flag does not exist' do + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + iid: 0 + } + end + + it 'returns not found' do + is_expected.to have_gitlab_http_status(:not_found) + end + end + + context 'when there is an additional scope' do + let!(:scope) { create_scope(feature_flag, 'production', false) } + + it 'destroys the default scope and production scope' do + expect { subject }.to change { Operations::FeatureFlagScope.count }.by(-2) + end + end + + context 'with a version 2 flag' do + let!(:new_version_flag) { create(:operations_feature_flag, :new_version_flag, project: project) } + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + iid: new_version_flag.iid + } + end + + it 'deletes the flag' do + expect { subject }.to change { Operations::FeatureFlag.count }.by(-1) + end + + context 'when new version flags are disabled' do + it 'returns a 404' do + stub_feature_flags(feature_flags_new_version: false) + + expect { subject }.not_to change { Operations::FeatureFlag.count } + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + end + + describe 'PUT update.json' do + def put_request(feature_flag, feature_flag_params) + params = { + namespace_id: project.namespace, + project_id: project, + iid: feature_flag.iid, + operations_feature_flag: feature_flag_params + } + + put(:update, params: params, format: :json, as: :json) + end + + before do + stub_feature_flags( + feature_flags_legacy_read_only: false, + feature_flags_legacy_read_only_override: false + ) + end + + subject { put(:update, params: params, format: :json) } + + let!(:feature_flag) do + create(:operations_feature_flag, + :legacy_flag, + name: 'ci_live_trace', + active: true, + project: project) + end + + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + iid: feature_flag.iid, + operations_feature_flag: { + name: 'ci_new_live_trace' + } + } + end + + it 'returns 200' do + is_expected.to have_gitlab_http_status(:ok) + end + + it 'updates the name of the feature flag name' do + subject + + expect(json_response['name']).to eq('ci_new_live_trace') + end + + it 'matches json schema' do + is_expected.to match_response_schema('feature_flag') + end + + context 'when updates active' do + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + iid: feature_flag.iid, + operations_feature_flag: { + active: false + } + } + end + + it 'updates active from true to false' do + expect { subject } + .to change { feature_flag.reload.active }.from(true).to(false) + end + + it "does not change default scope's active" do + expect { subject } + .not_to change { feature_flag.default_scope.reload.active }.from(true) + end + + it 'updates active from false to true when an inactive feature flag has an active scope' do + feature_flag = create(:operations_feature_flag, project: project, name: 'my_flag', active: false) + create(:operations_feature_flag_scope, feature_flag: feature_flag, environment_scope: 'production', active: true) + + put_request(feature_flag, { active: true }) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('feature_flag') + expect(json_response['active']).to eq(true) + expect(feature_flag.reload.active).to eq(true) + expect(feature_flag.default_scope.reload.active).to eq(false) + end + end + + context 'when user is reporter' do + let(:user) { reporter } + + it 'returns 404' do + is_expected.to have_gitlab_http_status(:not_found) + end + end + + context "when creates an additional scope for production environment" do + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + iid: feature_flag.iid, + operations_feature_flag: { + scopes_attributes: [{ environment_scope: 'production', active: false }] + } + } + end + + it 'creates a production scope' do + expect { subject }.to change { feature_flag.reload.scopes.count }.by(1) + + expect(json_response['scopes'].last['environment_scope']).to eq('production') + expect(json_response['scopes'].last['active']).to be_falsy + end + end + + context "when creates a default scope" do + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + iid: feature_flag.iid, + operations_feature_flag: { + scopes_attributes: [{ environment_scope: '*', active: false }] + } + } + end + + it 'returns 400' do + is_expected.to have_gitlab_http_status(:bad_request) + end + end + + context "when updates a default scope's active value" do + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + iid: feature_flag.iid, + operations_feature_flag: { + scopes_attributes: [ + { + id: feature_flag.default_scope.id, + environment_scope: '*', + active: false + } + ] + } + } + end + + it "updates successfully" do + subject + + expect(json_response['scopes'].first['environment_scope']).to eq('*') + expect(json_response['scopes'].first['active']).to be_falsy + end + end + + context "when changes default scope's spec" do + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + iid: feature_flag.iid, + operations_feature_flag: { + scopes_attributes: [ + { + id: feature_flag.default_scope.id, + environment_scope: 'review/*' + } + ] + } + } + end + + it 'returns 400' do + is_expected.to have_gitlab_http_status(:bad_request) + end + end + + context "when destroys the default scope" do + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + iid: feature_flag.iid, + operations_feature_flag: { + scopes_attributes: [ + { + id: feature_flag.default_scope.id, + _destroy: 1 + } + ] + } + } + end + + it 'raises an error' do + expect { subject }.to raise_error(ActiveRecord::ReadOnlyRecord) + end + end + + context "when destroys a production scope" do + let!(:production_scope) { create_scope(feature_flag, 'production', true) } + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + iid: feature_flag.iid, + operations_feature_flag: { + scopes_attributes: [ + { + id: production_scope.id, + _destroy: 1 + } + ] + } + } + end + + it 'destroys successfully' do + subject + + scopes = json_response['scopes'] + expect(scopes.any? { |scope| scope['environment_scope'] == 'production' }) + .to be_falsy + end + end + + describe "updating the strategy" do + it 'creates a default strategy' do + scope = create_scope(feature_flag, 'production', true, []) + + put_request(feature_flag, scopes_attributes: [{ + id: scope.id, + strategies: [{ name: 'default', parameters: {} }] + }]) + + expect(response).to have_gitlab_http_status(:ok) + scope_json = json_response['scopes'].find do |s| + s['environment_scope'] == 'production' + end + expect(scope_json['strategies']).to eq([{ + "name" => "default", + "parameters" => {} + }]) + end + + it 'creates a gradualRolloutUserId strategy' do + scope = create_scope(feature_flag, 'production', true, []) + + put_request(feature_flag, scopes_attributes: [{ + id: scope.id, + strategies: [{ name: 'gradualRolloutUserId', + parameters: { groupId: 'default', percentage: "70" } }] + }]) + + expect(response).to have_gitlab_http_status(:ok) + scope_json = json_response['scopes'].find do |s| + s['environment_scope'] == 'production' + end + expect(scope_json['strategies']).to eq([{ + "name" => "gradualRolloutUserId", + "parameters" => { + "groupId" => "default", + "percentage" => "70" + } + }]) + end + + it 'creates a userWithId strategy' do + scope = create_scope(feature_flag, 'production', true, [{ name: 'default', parameters: {} }]) + + put_request(feature_flag, scopes_attributes: [{ + id: scope.id, + strategies: [{ name: 'userWithId', parameters: { userIds: 'sam,fred' } }] + }]) + + expect(response).to have_gitlab_http_status(:ok) + scope_json = json_response['scopes'].find do |s| + s['environment_scope'] == 'production' + end + expect(scope_json['strategies']).to eq([{ + "name" => "userWithId", + "parameters" => { "userIds" => "sam,fred" } + }]) + end + + it 'updates an existing strategy' do + scope = create_scope(feature_flag, 'production', true, [{ name: 'default', parameters: {} }]) + + put_request(feature_flag, scopes_attributes: [{ + id: scope.id, + strategies: [{ name: 'gradualRolloutUserId', + parameters: { groupId: 'default', percentage: "50" } }] + }]) + + expect(response).to have_gitlab_http_status(:ok) + scope_json = json_response['scopes'].find do |s| + s['environment_scope'] == 'production' + end + expect(scope_json['strategies']).to eq([{ + "name" => "gradualRolloutUserId", + "parameters" => { + "groupId" => "default", + "percentage" => "50" + } + }]) + end + + it 'clears an existing strategy' do + scope = create_scope(feature_flag, 'production', true, [{ name: 'default', parameters: {} }]) + + put_request(feature_flag, scopes_attributes: [{ + id: scope.id, + strategies: [] + }]) + + expect(response).to have_gitlab_http_status(:ok) + scope_json = json_response['scopes'].find do |s| + s['environment_scope'] == 'production' + end + expect(scope_json['strategies']).to eq([]) + end + + it 'accepts multiple strategies' do + scope = create_scope(feature_flag, 'production', true, [{ name: 'default', parameters: {} }]) + + put_request(feature_flag, scopes_attributes: [{ + id: scope.id, + strategies: [ + { name: 'gradualRolloutUserId', parameters: { groupId: 'mygroup', percentage: '55' } }, + { name: 'userWithId', parameters: { userIds: 'joe' } } + ] + }]) + + expect(response).to have_gitlab_http_status(:ok) + scope_json = json_response['scopes'].find do |s| + s['environment_scope'] == 'production' + end + expect(scope_json['strategies'].length).to eq(2) + expect(scope_json['strategies']).to include({ + "name" => "gradualRolloutUserId", + "parameters" => { "groupId" => "mygroup", "percentage" => "55" } + }) + expect(scope_json['strategies']).to include({ + "name" => "userWithId", + "parameters" => { "userIds" => "joe" } + }) + end + + it 'does not modify strategies when there is no strategies key in the params' do + scope = create_scope(feature_flag, 'production', true, [{ name: 'default', parameters: {} }]) + + put_request(feature_flag, scopes_attributes: [{ id: scope.id }]) + + expect(response).to have_gitlab_http_status(:ok) + scope_json = json_response['scopes'].find do |s| + s['environment_scope'] == 'production' + end + expect(scope_json['strategies']).to eq([{ + "name" => "default", + "parameters" => {} + }]) + end + + it 'leaves an existing strategy when there are no strategies in the params' do + scope = create_scope(feature_flag, 'production', true, [{ name: 'gradualRolloutUserId', + parameters: { groupId: 'default', percentage: '10' } }]) + + put_request(feature_flag, scopes_attributes: [{ id: scope.id }]) + + expect(response).to have_gitlab_http_status(:ok) + scope_json = json_response['scopes'].find do |s| + s['environment_scope'] == 'production' + end + expect(scope_json['strategies']).to eq([{ + "name" => "gradualRolloutUserId", + "parameters" => { "groupId" => "default", "percentage" => "10" } + }]) + end + + it 'does not accept extra parameters in the strategy params' do + scope = create_scope(feature_flag, 'production', true, [{ name: 'default', parameters: {} }]) + + put_request(feature_flag, scopes_attributes: [{ + id: scope.id, + strategies: [{ name: 'userWithId', parameters: { userIds: 'joe', groupId: 'default' } }] + }]) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq(["Scopes strategies parameters are invalid"]) + end + end + + context 'when legacy feature flags are set to be read only' do + it 'does not update the flag' do + stub_feature_flags(feature_flags_legacy_read_only: true) + + put_request(feature_flag, name: 'ci_new_live_trace') + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq(["Legacy feature flags are read-only"]) + end + + it 'updates the flag if the legacy read-only override is enabled for a particular project' do + stub_feature_flags( + feature_flags_legacy_read_only: true, + feature_flags_legacy_read_only_override: project + ) + + put_request(feature_flag, name: 'ci_new_live_trace') + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['name']).to eq('ci_new_live_trace') + end + end + + context 'with a version 2 feature flag' do + let!(:new_version_flag) do + create(:operations_feature_flag, + :new_version_flag, + name: 'new-feature', + active: true, + project: project) + end + + it 'creates a new strategy and scope' do + put_request(new_version_flag, strategies_attributes: [{ + name: 'userWithId', + parameters: { userIds: 'user1' }, + scopes_attributes: [{ + environment_scope: 'production' + }] + }]) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['strategies'].count).to eq(1) + strategy_json = json_response['strategies'].first + expect(strategy_json['name']).to eq('userWithId') + expect(strategy_json['parameters']).to eq({ + 'userIds' => 'user1' + }) + expect(strategy_json['scopes'].count).to eq(1) + scope_json = strategy_json['scopes'].first + expect(scope_json['environment_scope']).to eq('production') + end + + it 'creates a gradualRolloutUserId strategy' do + put_request(new_version_flag, strategies_attributes: [{ + name: 'gradualRolloutUserId', + parameters: { groupId: 'default', percentage: '30' } + }]) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['strategies'].count).to eq(1) + strategy_json = json_response['strategies'].first + expect(strategy_json['name']).to eq('gradualRolloutUserId') + expect(strategy_json['parameters']).to eq({ + 'groupId' => 'default', + 'percentage' => '30' + }) + expect(strategy_json['scopes']).to eq([]) + end + + it 'creates a flexibleRollout strategy' do + put_request(new_version_flag, strategies_attributes: [{ + name: 'flexibleRollout', + parameters: { groupId: 'default', rollout: '30', stickiness: 'DEFAULT' } + }]) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['strategies'].count).to eq(1) + strategy_json = json_response['strategies'].first + expect(strategy_json['name']).to eq('flexibleRollout') + expect(strategy_json['parameters']).to eq({ + 'groupId' => 'default', + 'rollout' => '30', + 'stickiness' => 'DEFAULT' + }) + expect(strategy_json['scopes']).to eq([]) + end + + it 'creates a gitlabUserList strategy' do + user_list = create(:operations_feature_flag_user_list, project: project, name: 'My List', user_xids: 'user1,user2') + + put_request(new_version_flag, strategies_attributes: [{ + name: 'gitlabUserList', + parameters: {}, + user_list_id: user_list.id + }]) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['strategies']).to match([a_hash_including({ + 'id' => an_instance_of(Integer), + 'name' => 'gitlabUserList', + 'parameters' => {}, + 'user_list' => { + 'id' => user_list.id, + 'iid' => user_list.iid, + 'name' => 'My List', + 'user_xids' => 'user1,user2' + }, + 'scopes' => [] + })]) + end + + it 'supports switching the associated user list for an existing gitlabUserList strategy' do + user_list = create(:operations_feature_flag_user_list, project: project, name: 'My List', user_xids: 'user1,user2') + strategy = create(:operations_strategy, feature_flag: new_version_flag, name: 'gitlabUserList', parameters: {}, user_list: user_list) + other_user_list = create(:operations_feature_flag_user_list, project: project, name: 'Other List', user_xids: 'user3') + + put_request(new_version_flag, strategies_attributes: [{ + id: strategy.id, + user_list_id: other_user_list.id + }]) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['strategies']).to eq([{ + 'id' => strategy.id, + 'name' => 'gitlabUserList', + 'parameters' => {}, + 'user_list' => { + 'id' => other_user_list.id, + 'iid' => other_user_list.iid, + 'name' => 'Other List', + 'user_xids' => 'user3' + }, + 'scopes' => [] + }]) + end + + it 'automatically dissociates the user list when switching the type of an existing gitlabUserList strategy' do + user_list = create(:operations_feature_flag_user_list, project: project, name: 'My List', user_xids: 'user1,user2') + strategy = create(:operations_strategy, feature_flag: new_version_flag, name: 'gitlabUserList', parameters: {}, user_list: user_list) + + put_request(new_version_flag, strategies_attributes: [{ + id: strategy.id, + name: 'gradualRolloutUserId', + parameters: { + groupId: 'default', + percentage: '25' + } + }]) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['strategies']).to eq([{ + 'id' => strategy.id, + 'name' => 'gradualRolloutUserId', + 'parameters' => { + 'groupId' => 'default', + 'percentage' => '25' + }, + 'scopes' => [] + }]) + end + + it 'does not delete a user list when deleting a gitlabUserList strategy' do + user_list = create(:operations_feature_flag_user_list, project: project, name: 'My List', user_xids: 'user1,user2') + strategy = create(:operations_strategy, feature_flag: new_version_flag, name: 'gitlabUserList', parameters: {}, user_list: user_list) + + put_request(new_version_flag, strategies_attributes: [{ + id: strategy.id, + _destroy: true + }]) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['strategies']).to eq([]) + expect(::Operations::FeatureFlags::Strategy.count).to eq(0) + expect(::Operations::FeatureFlags::StrategyUserList.count).to eq(0) + expect(::Operations::FeatureFlags::UserList.first).to eq(user_list) + end + + it 'returns not found when trying to create a gitlabUserList strategy with an invalid user list id' do + put_request(new_version_flag, strategies_attributes: [{ + name: 'gitlabUserList', + parameters: {}, + user_list_id: 1 + }]) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'updates an existing strategy' do + strategy = create(:operations_strategy, feature_flag: new_version_flag, name: 'default', parameters: {}) + + put_request(new_version_flag, strategies_attributes: [{ + id: strategy.id, + name: 'userWithId', + parameters: { userIds: 'user2,user3' } + }]) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['strategies']).to eq([{ + 'id' => strategy.id, + 'name' => 'userWithId', + 'parameters' => { 'userIds' => 'user2,user3' }, + 'scopes' => [] + }]) + end + + it 'updates an existing scope' do + strategy = create(:operations_strategy, feature_flag: new_version_flag, name: 'default', parameters: {}) + scope = create(:operations_scope, strategy: strategy, environment_scope: 'staging') + + put_request(new_version_flag, strategies_attributes: [{ + id: strategy.id, + scopes_attributes: [{ + id: scope.id, + environment_scope: 'sandbox' + }] + }]) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['strategies'].first['scopes']).to eq([{ + 'id' => scope.id, + 'environment_scope' => 'sandbox' + }]) + end + + it 'deletes an existing strategy' do + strategy = create(:operations_strategy, feature_flag: new_version_flag, name: 'default', parameters: {}) + + put_request(new_version_flag, strategies_attributes: [{ + id: strategy.id, + _destroy: true + }]) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['strategies']).to eq([]) + end + + it 'deletes an existing scope' do + strategy = create(:operations_strategy, feature_flag: new_version_flag, name: 'default', parameters: {}) + scope = create(:operations_scope, strategy: strategy, environment_scope: 'staging') + + put_request(new_version_flag, strategies_attributes: [{ + id: strategy.id, + scopes_attributes: [{ + id: scope.id, + _destroy: true + }] + }]) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['strategies'].first['scopes']).to eq([]) + end + + it 'does not update the flag if version 2 flags are disabled' do + stub_feature_flags(feature_flags_new_version: false) + + put_request(new_version_flag, { name: 'some-other-name' }) + + expect(response).to have_gitlab_http_status(:not_found) + expect(new_version_flag.reload.name).to eq('new-feature') + end + + it 'updates the flag when legacy feature flags are set to be read only' do + stub_feature_flags(feature_flags_legacy_read_only: true) + + put_request(new_version_flag, name: 'some-other-name') + + expect(response).to have_gitlab_http_status(:ok) + expect(new_version_flag.reload.name).to eq('some-other-name') + end + end + end + + private + + def view_params + { namespace_id: project.namespace, project_id: project } + end +end diff --git a/spec/controllers/projects/feature_flags_user_lists_controller_spec.rb b/spec/controllers/projects/feature_flags_user_lists_controller_spec.rb new file mode 100644 index 00000000000..e0d1d3765b2 --- /dev/null +++ b/spec/controllers/projects/feature_flags_user_lists_controller_spec.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::FeatureFlagsUserListsController do + let_it_be(:project) { create(:project) } + let_it_be(:reporter) { create(:user) } + let_it_be(:developer) { create(:user) } + + before_all do + project.add_reporter(reporter) + project.add_developer(developer) + end + + def request_params(extra_params = {}) + { namespace_id: project.namespace, project_id: project }.merge(extra_params) + end + + describe 'GET #new' do + it 'redirects when the user is unauthenticated' do + get(:new, params: request_params) + + expect(response).to redirect_to(new_user_session_path) + end + + it 'returns not found if the user does not belong to the project' do + user = create(:user) + sign_in(user) + + get(:new, params: request_params) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns not found for a reporter' do + sign_in(reporter) + + get(:new, params: request_params) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'renders the new page for a developer' do + sign_in(developer) + + get(:new, params: request_params) + + expect(response).to have_gitlab_http_status(:ok) + end + end + + describe 'GET #edit' do + before do + sign_in(developer) + end + + it 'renders the edit page for a developer' do + list = create(:operations_feature_flag_user_list, project: project) + + get(:edit, params: request_params(iid: list.iid)) + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'returns not found with an iid that does not exist' do + list = create(:operations_feature_flag_user_list, project: project) + + get(:edit, params: request_params(iid: list.iid + 1)) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns not found for a list belonging to a another project' do + other_project = create(:project) + list = create(:operations_feature_flag_user_list, project: other_project) + + get(:edit, params: request_params(iid: list.iid)) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + describe 'GET #show' do + before do + sign_in(developer) + end + + it 'renders the page for a developer' do + list = create(:operations_feature_flag_user_list, project: project) + + get(:show, params: request_params(iid: list.iid)) + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'returns not found with an iid that does not exist' do + list = create(:operations_feature_flag_user_list, project: project) + + get(:show, params: request_params(iid: list.iid + 1)) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns not found for a list belonging to a another project' do + other_project = create(:project) + list = create(:operations_feature_flag_user_list, project: other_project) + + get(:show, params: request_params(iid: list.iid)) + + expect(response).to have_gitlab_http_status(:not_found) + end + end +end diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb index 762ef795f6e..3baadde46dc 100644 --- a/spec/controllers/projects/group_links_controller_spec.rb +++ b/spec/controllers/projects/group_links_controller_spec.rb @@ -3,10 +3,10 @@ require 'spec_helper' RSpec.describe Projects::GroupLinksController do - let(:group) { create(:group, :private) } - let(:group2) { create(:group, :private) } - let(:project) { create(:project, :private, group: group2) } - let(:user) { create(:user) } + let_it_be(:group) { create(:group, :private) } + let_it_be(:group2) { create(:group, :private) } + let_it_be(:project) { create(:project, :private, group: group2) } + let_it_be(:user) { create(:user) } before do project.add_maintainer(user) @@ -142,4 +142,47 @@ RSpec.describe Projects::GroupLinksController do end end end + + describe '#update' do + let_it_be(:link) do + create( + :project_group_link, + { + project: project, + group: group + } + ) + end + + let(:expiry_date) { 1.month.from_now.to_date } + + before do + travel_to Time.now.utc.beginning_of_day + + put( + :update, + params: { + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: link.id, + group_link: { group_access: Gitlab::Access::GUEST, expires_at: expiry_date } + }, + format: :json + ) + end + + context 'when `expires_at` is set' do + it 'returns correct json response' do + expect(json_response).to eq({ "expires_in" => "about 1 month", "expires_soon" => false }) + end + end + + context 'when `expires_at` is not set' do + let(:expiry_date) { nil } + + it 'returns empty json response' do + expect(json_response).to be_empty + end + end + end end diff --git a/spec/controllers/projects/hooks_controller_spec.rb b/spec/controllers/projects/hooks_controller_spec.rb index bd543cebeec..b9c008d2950 100644 --- a/spec/controllers/projects/hooks_controller_spec.rb +++ b/spec/controllers/projects/hooks_controller_spec.rb @@ -48,6 +48,14 @@ RSpec.describe Projects::HooksController do end end + describe 'DELETE #destroy' do + let!(:hook) { create(:project_hook, project: project) } + let!(:log) { create(:web_hook_log, web_hook: hook) } + let(:params) { { namespace_id: project.namespace, project_id: project, id: hook } } + + it_behaves_like 'Web hook destroyer' + end + describe '#test' do let(:hook) { create(:project_hook, project: project) } diff --git a/spec/controllers/projects/import/jira_controller_spec.rb b/spec/controllers/projects/import/jira_controller_spec.rb index b82735a56b3..37a7fce0c23 100644 --- a/spec/controllers/projects/import/jira_controller_spec.rb +++ b/spec/controllers/projects/import/jira_controller_spec.rb @@ -12,7 +12,6 @@ RSpec.describe Projects::Import::JiraController do def ensure_correct_config sign_in(user) project.add_maintainer(user) - stub_feature_flags(jira_issue_import: true) stub_jira_service_test end @@ -77,7 +76,6 @@ RSpec.describe Projects::Import::JiraController do before do sign_in(user) project.add_maintainer(user) - stub_feature_flags(jira_issue_import: true) end context 'when Jira service is not enabled for the project' do diff --git a/spec/controllers/projects/incidents_controller_spec.rb b/spec/controllers/projects/incidents_controller_spec.rb index 2baae0661cb..ddd15b9b1dd 100644 --- a/spec/controllers/projects/incidents_controller_spec.rb +++ b/spec/controllers/projects/incidents_controller_spec.rb @@ -3,44 +3,119 @@ require 'spec_helper' RSpec.describe Projects::IncidentsController do - let_it_be(:project) { create(:project) } + let_it_be_with_refind(:project) { create(:project) } let_it_be(:developer) { create(:user) } let_it_be(:guest) { create(:user) } + let_it_be(:anonymous) { nil } before_all do - project.add_developer(developer) project.add_guest(guest) + project.add_developer(developer) + end + + before do + sign_in(user) if user + end + + subject { make_request } + + shared_examples 'not found' do + include_examples 'returning response status', :not_found + end + + shared_examples 'login required' do + it 'redirects to the login page' do + subject + + expect(response).to redirect_to(new_user_session_path) + end end describe 'GET #index' do def make_request - get :index, params: { namespace_id: project.namespace, project_id: project } + get :index, params: project_params end - it 'shows the page for user with developer role' do - sign_in(developer) - make_request + let(:user) { developer } + + it 'shows the page' do + subject expect(response).to have_gitlab_http_status(:ok) expect(response).to render_template(:index) end context 'when user is unauthorized' do - it 'redirects to the login page' do - sign_out(developer) - make_request + let(:user) { anonymous } + + it_behaves_like 'login required' + end + + context 'when user is a guest' do + let(:user) { guest } + + it 'shows the page' do + subject - expect(response).to redirect_to(new_user_session_path) + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:index) end end + end + + describe 'GET #show' do + def make_request + get :show, params: project_params(id: resource) + end + + let_it_be(:resource) { create(:incident, project: project) } + let(:user) { developer } + + it 'renders incident page' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:show) + + expect(assigns(:incident)).to be_present + expect(assigns(:incident).author.association(:status)).to be_loaded + expect(assigns(:issue)).to be_present + expect(assigns(:noteable)).to eq(assigns(:incident)) + end + + context 'with non existing id' do + let(:resource) { non_existing_record_id } + + it_behaves_like 'not found' + end + + context 'for issue' do + let_it_be(:resource) { create(:issue, project: project) } + + it_behaves_like 'not found' + end context 'when user is a guest' do - it 'shows 404' do - sign_in(guest) - make_request + let(:user) { guest } - expect(response).to have_gitlab_http_status(:not_found) + it 'shows the page' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:show) end end + + context 'when unauthorized' do + let(:user) { anonymous } + + it_behaves_like 'login required' + end + end + + private + + def project_params(opts = {}) + opts.reverse_merge(namespace_id: project.namespace, project_id: project) end end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index ed5198bf015..f956baa0e22 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -388,15 +388,23 @@ RSpec.describe Projects::IssuesController do # Rails router. A controller-style spec matches the wrong route, and # session['user_return_to'] becomes incorrect. describe 'Redirect after sign in', type: :request do - context 'with an AJAX request' do + before_all do + project.add_developer(user) + end + + before do + login_as(user) + end + + context 'with a JSON request' do it 'does not store the visited URL' do - get project_issue_path(project, issue), xhr: true + get project_issue_path(project, issue, format: :json) expect(session['user_return_to']).to be_blank end end - context 'without an AJAX request' do + context 'with an HTML request' do it 'stores the visited URL' do get project_issue_path(project, issue) @@ -1642,7 +1650,7 @@ RSpec.describe Projects::IssuesController do end it 'allows CSV export' do - expect(ExportCsvWorker).to receive(:perform_async).with(viewer.id, project.id, anything) + expect(IssuableExportCsvWorker).to receive(:perform_async).with(:issue, viewer.id, project.id, anything) request_csv @@ -1657,7 +1665,7 @@ RSpec.describe Projects::IssuesController do it 'redirects to the sign in page' do request_csv - expect(ExportCsvWorker).not_to receive(:perform_async) + expect(IssuableExportCsvWorker).not_to receive(:perform_async) expect(response).to redirect_to(new_user_session_path) end end diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index 94cce1964ca..80cb16966e5 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -121,13 +121,6 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do expect(response).to have_gitlab_http_status(:ok) expect(assigns(:build).id).to eq(job.id) end - - it 'has the correct build collection' do - builds = assigns(:builds).map(&:id) - - expect(builds).to include(job.id, second_job.id) - expect(builds).not_to include(third_job.id) - end end context 'when job does not exist' do @@ -204,16 +197,40 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do context 'with not expiry date' do let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } - it 'exposes needed information' do - get_show_json + context 'when artifacts are unlocked' do + before do + job.pipeline.unlocked! + end - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('job/job_details') - expect(json_response['artifact']['download_path']).to match(%r{artifacts/download}) - expect(json_response['artifact']['browse_path']).to match(%r{artifacts/browse}) - expect(json_response['artifact']).not_to have_key('keep_path') - expect(json_response['artifact']).not_to have_key('expired') - expect(json_response['artifact']).not_to have_key('expired_at') + it 'exposes needed information' do + get_show_json + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('job/job_details') + expect(json_response['artifact']['download_path']).to match(%r{artifacts/download}) + expect(json_response['artifact']['browse_path']).to match(%r{artifacts/browse}) + expect(json_response['artifact']).not_to have_key('keep_path') + expect(json_response['artifact']).not_to have_key('expired') + expect(json_response['artifact']).not_to have_key('expired_at') + end + end + + context 'when artifacts are locked' do + before do + job.pipeline.artifacts_locked! + end + + it 'exposes needed information' do + get_show_json + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('job/job_details') + expect(json_response['artifact']['download_path']).to match(%r{artifacts/download}) + expect(json_response['artifact']['browse_path']).to match(%r{artifacts/browse}) + expect(json_response['artifact']).not_to have_key('keep_path') + expect(json_response['artifact']).not_to have_key('expired') + expect(json_response['artifact']).not_to have_key('expired_at') + end end end @@ -740,19 +757,21 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do name: 'master', project: project) sign_in(user) - - post_play end context 'when job is playable' do let(:job) { create(:ci_build, :playable, pipeline: pipeline) } it 'redirects to the played job page' do + post_play + expect(response).to have_gitlab_http_status(:found) expect(response).to redirect_to(namespace_project_job_path(id: job.id)) end it 'transits to pending' do + post_play + expect(job.reload).to be_pending end @@ -760,15 +779,54 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do let(:variable_attributes) { [{ key: 'first', secret_value: 'first' }] } it 'assigns the job variables' do + post_play + expect(job.reload.job_variables.map(&:key)).to contain_exactly('first') end end + + context 'when job is bridge' do + let(:downstream_project) { create(:project) } + let(:job) { create(:ci_bridge, :playable, pipeline: pipeline, downstream: downstream_project) } + + before do + downstream_project.add_developer(user) + end + + it 'redirects to the pipeline page' do + post_play + + expect(response).to have_gitlab_http_status(:found) + expect(response).to redirect_to(pipeline_path(pipeline)) + builds_namespace_project_pipeline_path(id: pipeline.id) + end + + it 'transits to pending' do + post_play + + expect(job.reload).to be_pending + end + + context 'when FF ci_manual_bridges is disabled' do + before do + stub_feature_flags(ci_manual_bridges: false) + end + + it 'returns 404' do + post_play + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end end context 'when job is not playable' do let(:job) { create(:ci_build, pipeline: pipeline) } it 'renders unprocessable_entity' do + post_play + expect(response).to have_gitlab_http_status(:unprocessable_entity) end end diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb index f213d104747..8a3c55033cb 100644 --- a/spec/controllers/projects/labels_controller_spec.rb +++ b/spec/controllers/projects/labels_controller_spec.rb @@ -3,9 +3,9 @@ require 'spec_helper' RSpec.describe Projects::LabelsController do - let(:group) { create(:group) } - let(:project) { create(:project, namespace: group) } - let(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project, reload: true) { create(:project, namespace: group) } + let_it_be(:user) { create(:user) } before do project.add_maintainer(user) @@ -14,16 +14,21 @@ RSpec.describe Projects::LabelsController do end describe 'GET #index' do - let!(:label_1) { create(:label, project: project, priority: 1, title: 'Label 1') } - let!(:label_2) { create(:label, project: project, priority: 3, title: 'Label 2') } - let!(:label_3) { create(:label, project: project, priority: 1, title: 'Label 3') } - let!(:label_4) { create(:label, project: project, title: 'Label 4') } - let!(:label_5) { create(:label, project: project, title: 'Label 5') } - - let!(:group_label_1) { create(:group_label, group: group, title: 'Group Label 1') } - let!(:group_label_2) { create(:group_label, group: group, title: 'Group Label 2') } - let!(:group_label_3) { create(:group_label, group: group, title: 'Group Label 3') } - let!(:group_label_4) { create(:group_label, group: group, title: 'Group Label 4') } + let_it_be(:label_1) { create(:label, project: project, priority: 1, title: 'Label 1') } + let_it_be(:label_2) { create(:label, project: project, priority: 3, title: 'Label 2') } + let_it_be(:label_3) { create(:label, project: project, priority: 1, title: 'Label 3') } + let_it_be(:label_4) { create(:label, project: project, title: 'Label 4') } + let_it_be(:label_5) { create(:label, project: project, title: 'Label 5') } + + let_it_be(:group_label_1) { create(:group_label, group: group, title: 'Group Label 1') } + let_it_be(:group_label_2) { create(:group_label, group: group, title: 'Group Label 2') } + let_it_be(:group_label_3) { create(:group_label, group: group, title: 'Group Label 3') } + let_it_be(:group_label_4) { create(:group_label, group: group, title: 'Group Label 4') } + + let_it_be(:group_labels) { [group_label_3, group_label_4]} + let_it_be(:project_labels) { [label_4, label_5]} + let_it_be(:group_priority_labels) { [group_label_1, group_label_2]} + let_it_be(:project_priority_labels) { [label_1, label_2, label_3]} before do create(:label_priority, project: project, label: group_label_1, priority: 3) @@ -68,6 +73,60 @@ RSpec.describe Projects::LabelsController do end end + context 'with subgroups' do + let_it_be(:subgroup) { create(:group, parent: group) } + let_it_be(:subgroup_label_1) { create(:group_label, group: subgroup, title: 'subgroup_label_1') } + let_it_be(:subgroup_label_2) { create(:group_label, group: subgroup, title: 'subgroup_label_2') } + + before do + project.update!(namespace: subgroup) + subgroup.add_owner(user) + create(:label_priority, project: project, label: subgroup_label_2, priority: 1) + end + + RSpec.shared_examples 'returns ancestor group labels' do + it 'returns ancestor group labels', :aggregate_failures do + get :index, params: params + + expect(assigns(:labels)).to match_array([subgroup_label_1] + group_labels + project_labels) + expect(assigns(:prioritized_labels)).to match_array([subgroup_label_2] + group_priority_labels + project_priority_labels) + end + end + + context 'when show_inherited_labels disabled' do + before do + stub_feature_flags(show_inherited_labels: false) + end + + context 'when include_ancestor_groups false' do + let(:params) { { namespace_id: project.namespace.to_param, project_id: project } } + + it 'does not return ancestor group labels', :aggregate_failures do + get :index, params: params + + expect(assigns(:labels)).to match_array([subgroup_label_1] + project_labels) + expect(assigns(:prioritized_labels)).to match_array([subgroup_label_2] + project_priority_labels) + end + end + + context 'when include_ancestor_groups true' do + let(:params) { { namespace_id: project.namespace.to_param, project_id: project, include_ancestor_groups: true } } + + it_behaves_like 'returns ancestor group labels' + end + end + + context 'when show_inherited_labels enabled' do + let(:params) { { namespace_id: project.namespace.to_param, project_id: project } } + + before do + stub_feature_flags(show_inherited_labels: true) + end + + it_behaves_like 'returns ancestor group labels' + end + end + def list_labels get :index, params: { namespace_id: project.namespace.to_param, project_id: project } end @@ -75,7 +134,7 @@ RSpec.describe Projects::LabelsController do describe 'POST #generate' do context 'personal project' do - let(:personal_project) { create(:project, namespace: user.namespace) } + let_it_be(:personal_project) { create(:project, namespace: user.namespace) } it 'creates labels' do post :generate, params: { namespace_id: personal_project.namespace.to_param, project_id: personal_project } @@ -116,8 +175,8 @@ RSpec.describe Projects::LabelsController do end describe 'POST #promote' do - let!(:promoted_label_name) { "Promoted Label" } - let!(:label_1) { create(:label, title: promoted_label_name, project: project) } + let_it_be(:promoted_label_name) { "Promoted Label" } + let_it_be(:label_1) { create(:label, title: promoted_label_name, project: project) } context 'not group reporters' do it 'denies access' do @@ -196,7 +255,7 @@ RSpec.describe Projects::LabelsController do end context 'when requesting a redirected path' do - let!(:redirect_route) { project.redirect_routes.create(path: project.full_path + 'old') } + let_it_be(:redirect_route) { project.redirect_routes.create(path: project.full_path + 'old') } it 'redirects to the canonical path' do get :index, params: { namespace_id: project.namespace, project_id: project.to_param + 'old' } @@ -242,7 +301,7 @@ RSpec.describe Projects::LabelsController do end context 'when requesting a redirected path' do - let!(:redirect_route) { project.redirect_routes.create(path: project.full_path + 'old') } + let_it_be(:redirect_route) { project.redirect_routes.create(path: project.full_path + 'old') } it 'returns not found' do post :generate, params: { namespace_id: project.namespace, project_id: project.to_param + 'old' } diff --git a/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb index 5f636bd4340..c2cc3d10ea0 100644 --- a/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb +++ b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb @@ -156,7 +156,7 @@ RSpec.describe Projects::MergeRequests::ConflictsController do expect(json_response).to include('old_path' => path, 'new_path' => path, - 'blob_icon' => 'file-text-o', + 'blob_icon' => 'doc-text', 'blob_path' => a_string_ending_with(path), 'content' => content) end diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb index fa32d32f552..9e5d41b1075 100644 --- a/spec/controllers/projects/milestones_controller_spec.rb +++ b/spec/controllers/projects/milestones_controller_spec.rb @@ -17,7 +17,9 @@ RSpec.describe Projects::MilestonesController do controller.instance_variable_set(:@project, project) end - it_behaves_like 'milestone tabs' + it_behaves_like 'milestone tabs' do + let(:request_params) { { namespace_id: project.namespace, project_id: project, id: milestone.iid } } + end describe "#show" do render_views diff --git a/spec/controllers/projects/pipelines/stages_controller_spec.rb b/spec/controllers/projects/pipelines/stages_controller_spec.rb index 6e8c08d95a1..a8b328c7563 100644 --- a/spec/controllers/projects/pipelines/stages_controller_spec.rb +++ b/spec/controllers/projects/pipelines/stages_controller_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Projects::Pipelines::StagesController do let(:user) { create(:user) } let(:project) { create(:project, :repository) } + let(:downstream_project) { create(:project, :repository) } before do sign_in(user) @@ -17,6 +18,7 @@ RSpec.describe Projects::Pipelines::StagesController do before do create_manual_build(pipeline, 'test', 'rspec 1/2') create_manual_build(pipeline, 'test', 'rspec 2/2') + create_manual_bridge(pipeline, 'test', 'trigger') pipeline.reload end @@ -32,6 +34,7 @@ RSpec.describe Projects::Pipelines::StagesController do context 'when user has access' do before do project.add_maintainer(user) + downstream_project.add_maintainer(user) end context 'when the stage does not exists' do @@ -46,12 +49,12 @@ RSpec.describe Projects::Pipelines::StagesController do context 'when the stage exists' do it 'starts all manual jobs' do - expect(pipeline.builds.manual.count).to eq(2) + expect(pipeline.processables.manual.count).to eq(3) play_manual_stage! expect(response).to have_gitlab_http_status(:ok) - expect(pipeline.builds.manual.count).to eq(0) + expect(pipeline.processables.manual.count).to eq(0) end end end @@ -68,5 +71,9 @@ RSpec.describe Projects::Pipelines::StagesController do def create_manual_build(pipeline, stage, name) create(:ci_build, :manual, pipeline: pipeline, stage: stage, name: name) end + + def create_manual_bridge(pipeline, stage, name) + create(:ci_bridge, :manual, pipeline: pipeline, stage: stage, name: name, downstream: downstream_project) + end end end diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index c3be7de25a8..0720124ea57 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -1148,4 +1148,84 @@ RSpec.describe Projects::PipelinesController do } end end + + describe 'GET config_variables.json' do + let(:result) { YAML.dump(ci_config) } + + before do + stub_gitlab_ci_yml_for_sha(sha, result) + end + + context 'when sending a valid sha' do + let(:sha) { 'master' } + let(:ci_config) do + { + variables: { + KEY1: { value: 'val 1', description: 'description 1' } + }, + test: { + stage: 'test', + script: 'echo' + } + } + end + + it 'returns variable list' do + get_config_variables + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['KEY1']).to eq({ 'value' => 'val 1', 'description' => 'description 1' }) + end + end + + context 'when sending an invalid sha' do + let(:sha) { 'invalid-sha' } + let(:ci_config) { nil } + + it 'returns empty json' do + get_config_variables + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({}) + end + end + + context 'when sending an invalid config' do + let(:sha) { 'master' } + let(:ci_config) do + { + variables: { + KEY1: { value: 'val 1', description: 'description 1' } + }, + test: { + stage: 'invalid', + script: 'echo' + } + } + end + + it 'returns empty result' do + get_config_variables + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({}) + end + end + + private + + def stub_gitlab_ci_yml_for_sha(sha, result) + allow_any_instance_of(Repository) + .to receive(:gitlab_ci_yml_for) + .with(sha, '.gitlab-ci.yml') + .and_return(result) + end + + def get_config_variables + get :config_variables, params: { namespace_id: project.namespace, + project_id: project, + sha: sha }, + format: :json + end + end end diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index ae05e2d2631..74311fa89f3 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -228,6 +228,43 @@ RSpec.describe Projects::ProjectMembersController do end end end + + context 'expiration date' do + let(:expiry_date) { 1.month.from_now.to_date } + + before do + travel_to Time.now.utc.beginning_of_day + + put( + :update, + params: { + project_member: { expires_at: expiry_date }, + namespace_id: project.namespace, + project_id: project, + id: requester + }, + format: :json + ) + end + + context 'when `expires_at` is set' do + it 'returns correct json response' do + expect(json_response).to eq({ + "expires_in" => "about 1 month", + "expires_soon" => false, + "expires_at_formatted" => expiry_date.to_time.in_time_zone.to_s(:medium) + }) + end + end + + context 'when `expires_at` is not set' do + let(:expiry_date) { nil } + + it 'returns empty json response' do + expect(json_response).to be_empty + end + end + end end describe 'DELETE destroy' do @@ -536,4 +573,19 @@ RSpec.describe Projects::ProjectMembersController do end end end + + describe 'POST resend_invite' do + let(:member) { create(:project_member, project: project) } + + before do + project.add_maintainer(user) + sign_in(user) + end + + it 'is successful' do + post :resend_invite, params: { namespace_id: project.namespace, project_id: project, id: member } + + expect(response).to have_gitlab_http_status(:found) + end + end end diff --git a/spec/controllers/projects/registry/tags_controller_spec.rb b/spec/controllers/projects/registry/tags_controller_spec.rb index 6adee35b60a..59df9e78a3c 100644 --- a/spec/controllers/projects/registry/tags_controller_spec.rb +++ b/spec/controllers/projects/registry/tags_controller_spec.rb @@ -109,7 +109,7 @@ RSpec.describe Projects::Registry::TagsController do it 'tracks the event' do expect_delete_tags(%w[test.]) - expect(controller).to receive(:track_event).with(:delete_tag) + expect(controller).to receive(:track_event).with(:delete_tag, {}) destroy_tag('test.') end diff --git a/spec/controllers/projects/releases/evidences_controller_spec.rb b/spec/controllers/projects/releases/evidences_controller_spec.rb index d5a9665d6a5..0ec4cdf2a31 100644 --- a/spec/controllers/projects/releases/evidences_controller_spec.rb +++ b/spec/controllers/projects/releases/evidences_controller_spec.rb @@ -113,18 +113,6 @@ RSpec.describe Projects::Releases::EvidencesController do it_behaves_like 'does not show the issue in evidence' - context 'when the issue is confidential' do - let(:issue) { create(:issue, :confidential, project: project) } - - it_behaves_like 'does not show the issue in evidence' - end - - context 'when the user is the author of the confidential issue' do - let(:issue) { create(:issue, :confidential, project: project, author: user) } - - it_behaves_like 'does not show the issue in evidence' - end - context 'when project is private' do let(:project) { create(:project, :repository, :private) } @@ -143,32 +131,16 @@ RSpec.describe Projects::Releases::EvidencesController do it_behaves_like 'does not show the issue in evidence' - context 'when the issue is confidential' do - let(:issue) { create(:issue, :confidential, project: project) } - - it_behaves_like 'does not show the issue in evidence' - end - - context 'when the user is the author of the confidential issue' do - let(:issue) { create(:issue, :confidential, project: project, author: user) } - - it_behaves_like 'does not show the issue in evidence' - end - context 'when project is private' do let(:project) { create(:project, :repository, :private) } - it 'returns evidence ' do - subject - - expect(json_response).to eq(evidence.summary) - end + it_behaves_like 'does not show the issue in evidence' end context 'when project restricts the visibility of issues to project members only' do let(:project) { create(:project, :repository, :issues_private) } - it_behaves_like 'evidence not found' + it_behaves_like 'does not show the issue in evidence' end end diff --git a/spec/controllers/projects/releases_controller_spec.rb b/spec/controllers/projects/releases_controller_spec.rb index 45beccfeef5..420d818daeb 100644 --- a/spec/controllers/projects/releases_controller_spec.rb +++ b/spec/controllers/projects/releases_controller_spec.rb @@ -194,14 +194,6 @@ RSpec.describe Projects::ReleasesController do end end - context 'when feature flag `release_show_page` is disabled' do - before do - stub_feature_flags(release_show_page: false) - end - - it_behaves_like 'not found' - end - context 'when release does not exist' do let(:tag) { 'non-existent-tag' } diff --git a/spec/controllers/projects/runners_controller_spec.rb b/spec/controllers/projects/runners_controller_spec.rb index 66f20bd50c4..2443a823070 100644 --- a/spec/controllers/projects/runners_controller_spec.rb +++ b/spec/controllers/projects/runners_controller_spec.rb @@ -73,4 +73,45 @@ RSpec.describe Projects::RunnersController do expect(runner.active).to eq(false) end end + + describe '#toggle_shared_runners' do + let(:group) { create(:group) } + let(:project) { create(:project, group: group) } + + it 'toggles shared_runners_enabled when the group allows shared runners' do + project.update!(shared_runners_enabled: true) + + post :toggle_shared_runners, params: params + + project.reload + + expect(response).to have_gitlab_http_status(:found) + expect(project.shared_runners_enabled).to eq(false) + end + + it 'toggles shared_runners_enabled when the group disallows shared runners but allows overrides' do + group.update!(shared_runners_enabled: false, allow_descendants_override_disabled_shared_runners: true) + project.update!(shared_runners_enabled: false) + + post :toggle_shared_runners, params: params + + project.reload + + expect(response).to have_gitlab_http_status(:found) + expect(project.shared_runners_enabled).to eq(true) + end + + it 'does not enable if the group disallows shared runners' do + group.update!(shared_runners_enabled: false, allow_descendants_override_disabled_shared_runners: false) + project.update!(shared_runners_enabled: false) + + post :toggle_shared_runners, params: params + + project.reload + + expect(response).to have_gitlab_http_status(:found) + expect(project.shared_runners_enabled).to eq(false) + expect(flash[:alert]).to eq("Cannot enable shared runners because parent group does not allow it") + end + end end diff --git a/spec/controllers/projects/serverless/functions_controller_spec.rb b/spec/controllers/projects/serverless/functions_controller_spec.rb index 7f558ad9231..75135839a06 100644 --- a/spec/controllers/projects/serverless/functions_controller_spec.rb +++ b/spec/controllers/projects/serverless/functions_controller_spec.rb @@ -206,7 +206,7 @@ RSpec.describe Projects::Serverless::FunctionsController do context 'on Knative 0.5.0' do before do - prepare_knative_stubs(knative_05_service(knative_stub_options)) + prepare_knative_stubs(knative_05_service(**knative_stub_options)) end include_examples 'GET #show with valid data' @@ -214,7 +214,7 @@ RSpec.describe Projects::Serverless::FunctionsController do context 'on Knative 0.6.0' do before do - prepare_knative_stubs(knative_06_service(knative_stub_options)) + prepare_knative_stubs(knative_06_service(**knative_stub_options)) end include_examples 'GET #show with valid data' @@ -222,7 +222,7 @@ RSpec.describe Projects::Serverless::FunctionsController do context 'on Knative 0.7.0' do before do - prepare_knative_stubs(knative_07_service(knative_stub_options)) + prepare_knative_stubs(knative_07_service(**knative_stub_options)) end include_examples 'GET #show with valid data' @@ -230,7 +230,7 @@ RSpec.describe Projects::Serverless::FunctionsController do context 'on Knative 0.9.0' do before do - prepare_knative_stubs(knative_09_service(knative_stub_options)) + prepare_knative_stubs(knative_09_service(**knative_stub_options)) end include_examples 'GET #show with valid data' @@ -275,7 +275,7 @@ RSpec.describe Projects::Serverless::FunctionsController do context 'on Knative 0.5.0' do before do - prepare_knative_stubs(knative_05_service(knative_stub_options)) + prepare_knative_stubs(knative_05_service(**knative_stub_options)) end include_examples 'GET #index with data' @@ -283,7 +283,7 @@ RSpec.describe Projects::Serverless::FunctionsController do context 'on Knative 0.6.0' do before do - prepare_knative_stubs(knative_06_service(knative_stub_options)) + prepare_knative_stubs(knative_06_service(**knative_stub_options)) end include_examples 'GET #index with data' @@ -291,7 +291,7 @@ RSpec.describe Projects::Serverless::FunctionsController do context 'on Knative 0.7.0' do before do - prepare_knative_stubs(knative_07_service(knative_stub_options)) + prepare_knative_stubs(knative_07_service(**knative_stub_options)) end include_examples 'GET #index with data' @@ -299,7 +299,7 @@ RSpec.describe Projects::Serverless::FunctionsController do context 'on Knative 0.9.0' do before do - prepare_knative_stubs(knative_09_service(knative_stub_options)) + prepare_knative_stubs(knative_09_service(**knative_stub_options)) end include_examples 'GET #index with data' diff --git a/spec/controllers/projects/settings/access_tokens_controller_spec.rb b/spec/controllers/projects/settings/access_tokens_controller_spec.rb index 4743ab2b7c1..ff52b2a765a 100644 --- a/spec/controllers/projects/settings/access_tokens_controller_spec.rb +++ b/spec/controllers/projects/settings/access_tokens_controller_spec.rb @@ -5,27 +5,21 @@ require('spec_helper') RSpec.describe Projects::Settings::AccessTokensController do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project) } + let_it_be(:bot_user) { create(:user, :project_bot) } before_all do project.add_maintainer(user) + project.add_maintainer(bot_user) end before do sign_in(user) end - shared_examples 'feature unavailability' do - context 'when flag is disabled' do + shared_examples 'feature unavailable' do + context 'user is not a maintainer' do before do - stub_feature_flags(resource_access_token: false) - end - - it { is_expected.to have_gitlab_http_status(:not_found) } - end - - context 'when environment is Gitlab.com' do - before do - allow(Gitlab).to receive(:com?).and_return(true) + project.add_developer(user) end it { is_expected.to have_gitlab_http_status(:not_found) } @@ -35,156 +29,25 @@ RSpec.describe Projects::Settings::AccessTokensController do describe '#index' do subject { get :index, params: { namespace_id: project.namespace, project_id: project } } - it_behaves_like 'feature unavailability' - - context 'when feature is available' do - let_it_be(:bot_user) { create(:user, :project_bot) } - let_it_be(:active_project_access_token) { create(:personal_access_token, user: bot_user) } - let_it_be(:inactive_project_access_token) { create(:personal_access_token, :revoked, user: bot_user) } - - before_all do - project.add_maintainer(bot_user) - end - - before do - enable_feature - end - - it 'retrieves active project access tokens' do - subject - - expect(assigns(:active_project_access_tokens)).to contain_exactly(active_project_access_token) - end - - it 'retrieves inactive project access tokens' do - subject - - expect(assigns(:inactive_project_access_tokens)).to contain_exactly(inactive_project_access_token) - end - - it 'lists all available scopes' do - subject - - expect(assigns(:scopes)).to eq(Gitlab::Auth.resource_bot_scopes) - end - - it 'retrieves newly created personal access token value' do - token_value = 'random-value' - allow(PersonalAccessToken).to receive(:redis_getdel).with("#{user.id}:#{project.id}").and_return(token_value) - - subject - - expect(assigns(:new_project_access_token)).to eq(token_value) - end - end + it_behaves_like 'feature unavailable' + it_behaves_like 'project access tokens available #index' end - describe '#create', :clean_gitlab_redis_shared_state do - subject { post :create, params: { namespace_id: project.namespace, project_id: project }.merge(project_access_token: access_token_params) } - - let_it_be(:access_token_params) { {} } - - it_behaves_like 'feature unavailability' - - context 'when feature is available' do - let_it_be(:access_token_params) { { name: 'Nerd bot', scopes: ["api"], expires_at: 1.month.since.to_date } } - - before do - enable_feature - end - - def created_token - PersonalAccessToken.order(:created_at).last - end - - it 'returns success message' do - subject - - expect(response.flash[:notice]).to match(/\AYour new project access token has been created./i) - end - - it 'creates project access token' do - subject - - expect(created_token.name).to eq(access_token_params[:name]) - expect(created_token.scopes).to eq(access_token_params[:scopes]) - expect(created_token.expires_at).to eq(access_token_params[:expires_at]) - end - - it 'creates project bot user' do - subject - - expect(created_token.user).to be_project_bot - end - - it 'stores newly created token redis store' do - expect(PersonalAccessToken).to receive(:redis_store!) - - subject - end + describe '#create' do + let(:access_token_params) { { name: 'Nerd bot', scopes: ["api"], expires_at: Date.today + 1.month } } - it { expect { subject }.to change { User.count }.by(1) } - it { expect { subject }.to change { PersonalAccessToken.count }.by(1) } - - context 'when unsuccessful' do - before do - allow_next_instance_of(ResourceAccessTokens::CreateService) do |service| - allow(service).to receive(:execute).and_return ServiceResponse.error(message: 'Failed!') - end - end + subject { post :create, params: { namespace_id: project.namespace, project_id: project }.merge(project_access_token: access_token_params) } - it { expect(subject).to render_template(:index) } - end - end + it_behaves_like 'feature unavailable' + it_behaves_like 'project access tokens available #create' end - describe '#revoke' do - subject { put :revoke, params: { namespace_id: project.namespace, project_id: project, id: project_access_token } } - - let_it_be(:bot_user) { create(:user, :project_bot) } - let_it_be(:project_access_token) { create(:personal_access_token, user: bot_user) } - - before_all do - project.add_maintainer(bot_user) - end - - it_behaves_like 'feature unavailability' + describe '#revoke', :sidekiq_inline do + let(:project_access_token) { create(:personal_access_token, user: bot_user) } - context 'when feature is available' do - before do - enable_feature - end - - it 'revokes token access' do - subject - - expect(project_access_token.reload.revoked?).to be true - end - - it 'removed membership of bot user' do - subject - - expect(project.reload.bots).not_to include(bot_user) - end - - it 'blocks project bot user' do - subject - - expect(bot_user.reload.blocked?).to be true - end - - it 'converts issuables of the bot user to ghost user' do - issue = create(:issue, author: bot_user) - - subject - - expect(issue.reload.author.ghost?).to be true - end - end - end + subject { put :revoke, params: { namespace_id: project.namespace, project_id: project, id: project_access_token } } - def enable_feature - allow(Gitlab).to receive(:com?).and_return(false) - stub_feature_flags(resource_access_token: true) + it_behaves_like 'feature unavailable' + it_behaves_like 'project access tokens available #revoke' end end diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb index 8498ff49826..7a6e11d53d4 100644 --- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb +++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb @@ -230,6 +230,21 @@ RSpec.describe Projects::Settings::CiCdController do end end + context 'when forward_deployment_enabled is not specified' do + let(:params) { { ci_cd_settings_attributes: { forward_deployment_enabled: false } } } + + before do + project.ci_cd_settings.update!(forward_deployment_enabled: nil) + end + + it 'sets forward deployment enabled' do + subject + + project.reload + expect(project.ci_forward_deployment_enabled).to eq(false) + end + end + context 'when max_artifacts_size is specified' do let(:params) { { max_artifacts_size: 10 } } @@ -266,4 +281,21 @@ RSpec.describe Projects::Settings::CiCdController do end end end + + describe 'GET #runner_setup_scripts' do + it 'renders the setup scripts' do + get :runner_setup_scripts, params: { os: 'linux', arch: 'amd64', namespace_id: project.namespace, project_id: project } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to have_key("install") + expect(json_response).to have_key("register") + end + + it 'renders errors if they occur' do + get :runner_setup_scripts, params: { os: 'foo', arch: 'bar', namespace_id: project.namespace, project_id: project } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response).to have_key("errors") + end + end end diff --git a/spec/controllers/projects/settings/operations_controller_spec.rb b/spec/controllers/projects/settings/operations_controller_spec.rb index ca1b0d2fe15..9fc9da1265e 100644 --- a/spec/controllers/projects/settings/operations_controller_spec.rb +++ b/spec/controllers/projects/settings/operations_controller_spec.rb @@ -6,9 +6,12 @@ RSpec.describe Projects::Settings::OperationsController do let_it_be(:user) { create(:user) } let_it_be(:project, reload: true) { create(:project) } + before_all do + project.add_maintainer(user) + end + before do sign_in(user) - project.add_maintainer(user) end shared_examples 'PATCHable' do @@ -163,10 +166,6 @@ RSpec.describe Projects::Settings::OperationsController do context 'updating each incident management setting' do let(:new_incident_management_settings) { {} } - before do - project.add_maintainer(user) - end - shared_examples 'a gitlab tracking event' do |params, event_key| it "creates a gitlab tracking event #{event_key}" do new_incident_management_settings = params @@ -194,10 +193,6 @@ RSpec.describe Projects::Settings::OperationsController do end describe 'POST #reset_pagerduty_token' do - before do - project.add_maintainer(user) - end - context 'with existing incident management setting has active PagerDuty webhook' do let!(:incident_management_setting) do create(:project_incident_management_setting, project: project, pagerduty_active: true) @@ -392,10 +387,6 @@ RSpec.describe Projects::Settings::OperationsController do end describe 'POST #reset_alerting_token' do - before do - project.add_maintainer(user) - end - context 'with existing alerting setting' do let!(:alerting_setting) do create(:project_alerting_setting, project: project) @@ -478,6 +469,104 @@ RSpec.describe Projects::Settings::OperationsController do end end + context 'tracing integration' do + describe 'GET #show' do + context 'with existing setting' do + let_it_be(:setting) do + create(:project_tracing_setting, project: project) + end + + it 'loads existing setting' do + get :show, params: project_params(project) + + expect(controller.helpers.tracing_setting).to eq(setting) + end + end + + context 'without an existing setting' do + it 'builds a new setting' do + get :show, params: project_params(project) + + expect(controller.helpers.tracing_setting).to be_new_record + end + end + end + + describe 'PATCH #update' do + let_it_be(:external_url) { 'https://gitlab.com' } + let(:params) do + { + tracing_setting_attributes: { + external_url: external_url + } + } + end + + it_behaves_like 'PATCHable' + + describe 'gitlab tracking', :snowplow do + shared_examples 'event tracking' do + it 'tracks an event' do + expect_snowplow_event( + category: 'project:operations:tracing', + action: 'external_url_populated' + ) + end + end + + shared_examples 'no event tracking' do + it 'does not track an event' do + expect_no_snowplow_event + end + end + + before do + make_request + end + + subject(:make_request) do + patch :update, params: project_params(project, params), format: :json + end + + context 'without existing setting' do + context 'when creating a new setting' do + it_behaves_like 'event tracking' + end + + context 'with invalid external_url' do + let_it_be(:external_url) { nil } + + it_behaves_like 'no event tracking' + end + end + + context 'with existing setting' do + let_it_be(:existing_setting) do + create(:project_tracing_setting, + project: project, + external_url: external_url) + end + + context 'when changing external_url' do + let_it_be(:external_url) { 'https://example.com' } + + it_behaves_like 'no event tracking' + end + + context 'with unchanged external_url' do + it_behaves_like 'no event tracking' + end + + context 'with invalid external_url' do + let_it_be(:external_url) { nil } + + it_behaves_like 'no event tracking' + end + end + end + end + end + private def project_params(project, params = {}) diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb index d0e412dfdb8..6b394fab14c 100644 --- a/spec/controllers/projects/snippets_controller_spec.rb +++ b/spec/controllers/projects/snippets_controller_spec.rb @@ -82,215 +82,6 @@ RSpec.describe Projects::SnippetsController do end end - describe 'POST #create' do - def create_snippet(project, snippet_params = {}, additional_params = {}) - sign_in(user) - - project.add_developer(user) - - post :create, params: { - namespace_id: project.namespace.to_param, - project_id: project, - project_snippet: { title: 'Title', content: 'Content', description: 'Description' }.merge(snippet_params) - }.merge(additional_params) - - Snippet.last - end - - it 'creates the snippet correctly' do - snippet = create_snippet(project, visibility_level: Snippet::PRIVATE) - - expect(snippet.title).to eq('Title') - expect(snippet.content).to eq('Content') - expect(snippet.description).to eq('Description') - end - - context 'when the snippet is spam' do - before do - allow_next_instance_of(Spam::AkismetService) do |instance| - allow(instance).to receive(:spam?).and_return(true) - end - end - - context 'when the snippet is private' do - it 'creates the snippet' do - expect { create_snippet(project, visibility_level: Snippet::PRIVATE) } - .to change { Snippet.count }.by(1) - end - end - - context 'when the snippet is public' do - it 'rejects the snippet' do - expect { create_snippet(project, visibility_level: Snippet::PUBLIC) } - .not_to change { Snippet.count } - expect(response).to render_template(:new) - end - - it 'creates a spam log' do - expect { create_snippet(project, visibility_level: Snippet::PUBLIC) } - .to log_spam(title: 'Title', user_id: user.id, noteable_type: 'ProjectSnippet') - end - - it 'renders :new with reCAPTCHA disabled' do - stub_application_setting(recaptcha_enabled: false) - - create_snippet(project, visibility_level: Snippet::PUBLIC) - - expect(response).to render_template(:new) - end - - context 'reCAPTCHA enabled' do - before do - stub_application_setting(recaptcha_enabled: true) - end - - it 'renders :verify with reCAPTCHA enabled' do - create_snippet(project, visibility_level: Snippet::PUBLIC) - - expect(response).to render_template(:verify) - end - - it 'renders snippet page when reCAPTCHA verified' do - spammy_title = 'Whatever' - - spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title) - create_snippet(project, - { visibility_level: Snippet::PUBLIC }, - { spam_log_id: spam_logs.last.id, - recaptcha_verification: true }) - - expect(response).to redirect_to(project_snippet_path(project, Snippet.last)) - end - end - end - end - end - - describe 'PUT #update' do - let(:visibility_level) { Snippet::PUBLIC } - let(:snippet) { create :project_snippet, author: user, project: project, visibility_level: visibility_level } - - def update_snippet(snippet_params = {}, additional_params = {}) - sign_in(user) - - project.add_developer(user) - - put :update, params: { - namespace_id: project.namespace.to_param, - project_id: project, - id: snippet, - project_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params) - }.merge(additional_params) - - snippet.reload - end - - context 'when the snippet is spam' do - before do - allow_next_instance_of(Spam::AkismetService) do |instance| - allow(instance).to receive(:spam?).and_return(true) - end - end - - context 'when the snippet is private' do - let(:visibility_level) { Snippet::PRIVATE } - - it 'updates the snippet' do - expect { update_snippet(title: 'Foo') } - .to change { snippet.reload.title }.to('Foo') - end - end - - context 'when the snippet is public' do - it 'rejects the snippet' do - expect { update_snippet(title: 'Foo') } - .not_to change { snippet.reload.title } - end - - it 'creates a spam log' do - expect { update_snippet(title: 'Foo') } - .to log_spam(title: 'Foo', user_id: user.id, noteable_type: 'ProjectSnippet') - end - - it 'renders :edit with reCAPTCHA disabled' do - stub_application_setting(recaptcha_enabled: false) - - update_snippet(title: 'Foo') - - expect(response).to render_template(:edit) - end - - context 'reCAPTCHA enabled' do - before do - stub_application_setting(recaptcha_enabled: true) - end - - it 'renders :verify with reCAPTCHA enabled' do - update_snippet(title: 'Foo') - - expect(response).to render_template(:verify) - end - - it 'renders snippet page when reCAPTCHA verified' do - spammy_title = 'Whatever' - - spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title) - snippet = update_snippet({ title: spammy_title }, - { spam_log_id: spam_logs.last.id, - recaptcha_verification: true }) - - expect(response).to redirect_to(project_snippet_path(project, snippet)) - end - end - end - - context 'when the private snippet is made public' do - let(:visibility_level) { Snippet::PRIVATE } - - it 'rejects the snippet' do - expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) } - .not_to change { snippet.reload.title } - end - - it 'creates a spam log' do - expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) } - .to log_spam(title: 'Foo', user_id: user.id, noteable_type: 'ProjectSnippet') - end - - it 'renders :edit with reCAPTCHA disabled' do - stub_application_setting(recaptcha_enabled: false) - - update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) - - expect(response).to render_template(:edit) - end - - context 'reCAPTCHA enabled' do - before do - stub_application_setting(recaptcha_enabled: true) - end - - it 'renders :verify' do - update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) - - expect(response).to render_template(:verify) - end - - it 'renders snippet page' do - spammy_title = 'Whatever' - - spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title) - snippet = update_snippet({ title: spammy_title, visibility_level: Snippet::PUBLIC }, - { spam_log_id: spam_logs.last.id, - recaptcha_verification: true }) - - expect(response).to redirect_to(project_snippet_path(project, snippet)) - end - end - end - end - end - describe 'POST #mark_as_spam' do let_it_be(:snippet) { create(:project_snippet, :private, project: project, author: user) } @@ -329,12 +120,6 @@ RSpec.describe Projects::SnippetsController do expect(assigns(:snippet)).to eq(project_snippet) expect(response).to have_gitlab_http_status(:ok) end - - it 'renders the blob from the repository' do - subject - - expect(assigns(:blob)).to eq(project_snippet.blobs.first) - end end %w[show raw].each do |action| @@ -395,6 +180,16 @@ RSpec.describe Projects::SnippetsController do end end + describe 'GET #show as JSON' do + it 'renders the blob from the repository' do + project_snippet = create(:project_snippet, :public, :repository, project: project, author: user) + + get :show, params: { namespace_id: project.namespace, project_id: project, id: project_snippet.to_param }, format: :json + + expect(assigns(:blob)).to eq(project_snippet.blobs.first) + end + end + describe "GET #show for embeddable content" do let(:project_snippet) { create(:project_snippet, :repository, snippet_permission, project: project, author: user) } let(:extra_params) { {} } @@ -533,62 +328,4 @@ RSpec.describe Projects::SnippetsController do it_behaves_like 'content disposition headers' end end - - describe 'DELETE #destroy' do - let_it_be(:snippet) { create(:project_snippet, :private, project: project, author: user) } - - let(:params) do - { - namespace_id: project.namespace.to_param, - project_id: project, - id: snippet.to_param - } - end - - subject { delete :destroy, params: params } - - context 'when current user has ability to destroy the snippet' do - before do - sign_in(user) - end - - it 'removes the snippet' do - subject - - expect { snippet.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - - context 'when snippet is succesfuly destroyed' do - it 'redirects to the project snippets page' do - subject - - expect(response).to redirect_to(project_snippets_path(project)) - end - end - - context 'when snippet is not destroyed' do - before do - allow(snippet).to receive(:destroy).and_return(false) - controller.instance_variable_set(:@snippet, snippet) - end - - it 'renders the snippet page with errors' do - subject - - expect(flash[:alert]).to eq('Failed to remove snippet.') - expect(response).to redirect_to(project_snippet_path(project, snippet)) - end - end - end - - context 'when current_user does not have ability to destroy the snippet' do - it 'responds with status 404' do - sign_in(other_user) - - subject - - expect(response).to have_gitlab_http_status(:not_found) - end - end - end end diff --git a/spec/controllers/projects/static_site_editor_controller_spec.rb b/spec/controllers/projects/static_site_editor_controller_spec.rb index 7883c7e6f81..6ea730cbf27 100644 --- a/spec/controllers/projects/static_site_editor_controller_spec.rb +++ b/spec/controllers/projects/static_site_editor_controller_spec.rb @@ -5,9 +5,11 @@ require 'spec_helper' RSpec.describe Projects::StaticSiteEditorController do let_it_be(:project) { create(:project, :public, :repository) } let_it_be(:user) { create(:user) } - let(:data) { instance_double(Hash) } + let(:data) { { key: 'value' } } describe 'GET show' do + render_views + let(:default_params) do { namespace_id: project.namespace, @@ -50,41 +52,83 @@ RSpec.describe Projects::StaticSiteEditorController do end end - %w[developer maintainer].each do |role| - context "as #{role}" do - before_all do - project.add_role(user, role) + context "as developer" do + before do + allow(Gitlab::UsageDataCounters::StaticSiteEditorCounter).to receive(:increment_views_count) + project.add_role(user, 'developer') + sign_in(user) + get :show, params: default_params + end + + it 'increases the views counter' do + expect(Gitlab::UsageDataCounters::StaticSiteEditorCounter).to have_received(:increment_views_count) + end + + it 'renders the edit page' do + expect(response).to render_template(:show) + end + + it 'assigns ref and path variables' do + expect(assigns(:ref)).to eq('master') + expect(assigns(:path)).to eq('README.md') + end + + context 'when combination of ref and path is incorrect' do + let(:default_params) { super().merge(id: 'unknown') } + + it 'responds with 404 page' do + expect(response).to have_gitlab_http_status(:not_found) end + end + + context 'when invalid config file' do + let(:service_response) { ServiceResponse.error(message: 'invalid') } - before do - sign_in(user) - get :show, params: default_params + it 'redirects to project page and flashes error message' do + expect(response).to redirect_to(project_path(project)) + expect(response).to set_flash[:alert].to('invalid') end + end - it 'renders the edit page' do - expect(response).to render_template(:show) + context 'with a service response payload containing multiple data types' do + let(:data) do + { + a_string: 'string', + an_array: [ + { + foo: 'bar' + } + ], + an_integer: 123, + a_hash: { + a_deeper_hash: { + foo: 'bar' + } + }, + a_boolean: true + } end - it 'assigns a required variables' do - expect(assigns(:data)).to eq(data) - expect(assigns(:ref)).to eq('master') - expect(assigns(:path)).to eq('README.md') + let(:assigns_data) { assigns(:data) } + + it 'leaves data values which are strings as strings' do + expect(assigns_data[:a_string]).to eq('string') end - context 'when combination of ref and path is incorrect' do - let(:default_params) { super().merge(id: 'unknown') } + it 'leaves data values which are integers as integers' do + expect(assigns_data[:an_integer]).to eq(123) + end - it 'responds with 404 page' do - expect(response).to have_gitlab_http_status(:not_found) - end + it 'serializes data values which are booleans to JSON' do + expect(assigns_data[:a_boolean]).to eq('true') end - context 'when invalid config file' do - let(:service_response) { ServiceResponse.error(message: 'invalid') } + it 'serializes data values which are arrays to JSON' do + expect(assigns_data[:an_array]).to eq('[{"foo":"bar"}]') + end - it 'returns 422' do - expect(response).to have_gitlab_http_status(:unprocessable_entity) - end + it 'serializes data values which are hashes to JSON' do + expect(assigns_data[:a_hash]).to eq('{"a_deeper_hash":{"foo":"bar"}}') end end end diff --git a/spec/controllers/projects/tags_controller_spec.rb b/spec/controllers/projects/tags_controller_spec.rb index d213d003bed..57760088183 100644 --- a/spec/controllers/projects/tags_controller_spec.rb +++ b/spec/controllers/projects/tags_controller_spec.rb @@ -131,4 +131,25 @@ RSpec.describe Projects::TagsController do end end end + + describe 'DELETE #destroy' do + let(:tag) { project.repository.add_tag(user, 'fake-tag', 'master') } + let(:request) do + delete(:destroy, params: { id: tag.name, namespace_id: project.namespace.to_param, project_id: project }) + end + + before do + project.add_developer(user) + sign_in(user) + end + + it 'deletes tag' do + request + + expect(response).to be_successful + expect(response.body).to include("Tag was removed") + + expect(project.repository.find_tag(tag.name)).not_to be_present + end + end end diff --git a/spec/controllers/projects/tracings_controller_spec.rb b/spec/controllers/projects/tracings_controller_spec.rb new file mode 100644 index 00000000000..1f8a68cc861 --- /dev/null +++ b/spec/controllers/projects/tracings_controller_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::TracingsController do + let_it_be(:user) { create(:user) } + + describe 'GET show' do + shared_examples 'user with read access' do |visibility_level| + let(:project) { create(:project, visibility_level) } + + %w[developer maintainer].each do |role| + context "with a #{visibility_level} project and #{role} role" do + before do + project.add_role(user, role) + end + + it 'renders OK' do + get :show, params: { namespace_id: project.namespace, project_id: project } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:show) + end + end + end + end + + shared_examples 'user without read access' do |visibility_level| + let(:project) { create(:project, visibility_level) } + + %w[guest reporter].each do |role| + context "with a #{visibility_level} project and #{role} role" do + before do + project.add_role(user, role) + end + + it 'returns 404' do + get :show, params: { namespace_id: project.namespace, project_id: project } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + end + + before do + sign_in(user) + end + + context 'with maintainer role' do + it_behaves_like 'user with read access', :public + it_behaves_like 'user with read access', :internal + it_behaves_like 'user with read access', :private + end + + context 'without maintainer role' do + it_behaves_like 'user without read access', :public + it_behaves_like 'user without read access', :internal + it_behaves_like 'user without read access', :private + end + end +end diff --git a/spec/controllers/projects/web_ide_terminals_controller_spec.rb b/spec/controllers/projects/web_ide_terminals_controller_spec.rb index 3eb3d5da351..09c471d2885 100644 --- a/spec/controllers/projects/web_ide_terminals_controller_spec.rb +++ b/spec/controllers/projects/web_ide_terminals_controller_spec.rb @@ -9,17 +9,20 @@ RSpec.describe Projects::WebIdeTerminalsController do let_it_be(:developer) { create(:user) } let_it_be(:reporter) { create(:user) } let_it_be(:guest) { create(:user) } - let_it_be(:project) { create(:project, :private, :repository, namespace: owner.namespace) } + let_it_be(:project) do + create(:project, :private, :repository, namespace: owner.namespace).tap do |project| + project.add_maintainer(maintainer) + project.add_developer(developer) + project.add_reporter(reporter) + project.add_guest(guest) + end + end + let(:pipeline) { create(:ci_pipeline, project: project, source: :webide, config_source: :webide_source, user: user) } let(:job) { create(:ci_build, pipeline: pipeline, user: user, project: project) } let(:user) { maintainer } before do - project.add_maintainer(maintainer) - project.add_developer(developer) - project.add_reporter(reporter) - project.add_guest(guest) - sign_in(user) end @@ -158,11 +161,11 @@ RSpec.describe Projects::WebIdeTerminalsController do end context 'access rights' do - before do - subject + it_behaves_like 'terminal access rights' do + before do + subject + end end - - it_behaves_like 'terminal access rights' end it 'increases the web ide terminal counter' do diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index e4374a8f104..0640f9e5724 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -213,13 +213,13 @@ RSpec.describe ProjectsController do expect(assigns(:issuable_meta_data)).not_to be_nil end - it 'shows customize workflow page if wiki and issues are disabled' do + it 'shows activity page if wiki and issues are disabled' do project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED) project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) get :show, params: { namespace_id: project.namespace, id: project } - expect(response).to render_template("projects/_customize_workflow") + expect(response).to render_template("projects/_activity") end it 'shows activity if enabled by user' do diff --git a/spec/controllers/registrations/experience_levels_controller_spec.rb b/spec/controllers/registrations/experience_levels_controller_spec.rb index ee1acf3d93d..4be67f29107 100644 --- a/spec/controllers/registrations/experience_levels_controller_spec.rb +++ b/spec/controllers/registrations/experience_levels_controller_spec.rb @@ -85,16 +85,49 @@ RSpec.describe Registrations::ExperienceLevelsController do end end - context 'when a namespace_path is sent' do - it { is_expected.to have_gitlab_http_status(:redirect) } - it { is_expected.to redirect_to(group_path(namespace)) } - end + describe 'redirection' do + let(:project) { build(:project, namespace: namespace, creator: user, path: 'project-path') } + let(:issues_board) { build(:board, id: 123, project: project) } + + before do + stub_experiment_for_user( + onboarding_issues: true, + default_to_issues_board: default_to_issues_board_xp? + ) + allow_next_instance_of(LearnGitlab) do |learn_gitlab| + allow(learn_gitlab).to receive(:available?).and_return(learn_gitlab_available?) + allow(learn_gitlab).to receive(:project).and_return(project) + allow(learn_gitlab).to receive(:board).and_return(issues_board) + end + end + + context 'when namespace_path param is missing' do + let(:params) { super().merge(namespace_path: nil) } + + where( + default_to_issues_board_xp?: [true, false], + learn_gitlab_available?: [true, false] + ) - context 'when no namespace_path is sent' do - let(:params) { super().merge(namespace_path: nil) } + with_them do + it { is_expected.to redirect_to('/') } + end + end - it { is_expected.to have_gitlab_http_status(:redirect) } - it { is_expected.to redirect_to(root_path) } + context 'when we have a namespace_path param' do + using RSpec::Parameterized::TableSyntax + + where(:default_to_issues_board_xp?, :learn_gitlab_available?, :path) do + true | true | '/group-path/project-path/-/boards/123' + true | false | '/group-path' + false | true | '/group-path' + false | false | '/group-path' + end + + with_them do + it { is_expected.to redirect_to(path) } + end + end end describe 'applying the chosen level' do diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index 60957dc72e6..501d8d4a78d 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -37,65 +37,123 @@ RSpec.describe RegistrationsController do expect(response).to redirect_to(new_user_session_path(anchor: 'register-pane')) end end + end - context 'with sign up flow and terms_opt_in experiment being enabled' do - before do - stub_experiment(signup_flow: true, terms_opt_in: true) - end + describe '#create' do + let(:base_user_params) { { first_name: 'first', last_name: 'last', username: 'new_username', email: 'new@user.com', password: 'Any_password' } } + let(:user_params) { { user: base_user_params } } + + subject { post(:create, params: user_params) } - context 'when user is not part of the experiment' do + context '`blocked_pending_approval` state' do + context 'when the feature is enabled' do before do - stub_experiment_for_user(signup_flow: true, terms_opt_in: false) + stub_feature_flags(admin_approval_for_new_user_signups: true) end - it 'tracks event with right parameters' do - expect(Gitlab::Tracking).to receive(:event).with( - 'Growth::Acquisition::Experiment::TermsOptIn', - 'start', - label: anything, - property: 'control_group' - ) + context 'when the `require_admin_approval_after_user_signup` setting is turned on' do + before do + stub_application_setting(require_admin_approval_after_user_signup: true) + end - subject + it 'signs up the user in `blocked_pending_approval` state' do + subject + created_user = User.find_by(email: 'new@user.com') + + expect(created_user).to be_present + expect(created_user.blocked_pending_approval?).to eq(true) + end + + it 'does not log in the user after sign up' do + subject + + expect(controller.current_user).to be_nil + end + + it 'shows flash message after signing up' do + subject + + expect(response).to redirect_to(new_user_session_path(anchor: 'login-pane')) + expect(flash[:notice]) + .to eq('You have signed up successfully. However, we could not sign you in because your account is awaiting approval from your GitLab administrator.') + end + + context 'email confirmation' do + context 'when `send_user_confirmation_email` is true' do + before do + stub_application_setting(send_user_confirmation_email: true) + end + + it 'does not send a confirmation email' do + expect { subject } + .not_to have_enqueued_mail(DeviseMailer, :confirmation_instructions) + end + end + end + end + + context 'when the `require_admin_approval_after_user_signup` setting is turned off' do + before do + stub_application_setting(require_admin_approval_after_user_signup: false) + end + + it 'signs up the user in `active` state' do + subject + created_user = User.find_by(email: 'new@user.com') + + expect(created_user).to be_present + expect(created_user.active?).to eq(true) + end + + it 'does not show any flash message after signing up' do + subject + + expect(flash[:notice]).to be_nil + end + + context 'email confirmation' do + context 'when `send_user_confirmation_email` is true' do + before do + stub_application_setting(send_user_confirmation_email: true) + end + + it 'sends a confirmation email' do + expect { subject } + .to have_enqueued_mail(DeviseMailer, :confirmation_instructions) + end + end + end end end - context 'when user is part of the experiment' do + context 'when the feature is disabled' do before do - stub_experiment_for_user(signup_flow: true, terms_opt_in: true) + stub_feature_flags(admin_approval_for_new_user_signups: false) end - it 'tracks event with right parameters' do - expect(Gitlab::Tracking).to receive(:event).with( - 'Growth::Acquisition::Experiment::TermsOptIn', - 'start', - label: anything, - property: 'experimental_group' - ) + context 'when the `require_admin_approval_after_user_signup` setting is turned on' do + before do + stub_application_setting(require_admin_approval_after_user_signup: true) + end - subject + it 'signs up the user in `active` state' do + subject + + created_user = User.find_by(email: 'new@user.com') + expect(created_user).to be_present + expect(created_user.active?).to eq(true) + end end end end - end - - describe '#create' do - let(:base_user_params) { { name: 'new_user', username: 'new_username', email: 'new@user.com', password: 'Any_password' } } - let(:user_params) { { user: base_user_params } } context 'email confirmation' do - around do |example| - perform_enqueued_jobs do - example.run - end - end - context 'when send_user_confirmation_email is false' do it 'signs the user in' do stub_application_setting(send_user_confirmation_email: false) - expect { post(:create, params: user_params) }.not_to change { ActionMailer::Base.deliveries.size } - expect(subject.current_user).not_to be_nil + expect { subject }.not_to have_enqueued_mail(DeviseMailer, :confirmation_instructions) + expect(controller.current_user).not_to be_nil end end @@ -111,10 +169,8 @@ RSpec.describe RegistrationsController do end it 'does not authenticate the user and sends a confirmation email' do - post(:create, params: user_params) - - expect(ActionMailer::Base.deliveries.last.to.first).to eq(user_params[:user][:email]) - expect(subject.current_user).to be_nil + expect { subject }.to have_enqueued_mail(DeviseMailer, :confirmation_instructions) + expect(controller.current_user).to be_nil end end @@ -125,9 +181,8 @@ RSpec.describe RegistrationsController do end it 'authenticates the user and sends a confirmation email' do - post(:create, params: user_params) - - expect(ActionMailer::Base.deliveries.last.to.first).to eq(user_params[:user][:email]) + expect { subject }.to have_enqueued_mail(DeviseMailer, :confirmation_instructions) + expect(controller.current_user).to be_present expect(response).to redirect_to(users_sign_up_welcome_path) end end @@ -137,7 +192,7 @@ RSpec.describe RegistrationsController do it 'redirects to sign_in' do stub_application_setting(signup_enabled: false) - expect { post(:create, params: user_params) }.not_to change(User, :count) + expect { subject }.not_to change(User, :count) expect(response).to redirect_to(new_user_session_path) end end @@ -158,14 +213,14 @@ RSpec.describe RegistrationsController do it 'displays an error when the reCAPTCHA is not solved' do allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) - post(:create, params: user_params) + subject expect(response).to render_template(:new) expect(flash[:alert]).to eq(_('There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.')) end it 'redirects to the welcome page when the reCAPTCHA is solved' do - post(:create, params: user_params) + subject expect(response).to redirect_to(users_sign_up_welcome_path) end @@ -258,102 +313,26 @@ RSpec.describe RegistrationsController do end end - context 'when terms are enforced' do - before do - enforce_terms - end - - it 'redirects back with a notice when the checkbox was not checked' do - post :create, params: user_params - - expect(flash[:alert]).to eq(_('You must accept our Terms of Service and privacy policy in order to register an account')) - end - - it 'creates the user with agreement when terms are accepted' do - post :create, params: user_params.merge(terms_opt_in: '1') - - expect(subject.current_user).to be_present - expect(subject.current_user.terms_accepted?).to be(true) - end - - context 'when experiment terms_opt_in is enabled' do + context 'terms of service' do + context 'when terms are enforced' do before do - stub_experiment(terms_opt_in: true) - end - - context 'when user is part of the experiment' do - before do - stub_experiment_for_user(terms_opt_in: true) - end - - it 'creates the user with accepted terms' do - post :create, params: user_params - - expect(subject.current_user).to be_present - expect(subject.current_user.terms_accepted?).to be(true) - end + enforce_terms end - context 'when user is not part of the experiment' do - before do - stub_experiment_for_user(terms_opt_in: false) - end - - it 'creates the user without accepted terms' do - post :create, params: user_params + it 'creates the user with accepted terms' do + subject - expect(flash[:alert]).to eq(_('You must accept our Terms of Service and privacy policy in order to register an account')) - end + expect(controller.current_user).to be_present + expect(controller.current_user.terms_accepted?).to be(true) end end - end - - describe 'tracking data' do - context 'with sign up flow and terms_opt_in experiment being enabled' do - subject { post :create, params: user_params } - - before do - stub_experiment(signup_flow: true, terms_opt_in: true) - end - - it 'records user for the terms_opt_in experiment' do - expect(controller).to receive(:record_experiment_user).with(:terms_opt_in) + context 'when terms are not enforced' do + it 'creates the user without accepted terms' do subject - end - context 'when user is not part of the experiment' do - before do - stub_experiment_for_user(signup_flow: true, terms_opt_in: false) - end - - it 'tracks event with right parameters' do - expect(Gitlab::Tracking).to receive(:event).with( - 'Growth::Acquisition::Experiment::TermsOptIn', - 'end', - label: anything, - property: 'control_group' - ) - - subject - end - end - - context 'when user is part of the experiment' do - before do - stub_experiment_for_user(signup_flow: true, terms_opt_in: true) - end - - it 'tracks event with right parameters' do - expect(Gitlab::Tracking).to receive(:event).with( - 'Growth::Acquisition::Experiment::TermsOptIn', - 'end', - label: anything, - property: 'experimental_group' - ) - - subject - end + expect(controller.current_user).to be_present + expect(controller.current_user.terms_accepted?).to be(false) end end end @@ -361,30 +340,21 @@ RSpec.describe RegistrationsController do it "logs a 'User Created' message" do expect(Gitlab::AppLogger).to receive(:info).with(/\AUser Created: username=new_username email=new@user.com.+\z/).and_call_original - post(:create, params: user_params) + subject end it 'handles when params are new_user' do post(:create, params: { new_user: base_user_params }) - expect(subject.current_user).not_to be_nil + expect(controller.current_user).not_to be_nil end - context 'with the experimental signup flow enabled and the user is part of the experimental group' do - before do - stub_experiment(signup_flow: true) - stub_experiment_for_user(signup_flow: true) - end - - let(:base_user_params) { { first_name: 'First', last_name: 'Last', username: 'new_username', email: 'new@user.com', password: 'Any_password' } } - - it 'sets name from first and last name' do - post :create, params: { new_user: base_user_params } + it 'sets name from first and last name' do + post :create, params: { new_user: base_user_params } - expect(User.last.first_name).to eq(base_user_params[:first_name]) - expect(User.last.last_name).to eq(base_user_params[:last_name]) - expect(User.last.name).to eq("#{base_user_params[:first_name]} #{base_user_params[:last_name]}") - end + expect(User.last.first_name).to eq(base_user_params[:first_name]) + expect(User.last.last_name).to eq(base_user_params[:last_name]) + expect(User.last.name).to eq("#{base_user_params[:first_name]} #{base_user_params[:last_name]}") end end @@ -507,10 +477,16 @@ RSpec.describe RegistrationsController do patch :update_registration, params: { user: { role: 'software_developer', setup_for_company: 'false' } } end - before do - sign_in(create(:user)) + context 'without a signed in user' do + it { is_expected.to redirect_to new_user_registration_path } end - it { is_expected.to redirect_to(dashboard_projects_path)} + context 'with a signed in user' do + before do + sign_in(create(:user)) + end + + it { is_expected.to redirect_to(dashboard_projects_path)} + end end end diff --git a/spec/controllers/runner_setup_controller_spec.rb b/spec/controllers/runner_setup_controller_spec.rb new file mode 100644 index 00000000000..0b237500907 --- /dev/null +++ b/spec/controllers/runner_setup_controller_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe RunnerSetupController do + let(:user) { create(:user) } + + before do + sign_in(user) + end + + describe 'GET #platforms' do + it 'renders the platforms' do + get :platforms + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to have_key("windows") + expect(json_response).to have_key("kubernetes") + end + end +end diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb index f244392bbad..a0cb696828d 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/controllers/search_controller_spec.rb @@ -183,7 +183,7 @@ RSpec.describe SearchController do end it_behaves_like 'tracking unique hll events', :search_track_unique_users do - subject { get :show, params: { scope: 'projects', search: 'term' }, format: format } + subject(:request) { get :show, params: { scope: 'projects', search: 'term' } } let(:target_id) { 'i_search_total' } let(:expected_type) { instance_of(String) } diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index 688539f2a03..75bcc32e6f3 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -78,6 +78,9 @@ RSpec.describe SessionsController do end context 'when using standard authentications' do + let(:user) { create(:user) } + let(:post_action) { post(:create, params: { user: { login: user.username, password: user.password } }) } + context 'invalid password' do it 'does not authenticate user' do post(:create, params: { user: { login: 'invalid', password: 'invalid' } }) @@ -87,6 +90,36 @@ RSpec.describe SessionsController do end end + context 'a blocked user' do + it 'does not authenticate the user' do + user.block! + post_action + + expect(@request.env['warden']).not_to be_authenticated + expect(flash[:alert]).to include('Your account has been blocked') + end + end + + context 'a `blocked pending approval` user' do + it 'does not authenticate the user' do + user.block_pending_approval! + post_action + + expect(@request.env['warden']).not_to be_authenticated + expect(flash[:alert]).to include('Your account is pending approval from your GitLab administrator and hence blocked') + end + end + + context 'an internal user' do + it 'does not authenticate the user' do + user.ghost! + post_action + + expect(@request.env['warden']).not_to be_authenticated + expect(flash[:alert]).to include('Your account does not have the required permission to login') + end + end + context 'when using valid password', :clean_gitlab_redis_shared_state do let(:user) { create(:user) } let(:user_params) { { login: user.username, password: user.password } } diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index 6517922d92a..1ccba7f9114 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -4,6 +4,8 @@ require 'spec_helper' RSpec.describe SnippetsController do let_it_be(:user) { create(:user) } + let_it_be(:other_user) { create(:user) } + let_it_be(:public_snippet) { create(:personal_snippet, :public, :repository, author: user) } describe 'GET #index' do let(:base_params) { { username: user.username } } @@ -12,10 +14,6 @@ RSpec.describe SnippetsController do it_behaves_like 'paginated collection' do let(:collection) { Snippet.all } let(:params) { { username: user.username } } - - before do - create(:personal_snippet, :public, author: user) - end end it 'renders snippets of a user when username is present' do @@ -86,12 +84,6 @@ RSpec.describe SnippetsController do expect(assigns(:snippet)).to eq(personal_snippet) expect(response).to have_gitlab_http_status(:ok) end - - it 'renders the blob from the repository' do - subject - - expect(assigns(:blob)).to eq(personal_snippet.blobs.first) - end end context 'when the personal snippet is private' do @@ -103,8 +95,7 @@ RSpec.describe SnippetsController do end context 'when signed in user is not the author' do - let(:other_author) { create(:author) } - let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) } + let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_user) } it 'responds with status 404' do get :show, params: { id: other_personal_snippet.to_param } @@ -164,7 +155,7 @@ RSpec.describe SnippetsController do end context 'when the personal snippet is public' do - let_it_be(:personal_snippet) { create(:personal_snippet, :public, :repository, author: user) } + let(:personal_snippet) { public_snippet } context 'when signed in' do before do @@ -172,22 +163,22 @@ RSpec.describe SnippetsController do end it_behaves_like 'successful response' do - subject { get :show, params: { id: personal_snippet.to_param } } + subject { get :show, params: { id: public_snippet.to_param } } end it 'responds with status 200 when embeddable content is requested' do - get :show, params: { id: personal_snippet.to_param }, format: :js + get :show, params: { id: public_snippet.to_param }, format: :js - expect(assigns(:snippet)).to eq(personal_snippet) + expect(assigns(:snippet)).to eq(public_snippet) expect(response).to have_gitlab_http_status(:ok) end end context 'when not signed in' do it 'renders the snippet' do - get :show, params: { id: personal_snippet.to_param } + get :show, params: { id: public_snippet.to_param } - expect(assigns(:snippet)).to eq(personal_snippet) + expect(assigns(:snippet)).to eq(public_snippet) expect(response).to have_gitlab_http_status(:ok) end end @@ -200,7 +191,7 @@ RSpec.describe SnippetsController do end it 'responds with status 404' do - get :show, params: { id: 'doesntexist' } + get :show, params: { id: non_existing_record_id } expect(response).to have_gitlab_http_status(:not_found) end @@ -208,260 +199,43 @@ RSpec.describe SnippetsController do context 'when not signed in' do it 'responds with status 404' do - get :show, params: { id: 'doesntexist' } + get :show, params: { id: non_existing_record_id } expect(response).to redirect_to(new_user_session_path) end end end - end - - describe 'POST #create' do - def create_snippet(snippet_params = {}, additional_params = {}) - sign_in(user) - - post :create, params: { - personal_snippet: { title: 'Title', content: 'Content', description: 'Description' }.merge(snippet_params) - }.merge(additional_params) - - Snippet.last - end - - it 'creates the snippet correctly' do - snippet = create_snippet(visibility_level: Snippet::PRIVATE) - - expect(snippet.title).to eq('Title') - expect(snippet.content).to eq('Content') - expect(snippet.description).to eq('Description') - end - - context 'when user is not allowed to create a personal snippet' do - let(:user) { create(:user, :external) } - - it 'responds with status 404' do - aggregate_failures do - expect do - create_snippet(visibility_level: Snippet::PUBLIC) - end.not_to change { Snippet.count } - - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - - context 'when the controller receives the files param' do - let(:files) { %w(foo bar) } - - it 'passes the files param to the snippet create service' do - expect(Snippets::CreateService).to receive(:new).with(nil, user, hash_including(files: files)).and_call_original - - create_snippet({ title: nil }, { files: files }) - end - end - - context 'when the snippet is spam' do - before do - allow_next_instance_of(Spam::AkismetService) do |instance| - allow(instance).to receive(:spam?).and_return(true) - end - end - - context 'when the snippet is private' do - it 'creates the snippet' do - expect { create_snippet(visibility_level: Snippet::PRIVATE) } - .to change { Snippet.count }.by(1) - end - end - - context 'when the snippet is public' do - it 'rejects the snippet' do - expect { create_snippet(visibility_level: Snippet::PUBLIC) } - .not_to change { Snippet.count } - end - - it 'creates a spam log' do - expect { create_snippet(visibility_level: Snippet::PUBLIC) } - .to log_spam(title: 'Title', user: user, noteable_type: 'PersonalSnippet') - end - - it 'renders :new with reCAPTCHA disabled' do - stub_application_setting(recaptcha_enabled: false) - - create_snippet(visibility_level: Snippet::PUBLIC) - - expect(response).to render_template(:new) - end - - context 'reCAPTCHA enabled' do - before do - stub_application_setting(recaptcha_enabled: true) - end - - it 'renders :verify' do - create_snippet(visibility_level: Snippet::PUBLIC) - - expect(response).to render_template(:verify) - end - - it 'renders snippet page' do - spammy_title = 'Whatever' - - spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title) - snippet = create_snippet({ title: spammy_title }, - { spam_log_id: spam_logs.last.id, - recaptcha_verification: true }) - - expect(response).to redirect_to(snippet_path(snippet)) - end - end - end - end - end - - describe 'PUT #update' do - let(:project) { create :project } - let(:visibility_level) { Snippet::PUBLIC } - let(:snippet) { create :personal_snippet, author: user, project: project, visibility_level: visibility_level } - - def update_snippet(snippet_params = {}, additional_params = {}) - sign_in(user) - - put :update, params: { - id: snippet.id, - personal_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params) - }.merge(additional_params) - - snippet.reload - end - - context 'when the snippet is spam' do - before do - allow_next_instance_of(Spam::AkismetService) do |instance| - allow(instance).to receive(:spam?).and_return(true) - end - end - - context 'when the snippet is private' do - let(:visibility_level) { Snippet::PRIVATE } - - it 'updates the snippet' do - expect { update_snippet(title: 'Foo') } - .to change { snippet.reload.title }.to('Foo') - end - end - - context 'when a private snippet is made public' do - let(:visibility_level) { Snippet::PRIVATE } - - it 'rejects the snippet' do - expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) } - .not_to change { snippet.reload.title } - end - - it 'creates a spam log' do - expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) } - .to log_spam(title: 'Foo', user: user, noteable_type: 'PersonalSnippet') - end - - it 'renders :edit with reCAPTCHA disabled' do - stub_application_setting(recaptcha_enabled: false) - - update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) - - expect(response).to render_template(:edit) - end - - context 'reCAPTCHA enabled' do - before do - stub_application_setting(recaptcha_enabled: true) - end - - it 'renders :verify' do - update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) - - expect(response).to render_template(:verify) - end - - it 'renders snippet page when reCAPTCHA verified' do - spammy_title = 'Whatever' - - spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title) - snippet = update_snippet({ title: spammy_title, visibility_level: Snippet::PUBLIC }, - { spam_log_id: spam_logs.last.id, - recaptcha_verification: true }) - - expect(response).to redirect_to(snippet_path(snippet)) - end - end - end - - context 'when the snippet is public' do - it 'rejects the snippet' do - expect { update_snippet(title: 'Foo') } - .not_to change { snippet.reload.title } - end - - it 'creates a spam log' do - expect {update_snippet(title: 'Foo') } - .to log_spam(title: 'Foo', user: user, noteable_type: 'PersonalSnippet') - end - - it 'renders :edit with reCAPTCHA disabled' do - stub_application_setting(recaptcha_enabled: false) - - update_snippet(title: 'Foo') - - expect(response).to render_template(:edit) - end - - context 'recaptcha enabled' do - before do - stub_application_setting(recaptcha_enabled: true) - end - - it 'renders :verify' do - update_snippet(title: 'Foo') - - expect(response).to render_template(:verify) - end - - it 'renders snippet page when reCAPTCHA verified' do - spammy_title = 'Whatever' - spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title) - snippet = update_snippet({ title: spammy_title }, - { spam_log_id: spam_logs.last.id, - recaptcha_verification: true }) + context 'when requesting JSON' do + it 'renders the blob from the repository' do + get :show, params: { id: public_snippet.to_param }, format: :json - expect(response).to redirect_to(snippet_path(snippet)) - end - end + expect(assigns(:blob)).to eq(public_snippet.blobs.first) end end end describe 'POST #mark_as_spam' do - let(:snippet) { create(:personal_snippet, :public, author: user) } - before do allow_next_instance_of(Spam::AkismetService) do |instance| allow(instance).to receive_messages(submit_spam: true) end + stub_application_setting(akismet_enabled: true) end def mark_as_spam admin = create(:admin) - create(:user_agent_detail, subject: snippet) + create(:user_agent_detail, subject: public_snippet) sign_in(admin) - post :mark_as_spam, params: { id: snippet.id } + post :mark_as_spam, params: { id: public_snippet.id } end it 'updates the snippet' do mark_as_spam - expect(snippet.reload).not_to be_submittable_as_spam + expect(public_snippet.reload).not_to be_submittable_as_spam end end @@ -489,9 +263,7 @@ RSpec.describe SnippetsController do shared_examples 'CRLF line ending' do let(:content) { "first line\r\nsecond line\r\nthird line" } let(:formatted_content) { content.gsub(/\r\n/, "\n") } - let(:snippet) do - create(:personal_snippet, :public, :repository, author: user, content: content) - end + let(:snippet) { public_snippet } before do allow_next_instance_of(Blob) do |instance| @@ -560,8 +332,7 @@ RSpec.describe SnippetsController do end context 'when signed in user is not the author' do - let(:other_author) { create(:author) } - let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) } + let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_user) } it 'responds with status 404' do get :raw, params: { id: other_personal_snippet.to_param } @@ -605,7 +376,7 @@ RSpec.describe SnippetsController do end context 'when the personal snippet is public' do - let_it_be(:snippet) { create(:personal_snippet, :public, :repository, author: user) } + let(:snippet) { public_snippet } context 'when signed in' do before do @@ -632,7 +403,7 @@ RSpec.describe SnippetsController do end it 'responds with status 404' do - get :raw, params: { id: 'doesntexist' } + get :raw, params: { id: non_existing_record_id } expect(response).to have_gitlab_http_status(:not_found) end @@ -640,7 +411,7 @@ RSpec.describe SnippetsController do context 'when not signed in' do it 'redirects to the sign in path' do - get :raw, params: { id: 'doesntexist' } + get :raw, params: { id: non_existing_record_id } expect(response).to redirect_to(new_user_session_path) end @@ -649,11 +420,10 @@ RSpec.describe SnippetsController do end context 'award emoji on snippets' do - let(:personal_snippet) { create(:personal_snippet, :public, author: user) } - let(:another_user) { create(:user) } + let(:personal_snippet) { public_snippet } before do - sign_in(another_user) + sign_in(other_user) end describe 'POST #toggle_award_emoji' do @@ -678,66 +448,12 @@ RSpec.describe SnippetsController do end describe 'POST #preview_markdown' do - let(:snippet) { create(:personal_snippet, :public) } - it 'renders json in a correct format' do sign_in(user) - post :preview_markdown, params: { id: snippet, text: '*Markdown* text' } + post :preview_markdown, params: { id: public_snippet, text: '*Markdown* text' } expect(json_response.keys).to match_array(%w(body references)) end end - - describe 'DELETE #destroy' do - let!(:snippet) { create :personal_snippet, author: user } - - context 'when current user has ability to destroy the snippet' do - before do - sign_in(user) - end - - it 'removes the snippet' do - delete :destroy, params: { id: snippet.to_param } - - expect { snippet.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - - context 'when snippet is succesfuly destroyed' do - it 'redirects to the project snippets page' do - delete :destroy, params: { id: snippet.to_param } - - expect(response).to redirect_to(dashboard_snippets_path) - end - end - - context 'when snippet is not destroyed' do - before do - allow(snippet).to receive(:destroy).and_return(false) - controller.instance_variable_set(:@snippet, snippet) - end - - it 'renders the snippet page with errors' do - delete :destroy, params: { id: snippet.to_param } - - expect(flash[:alert]).to eq('Failed to remove snippet.') - expect(response).to redirect_to(snippet_path(snippet)) - end - end - end - - context 'when current_user does not have ability to destroy the snippet' do - let(:another_user) { create(:user) } - - before do - sign_in(another_user) - end - - it 'responds with status 404' do - delete :destroy, params: { id: snippet.to_param } - - expect(response).to have_gitlab_http_status(:not_found) - end - end - end end |