diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-21 07:08:36 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-21 07:08:36 +0000 |
commit | 48aff82709769b098321c738f3444b9bdaa694c6 (patch) | |
tree | e00c7c43e2d9b603a5a6af576b1685e400410dee /spec/controllers/projects | |
parent | 879f5329ee916a948223f8f43d77fba4da6cd028 (diff) | |
download | gitlab-ce-48aff82709769b098321c738f3444b9bdaa694c6.tar.gz |
Add latest changes from gitlab-org/gitlab@13-5-stable-eev13.5.0-rc42
Diffstat (limited to 'spec/controllers/projects')
31 files changed, 2615 insertions, 589 deletions
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 |