ee/spec/requests/api/projects_spec.rb (1,711 lines of code) (raw):

# frozen_string_literal: true require 'spec_helper' RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and_projects do include ExternalAuthorizationServiceHelpers include StubRequests let_it_be(:user) { create(:user) } let_it_be(:another_user) { create(:user) } let(:project) { create(:project, namespace: user.namespace) } shared_examples 'inaccessable by reporter role and lower' do context 'for reporter' do before do reporter = create(:user) project.add_reporter(reporter) get api(path, reporter) end it 'returns 403 response' do expect(response).to have_gitlab_http_status(:forbidden) end end context 'for guest' do before do guest = create(:user) project.add_guest(guest) get api(path, guest) end it 'returns 403 response' do expect(response).to have_gitlab_http_status(:forbidden) end end context 'for anonymous' do before do anonymous = create(:user) get api(path, anonymous) end it 'returns 403 response' do expect(response).to have_gitlab_http_status(:forbidden) end end end describe 'GET /projects' do it 'does not break on license checks' do enable_namespace_license_check! create(:project, :private, namespace: user.namespace) create(:project, :public, namespace: user.namespace) get api('/projects', user) expect(response).to have_gitlab_http_status(:ok) end context 'when there are several projects owned by groups' do let_it_be(:admin) { create(:admin) } it 'avoids N+1 queries', :use_sql_query_cache do create(:project, :public, namespace: create(:group)) control = ActiveRecord::QueryRecorder.new(skip_cached: false) do get api('/projects', admin) end create_list(:project, 2, :public, namespace: create(:group)) expect do get api('/projects', admin) end.not_to exceed_all_query_limit(control) end end context 'when user requests hidden projects' do let_it_be(:hidden) { create(:project, :public, :hidden) } let(:filter_params) { { include_hidden: true } } context 'when user is not admin' do before do project.add_owner(user) end it 'does not return hidden projects' do get api('/projects', user), params: filter_params expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Array expect(json_response.map { |p| p['id'] }).not_to include(hidden.id) end end context 'when user is an admin' do let_it_be(:admin) { create(:admin) } it 'also returns hidden projects' do get api("/projects", admin, admin_mode: true), params: filter_params expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Array expect(json_response.map { |p| p['id'] }).to include(hidden.id) end context 'when include_hidden option is off' do let(:filter_params) { { include_hidden: nil } } it 'does not return hidden projects' do get api("/projects", admin, admin_mode: true), params: filter_params expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Array expect(json_response.map { |p| p['id'] }).not_to include(hidden.id) end end end end context 'when custom roles are enabled' do let_it_be(:admin) { create(:admin) } before do stub_licensed_features(custom_roles: true) end it 'avoids N+1 queries', :use_sql_query_cache do create(:project, :public, namespace: create(:group)) control = ActiveRecord::QueryRecorder.new(skip_cached: false) do get api('/projects', admin) end create_list(:project, 2, :public, namespace: create(:group)) expect do get api('/projects', admin) end.not_to exceed_all_query_limit(control) end end end describe 'GET /projects/:id' do subject { get api("/projects/#{project.id}", user) } context 'with external authorization' do let(:project) do create(:project, namespace: user.namespace, external_authorization_classification_label: 'the-label') end before do stub_licensed_features(external_authorization_service_api_management: true) end context 'when the user has access to the project' do before do external_service_allow_access(user, project) end it 'includes the label in the response' do get api("/projects/#{project.id}", user) expect(response).to have_gitlab_http_status(:ok) expect(json_response['external_authorization_classification_label']).to eq('the-label') end context 'when authenticated with a token that has the ai_workflows scope' do let(:oauth_token) { create(:oauth_access_token, user: user, scopes: [:ai_workflows]) } it 'is successful' do get api("/projects/#{project.id}", oauth_access_token: oauth_token) expect(response).to have_gitlab_http_status(:ok) end end end context 'when the external service denies access' do before do external_service_deny_access(user, project) end it 'returns a 404' do get api("/projects/#{project.id}", user) expect(response).to have_gitlab_http_status(:not_found) end end context 'it does not return the label when the feature is not available' do before do stub_licensed_features(external_authorization_service_api_management: false) end it 'does not include the label in the response' do get api("/projects/#{project.id}", user) expect(response).to have_gitlab_http_status(:ok) expect(json_response['external_authorization_classification_label']).to be_nil end end context 'with ip restriction' do let(:group) { create :group, :private } before do create(:ip_restriction, group: group) group.add_maintainer(user) project.update!(namespace: group) end context 'when the group_ip_restriction feature is not available' do before do stub_licensed_features(group_ip_restriction: false) end it 'returns 200' do get api("/projects/#{project.id}", user) expect(response).to have_gitlab_http_status(:ok) end end context 'when the group_ip_restriction feature is available' do before do stub_licensed_features(group_ip_restriction: true) end it 'returns 404 for request from ip not in the range' do get api("/projects/#{project.id}", user) expect(response).to have_gitlab_http_status(:not_found) end it 'returns 200 for request from ip in the range' do get api("/projects/#{project.id}", user), headers: { 'REMOTE_ADDR' => '192.168.0.0' } expect(response).to have_gitlab_http_status(:ok) end end end end describe 'compliance_frameworks attribute' do context 'when compliance_framework feature is available' do context 'when project has a compliance framework' do before do create(:compliance_framework_project_setting, :sox, project: project) get api("/projects/#{project.id}", user) end it 'exposes framework names as array of strings' do expect(json_response['compliance_frameworks']).to contain_exactly(project.compliance_framework_settings.first.compliance_management_framework.name) end end context 'when project has no compliance framework' do before do get api("/projects/#{project.id}", user) end it 'returns an empty array' do expect(json_response['compliance_frameworks']).to eq([]) end end end end describe 'ci_restrict_pipeline_cancellation_role' do before do project.add_maintainer(user) end context 'when unavailable' do it 'does not include ci_restrict_pipeline_cancellation_role' do subject expect(response).to have_gitlab_http_status(:ok) expect(json_response).not_to have_key 'ci_restrict_pipeline_cancellation_role' end end context 'when available' do before do allow_next_instance_of(Ci::ProjectCancellationRestriction) do |cr| allow(cr).to receive(:feature_available?).and_return(true) end end it 'includes ci_restrict_pipeline_cancellation_role' do subject expect(response).to have_gitlab_http_status(:ok) expect(json_response).to have_key 'ci_restrict_pipeline_cancellation_role' end end end context 'issuable default templates' do let(:project) { create(:project, :public) } context 'when feature is available' do before do stub_licensed_features(issuable_default_templates: true) end it 'returns issuable default templates' do subject expect(response).to have_gitlab_http_status(:ok) expect(json_response).to have_key 'issues_template' expect(json_response).to have_key 'merge_requests_template' end context 'when user does not have permission to see issues' do let(:project) { create(:project, :public, :issues_private) } it 'does not return issue default templates' do subject expect(response).to have_gitlab_http_status(:ok) expect(json_response).not_to have_key 'issues_template' expect(json_response).to have_key 'merge_requests_template' end end context 'when user does not have permission to see merge requests' do let(:project) { create(:project, :public, :merge_requests_private) } it 'does not return merge request default templates' do subject expect(response).to have_gitlab_http_status(:ok) expect(json_response).to have_key 'issues_template' expect(json_response).not_to have_key 'merge_requests_template' end end end context 'issuable default templates feature not available' do before do stub_licensed_features(issuable_default_templates: false) end it 'does not return issuable default templates' do subject expect(response).to have_gitlab_http_status(:ok) expect(json_response).not_to have_key 'issues_template' expect(json_response).not_to have_key 'merge_requests_template' end end end context 'merge pipelines feature is available through license' do before do stub_licensed_features(merge_pipelines: true) end it 'returns merge pipelines enabled flag' do subject expect(response).to have_gitlab_http_status(:ok) expect(json_response).to have_key 'merge_pipelines_enabled' end end context 'merge pipelines feature is available through usage ping features' do before do stub_usage_ping_features(true) end it 'returns merge pipelines enabled flag' do subject expect(response).to have_gitlab_http_status(:ok) expect(json_response).to have_key 'merge_pipelines_enabled' end end context 'when usage ping is disabled on free license' do before do stub_usage_ping_features(false) end it 'does not return merge pipelines enabled flag' do subject expect(response).to have_gitlab_http_status(:ok) expect(json_response).not_to have_key 'merge_pipelines_enabled' end end context 'merge pipelines feature not available' do before do stub_licensed_features(merge_pipelines: false) end it 'does not return merge pipelines enabled flag' do subject expect(response).to have_gitlab_http_status(:ok) expect(json_response).not_to have_key 'merge_pipelines_enabled' end end context 'when external_status_checks is available' do before do stub_licensed_features(external_status_checks: true) end it 'returns only_allow_merge_if_all_status_checks_passed flag' do subject expect(response).to have_gitlab_http_status(:ok) expect(json_response).to have_key 'only_allow_merge_if_all_status_checks_passed' end end context 'when external_status_checks not available' do before do stub_licensed_features(external_status_checks: false) end it 'does not return only_allow_merge_if_all_status_checks_passed enabled flag' do subject expect(response).to have_gitlab_http_status(:ok) expect(json_response).not_to have_key 'only_allow_merge_if_all_status_checks_passed' end end context 'merge trains feature is available' do before do stub_licensed_features(merge_pipelines: true, merge_trains: true) project.update!(merge_pipelines_enabled: true, merge_trains_enabled: true) end it 'returns merge trains enabled flag' do subject expect(response).to have_gitlab_http_status(:ok) expect(json_response).to have_key 'merge_trains_enabled' end end context 'merge trains feature not available' do before do stub_licensed_features(merge_trains: false) end it 'does not return merge trains enabled flag' do subject expect(response).to have_gitlab_http_status(:ok) expect(json_response).not_to have_key 'merge_trains_enabled' end end context 'when protected_environments is available' do before do stub_licensed_features(protected_environments: true) end it 'returns allow_pipeline_trigger_approve_deployment flag' do subject expect(response).to have_gitlab_http_status(:ok) expect(json_response).to have_key 'allow_pipeline_trigger_approve_deployment' end end context 'when protected_environments is not available' do before do stub_licensed_features(protected_environments: false) end it 'does not returns allow_pipeline_trigger_approve_deployment flag' do subject expect(response).to have_gitlab_http_status(:ok) expect(json_response).not_to have_key 'allow_pipeline_trigger_approve_deployment' end end end # Assumes the following variables are defined: # group # project # new_project_name # api_call shared_examples 'creates projects with templates' do before do group.add_maintainer(user) stub_licensed_features(custom_project_templates: true) stub_ee_application_setting(custom_project_templates_group_id: group.id) end it 'creates a project using a template' do expect(ProjectExportWorker).to receive(:perform_async).and_call_original Sidekiq::Testing.fake! do expect { api_call }.to change { Project.count }.by(1) end expect(response).to have_gitlab_http_status(:created) project = Project.find(json_response['id']) expect(project.name).to eq(new_project_name) end it 'returns a 400 error for an invalid template name' do project_params.delete(:template_project_id) project_params[:template_name] = 'bogus-template' expect { api_call }.not_to change { Project.count } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['message']['template_name']).to eq(["'bogus-template' is unknown or invalid"]) end it 'returns a 400 error for an invalid template ID' do project_params.delete(:template_name) new_project = create(:project) project_params[:template_project_id] = new_project.id expect { api_call }.not_to change { Project.count } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['message']['template_project_id']).to eq(["#{new_project.id} is unknown or invalid"]) end context 'When template_name and template_project_id both are missing while use_custom_template is true' do let(:project_params) { super().merge(use_custom_template: true).except(:template_name, :template_project_id) } it 'return 400 error with missing template_name and template_project_id error' do expect { api_call }.not_to change { Project.count } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['message']['use_custom_template']).to eq(["must be used with template_name or template_project_id"]) end end end shared_context 'base instance template models' do let(:group) { create(:group) } let!(:project) { create(:project, :public, namespace: group) } let(:new_project_name) { "project-#{SecureRandom.hex}" } end shared_context 'instance template name' do include_context 'base instance template models' let(:project_params) do { template_name: project.name, name: new_project_name, path: new_project_name, use_custom_template: true, namespace_id: group.id } end end shared_context 'instance template ID' do include_context 'base instance template models' let(:project_params) do { template_project_id: project.id, name: new_project_name, path: new_project_name, use_custom_template: true, namespace_id: group.id } end end shared_context 'base group template models' do let(:parent_group) { create(:group) } let(:subgroup) { create(:group, :public, parent: parent_group) } let(:group) { subgroup } let!(:project) { create(:project, :public, namespace: subgroup) } let(:new_project_name) { "project-#{SecureRandom.hex}" } end shared_context 'group template name' do include_context 'base group template models' let(:project_params) do { template_name: project.name, name: new_project_name, path: new_project_name, use_custom_template: true, group_with_project_templates_id: subgroup.id, namespace_id: subgroup.id } end end shared_context 'group template ID' do include_context 'base group template models' let(:project_params) do { template_project_id: project.id, name: new_project_name, path: new_project_name, use_custom_template: true, group_with_project_templates_id: subgroup.id, namespace_id: subgroup.id } end end describe 'GET /projects/:id/users' do shared_examples_for 'project users response' do it 'returns the project users' do get api("/projects/#{project.id}/users", current_user) user = project.namespace.owner expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(1) first_user = json_response.first expect(first_user['username']).to eq(user.username) expect(first_user['name']).to eq(user.name) end end context 'when unauthenticated' do it_behaves_like 'project users response' do let(:project) { create(:project, :public) } let(:current_user) { nil } end end context 'when authenticated' do context 'valid request' do it_behaves_like 'project users response' do let(:current_user) { user } end end end end describe 'POST /projects/user/:id' do let(:admin) { create(:admin) } let(:api_call) { post api("/projects/user/#{user.id}", admin, admin_mode: true), params: project_params } context 'with templates' do include_context 'instance template name' do it_behaves_like 'creates projects with templates' end include_context 'instance template ID' do it_behaves_like 'creates projects with templates' end include_context 'group template name' do it_behaves_like 'creates projects with templates' end include_context 'group template ID' do it_behaves_like 'creates projects with templates' end end end describe 'POST /projects' do let(:api_call) { post api('/projects', user), params: project_params } context 'with templates' do include_context 'instance template name' do it_behaves_like 'creates projects with templates' end include_context 'instance template ID' do it_behaves_like 'creates projects with templates' end include_context 'group template name' do it_behaves_like 'creates projects with templates' end include_context 'group template ID' do it_behaves_like 'creates projects with templates' end end context 'when importing with mirror attributes' do let(:import_url) { generate(:url) } let(:mirror_params) do { name: "Foo", mirror: true, import_url: import_url, mirror_trigger_builds: true } end before do allow(Gitlab::GitalyClient::RemoteService).to receive(:exists?).with(import_url).and_return(true) stub_application_setting(import_sources: ['git']) end it 'creates new project with pull mirroring set up' do post api('/projects', user), params: mirror_params expect(response).to have_gitlab_http_status(:created) expect(Project.find(json_response['id'])).to have_attributes( mirror: true, import_url: import_url, mirror_user_id: user.id, mirror_trigger_builds: true ) end it 'creates project without mirror settings when repository mirroring feature is disabled' do stub_licensed_features(repository_mirrors: false) expect { post api('/projects', user), params: mirror_params } .to change { Project.count }.by(1) expect(response).to have_gitlab_http_status(:created) expect(Project.find(json_response['id'])).to have_attributes( mirror: false, import_url: import_url, mirror_user_id: nil, mirror_trigger_builds: false ) end context 'when pull mirroring is not available' do before do stub_ee_application_setting(mirror_available: false) end it 'ignores the mirroring options' do post api('/projects', user), params: mirror_params expect(response).to have_gitlab_http_status(:created) expect(Project.find(json_response['id']).mirror?).to be false end it 'creates project with mirror settings' do admin = create(:admin) post api('/projects', admin, admin_mode: true), params: mirror_params expect(response).to have_gitlab_http_status(:created) expect(Project.find(json_response['id'])).to have_attributes( mirror: true, import_url: import_url, mirror_user_id: admin.id, mirror_trigger_builds: true ) end end end context 'with requirements_access_level' do let(:project_params) { { name: 'bar', requirements_access_level: 'disabled' } } before do stub_licensed_features(requirements: true) end it 'updates project with given value' do post api('/projects', user), params: project_params expect(response).to have_gitlab_http_status(:created) expect(json_response['requirements_access_level']).to eq(project_params[:requirements_access_level]) end end end describe 'GET projects/:id/audit_events' do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :public, namespace: user.namespace) } let(:path) { "/projects/#{project.id}/audit_events" } it_behaves_like 'inaccessable by reporter role and lower' context 'when authenticated, as a member' do let_it_be(:developer) { create(:user) } before do stub_licensed_features(audit_events: true) project.add_developer(developer) end context 'when read_audit_events_from_new_tables is disabled' do before do stub_feature_flags(read_audit_events_from_new_tables: false) end it 'returns only events authored by current user' do project_audit_event_1 = create(:project_audit_event, entity_id: project.id, author_id: developer.id) create(:project_audit_event, entity_id: project.id, author_id: 666) get api(path, developer) expect_response_contain_exactly(project_audit_event_1.id) end end context 'when read_audit_events_from_new_tables is enabled' do before do stub_feature_flags(read_audit_events_from_new_tables: true) end it 'returns only events authored by current user' do project_audit_event_1 = create(:audit_events_project_audit_event, project_id: project.id, author_id: developer.id) create(:audit_events_project_audit_event, project_id: project.id, author_id: 666) get api(path, developer) expect_response_contain_exactly(project_audit_event_1.id.to_i) end end end context 'when authenticated, as a project owner' do before do project.add_maintainer(user) end context 'audit events feature is not available' do before do stub_licensed_features(audit_events: false) end it_behaves_like '403 response' do let(:request) { get api(path, user) } end end context 'audit events feature is available' do before do stub_licensed_features(audit_events: true) end context 'when read_audit_events_from_new_tables is disabled' do before do stub_feature_flags(read_audit_events_from_new_tables: false) end let_it_be(:project_audit_event_1) { create(:project_audit_event, created_at: Date.new(2000, 1, 10), entity_id: project.id) } let_it_be(:project_audit_event_2) { create(:project_audit_event, created_at: Date.new(2000, 1, 15), entity_id: project.id) } let_it_be(:project_audit_event_3) { create(:project_audit_event, created_at: Date.new(2000, 1, 20), entity_id: project.id) } it 'returns 200 response' do get api(path, user) expect(response).to have_gitlab_http_status(:ok) end it 'includes the correct pagination headers' do audit_events_counts = 3 get api(path, user) expect(response).to include_pagination_headers expect(response.headers['X-Total']).to eq(audit_events_counts.to_s) expect(response.headers['X-Page']).to eq('1') end it 'does not include audit events of a different project' do project = create(:project) audit_event = create(:project_audit_event, created_at: Date.new(2000, 1, 20), entity_id: project.id) get api(path, user) audit_event_ids = json_response.map { |audit_event| audit_event['id'] } expect(audit_event_ids).not_to include(audit_event.id) end context 'parameters' do it_behaves_like 'an endpoint with keyset pagination' do let_it_be(:admin) { create(:admin) } let!(:audit_event_1) { create(:project_audit_event, entity_id: project.id) } let!(:audit_event_2) { create(:project_audit_event, entity_id: project.id) } let(:first_record) { audit_event_2 } let(:second_record) { audit_event_1 } let(:url) { "/projects/#{project.id}/audit_events" } let(:api_call) { api(url, admin, admin_mode: true) } end context 'created_before parameter' do it "returns audit events created before the given parameter" do created_before = '2000-01-20T00:00:00.060Z' get api(path, user), params: { created_before: created_before } expect(json_response.size).to eq 3 expect(json_response.first["id"]).to eq(project_audit_event_3.id) expect(json_response.last["id"]).to eq(project_audit_event_1.id) end end context 'created_after parameter' do it "returns audit events created after the given parameter" do created_after = '2000-01-12T00:00:00.060Z' get api(path, user), params: { created_after: created_after } expect(json_response.size).to eq 2 expect(json_response.first["id"]).to eq(project_audit_event_3.id) expect(json_response.last["id"]).to eq(project_audit_event_2.id) end end end context 'response schema' do it 'matches the response schema' do get api(path, user) expect(response).to match_response_schema('public_api/v4/audit_events', dir: 'ee') end end context 'Snowplow event tracking' do it_behaves_like 'Snowplow event tracking with RedisHLL context' do subject(:api_request) { get api(path, user) } let(:category) { 'EE::API::Projects' } let(:action) { 'project_audit_event_request' } let(:namespace) { project.namespace } let(:context) { [::Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: 'a_compliance_audit_events_api').to_context] } end end end context 'when read_audit_events_from_new_tables is enabled' do before do stub_feature_flags(read_audit_events_from_new_tables: true) end let_it_be(:project_audit_event_1) { create(:audit_events_project_audit_event, created_at: Date.new(2000, 1, 10), project_id: project.id) } let_it_be(:project_audit_event_2) { create(:audit_events_project_audit_event, created_at: Date.new(2000, 1, 15), project_id: project.id) } let_it_be(:project_audit_event_3) { create(:audit_events_project_audit_event, created_at: Date.new(2000, 1, 20), project_id: project.id) } it 'returns 200 response' do get api(path, user) expect(response).to have_gitlab_http_status(:ok) end it 'includes the correct pagination headers' do audit_events_counts = 3 get api(path, user) expect(response).to include_pagination_headers expect(response.headers['X-Total']).to eq(audit_events_counts.to_s) expect(response.headers['X-Page']).to eq('1') end it 'does not include audit events of a different project' do project = create(:project) audit_event = create(:audit_events_project_audit_event, created_at: Date.new(2000, 1, 20), project_id: project.id) get api(path, user) audit_event_ids = json_response.map { |audit_event| audit_event['id'] } expect(audit_event_ids).not_to include(audit_event.id) end context 'parameters' do it_behaves_like 'an endpoint with keyset pagination' do let_it_be(:admin) { create(:admin) } let!(:audit_event_1) { create(:audit_events_project_audit_event, project_id: project.id) } let!(:audit_event_2) { create(:audit_events_project_audit_event, project_id: project.id) } let(:first_record) { audit_event_2 } let(:second_record) { audit_event_1 } let(:url) { "/projects/#{project.id}/audit_events" } let(:api_call) { api(url, admin, admin_mode: true) } end context 'created_before parameter' do it "returns audit events created before the given parameter" do created_before = '2000-01-20T00:00:00.060Z' get api(path, user), params: { created_before: created_before } expect(json_response.size).to eq 3 expect(json_response.first["id"]).to eq(project_audit_event_3.id) expect(json_response.last["id"]).to eq(project_audit_event_1.id) end end context 'created_after parameter' do it "returns audit events created after the given parameter" do created_after = '2000-01-12T00:00:00.060Z' get api(path, user), params: { created_after: created_after } expect(json_response.size).to eq 2 expect(json_response.first["id"]).to eq(project_audit_event_3.id) expect(json_response.last["id"]).to eq(project_audit_event_2.id) end end end context 'response schema' do it 'matches the response schema' do get api(path, user) expect(response).to match_response_schema('public_api/v4/audit_events', dir: 'ee') end end context 'Snowplow event tracking' do it_behaves_like 'Snowplow event tracking with RedisHLL context' do subject(:api_request) { get api(path, user) } let(:category) { 'EE::API::Projects' } let(:action) { 'project_audit_event_request' } let(:namespace) { project.namespace } let(:context) { [::Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: 'a_compliance_audit_events_api').to_context] } end end end end end end describe 'GET projects/:id/audit_events/:audit_event_id' do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :public, namespace: user.namespace) } let(:path) { "/projects/#{project.id}/audit_events/#{project_audit_event.id}" } context 'when read_audit_events_from_new_tables is disabled' do before do stub_feature_flags(read_audit_events_from_new_tables: false) end let_it_be(:project_audit_event) { create(:project_audit_event, created_at: Date.new(2000, 1, 10), entity_id: project.id) } it_behaves_like 'inaccessable by reporter role and lower' context 'when authenticated, as a guest' do let_it_be(:guest) { create(:user) } before do stub_licensed_features(audit_events: true) project.add_guest(guest) end it_behaves_like '403 response' do let(:request) { get api(path, guest) } end end context 'when authenticated, as a member' do let_it_be(:developer) { create(:user) } before do stub_licensed_features(audit_events: true) project.add_developer(developer) end it 'returns 200 response' do audit_event = create(:project_audit_event, entity_id: project.id, author_id: developer.id) path = "/projects/#{project.id}/audit_events/#{audit_event.id}" get api(path, developer) expect(response).to have_gitlab_http_status(:ok) end context 'existing audit event of a different user' do let_it_be(:audit_event) { create(:project_audit_event, entity_id: project.id, author_id: another_user.id) } let(:path) { "/projects/#{project.id}/audit_events/#{audit_event.id}" } it_behaves_like '404 response' do let(:request) { get api(path, developer) } end end end context 'when authenticated, as a project owner' do context 'audit events feature is not available' do before do stub_licensed_features(audit_events: false) end it_behaves_like '403 response' do let(:request) { get api(path, user) } end end context 'audit events feature is available' do before do stub_licensed_features(audit_events: true) end context 'existent audit event' do it 'returns 200 response' do get api(path, user) expect(response).to have_gitlab_http_status(:ok) end context 'response schema' do it 'matches the response schema' do get api(path, user) expect(response).to match_response_schema('public_api/v4/audit_event', dir: 'ee') end end context 'invalid audit_event_id' do let(:path) { "/projects/#{project.id}/audit_events/an-invalid-id" } it_behaves_like '400 response' do let(:request) { get api(path, user) } end end context 'non existent audit event' do context 'non existent audit event of a project' do let(:path) { "/projects/#{project.id}/audit_events/666777" } it_behaves_like '404 response' do let(:request) { get api(path, user) } end end context 'existing audit event of a different project' do let(:new_project) { create(:project) } let(:audit_event) { create(:project_audit_event, created_at: Date.new(2000, 1, 10), entity_id: new_project.id) } let(:path) { "/projects/#{project.id}/audit_events/#{audit_event.id}" } it_behaves_like '404 response' do let(:request) { get api(path, user) } end end end end end end end context 'when read_audit_events_from_new_tables is enabled' do before do stub_feature_flags(read_audit_events_from_new_tables: true) end let_it_be(:project_audit_event) { create(:audit_events_project_audit_event, created_at: Date.new(2000, 1, 10), project_id: project.id) } it_behaves_like 'inaccessable by reporter role and lower' context 'when authenticated, as a guest' do let_it_be(:guest) { create(:user) } before do stub_licensed_features(audit_events: true) project.add_guest(guest) end it_behaves_like '403 response' do let(:request) { get api(path, guest) } end end context 'when authenticated, as a member' do let_it_be(:developer) { create(:user) } before do stub_licensed_features(audit_events: true) project.add_developer(developer) end it 'returns 200 response' do audit_event = create(:audit_events_project_audit_event, project_id: project.id, author_id: developer.id) path = "/projects/#{project.id}/audit_events/#{audit_event.id}" get api(path, developer) expect(response).to have_gitlab_http_status(:ok) end context 'existing audit event of a different user' do let_it_be(:audit_event) { create(:audit_events_project_audit_event, project_id: project.id, author_id: another_user.id) } let(:path) { "/projects/#{project.id}/audit_events/#{audit_event.id}" } it_behaves_like '404 response' do let(:request) { get api(path, developer) } end end end context 'when authenticated, as a project owner' do context 'audit events feature is not available' do before do stub_licensed_features(audit_events: false) end it_behaves_like '403 response' do let(:request) { get api(path, user) } end end context 'audit events feature is available' do before do stub_licensed_features(audit_events: true) end context 'existent audit event' do it 'returns 200 response' do get api(path, user) expect(response).to have_gitlab_http_status(:ok) end context 'response schema' do it 'matches the response schema' do get api(path, user) expect(response).to match_response_schema('public_api/v4/audit_event', dir: 'ee') end end context 'invalid audit_event_id' do let(:path) { "/projects/#{project.id}/audit_events/an-invalid-id" } it_behaves_like '400 response' do let(:request) { get api(path, user) } end end context 'non existent audit event' do context 'non existent audit event of a project' do let(:path) { "/projects/#{project.id}/audit_events/666777" } it_behaves_like '404 response' do let(:request) { get api(path, user) } end end context 'existing audit event of a different project' do let(:new_project) { create(:project) } let(:audit_event) { create(:audit_events_project_audit_event, created_at: Date.new(2000, 1, 10), project_id: new_project.id) } let(:path) { "/projects/#{project.id}/audit_events/#{audit_event.id}" } it_behaves_like '404 response' do let(:request) { get api(path, user) } end end end end end end end end describe 'PUT /projects/:id' do let(:project) { create(:project, namespace: user.namespace) } let(:project_params) { {} } subject { put api("/projects/#{project.id}", user), params: project_params } context 'issuable default templates feature is available' do before do stub_licensed_features(issuable_default_templates: true) end context 'when updating issues_template' do let(:project_params) { { issues_template: '## New Issue Template' } } it 'updates the content' do subject expect(response).to have_gitlab_http_status(:ok) expect(json_response['issues_template']).to eq(project_params[:issues_template]) end end context 'when updating merge_requests_template' do let(:project_params) { { merge_requests_template: '## New Merge Request Template' } } it 'updates the content' do subject expect(response).to have_gitlab_http_status(:ok) expect(json_response['merge_requests_template']).to eq(project_params[:merge_requests_template]) end end context 'when updating requirements_access_level' do let(:project_params) { { requirements_access_level: 'disabled' } } before do stub_licensed_features(requirements: true) end it 'updates project with given value' do subject expect(response).to have_gitlab_http_status(:ok) expect(json_response['requirements_access_level']).to eq(project_params[:requirements_access_level]) end end end context 'issuable default templates feature not available' do before do stub_licensed_features(issuable_default_templates: false) end context 'when updating issues_template' do let(:project_params) { { issues_template: '## New Issue Template' } } it 'does not update the content' do subject expect(response).to have_gitlab_http_status(:ok) expect(json_response).not_to have_key 'issues_template' end end context 'when updating merge_requests_template' do let(:project_params) { { merge_requests_template: '## New Merge Request Template' } } it 'does not update the content' do subject expect(response).to have_gitlab_http_status(:ok) expect(json_response).not_to have_key 'merge_requests_template' end end end context 'merge pipelines feature is available' do before do stub_licensed_features(merge_pipelines: true) end let(:project_params) { { merge_pipelines_enabled: true } } it 'updates the content' do expect { subject }.to change { project.reload.merge_pipelines_enabled } expect(response).to have_gitlab_http_status(:ok) expect(project.merge_pipelines_enabled).to eq(project_params[:merge_pipelines_enabled]) expect(json_response['merge_pipelines_enabled']).to eq(project_params[:merge_pipelines_enabled]) end context 'when user does not have permission' do let(:developer_user) { create(:user) } before do project.add_developer(developer_user) end it 'does not update the content' do expect do put api("/projects/#{project.id}", developer_user), params: project_params end.not_to change { project.reload.merge_pipelines_enabled } expect(response).to have_gitlab_http_status(:forbidden) end end end context 'merge pipelines feature feature not available' do before do stub_licensed_features(merge_pipelines: false) end let(:project_params) { { merge_pipelines_enabled: true } } it 'does not update the content' do subject expect(response).to have_gitlab_http_status(:ok) expect(json_response).not_to have_key 'merge_pipelines_enabled' end end context 'when external_status_checks is available' do before do stub_licensed_features(external_status_checks: true) end let(:project_params) { { only_allow_merge_if_all_status_checks_passed: true } } it 'updates the content' do expect { subject }.to change { project.reload.only_allow_merge_if_all_status_checks_passed }.from(false).to(true) expect(response).to have_gitlab_http_status(:ok) expect(json_response['only_allow_merge_if_all_status_checks_passed']).to eq(project_params[:only_allow_merge_if_all_status_checks_passed]) end context 'when user does not have permission' do let(:developer_user) { create(:user) } before do project.add_developer(developer_user) end it 'does not update the content' do expect do put api("/projects/#{project.id}", developer_user), params: project_params end.not_to change { project.reload.only_allow_merge_if_all_status_checks_passed } expect(response).to have_gitlab_http_status(:forbidden) end end end context 'when approvals_before_merge is nil' do let(:project_params) { { approvals_before_merge: nil } } it_behaves_like '400 response' do let(:request) { subject } end end context 'when external_status_checks not available' do before do stub_licensed_features(external_status_checks: false) end let(:project_params) { { only_allow_merge_if_all_status_checks_passed: true } } it 'does not update the content' do expect { subject }.to not_change { project.reload.only_allow_merge_if_all_status_checks_passed } expect(response).to have_gitlab_http_status(:ok) expect(json_response).not_to have_key 'only_allow_merge_if_all_status_checks_passed' end end context 'merge trains feature is available' do before do stub_licensed_features(merge_pipelines: true, merge_trains: true) project.update!(merge_pipelines_enabled: true, merge_trains_enabled: false) end let(:project_params) { { merge_trains_enabled: true } } it 'updates the content' do expect { subject }.to change { project.reload.merge_trains_enabled } expect(response).to have_gitlab_http_status(:ok) expect(project.merge_trains_enabled).to eq(project_params[:merge_trains_enabled]) expect(json_response['merge_trains_enabled']).to eq(project_params[:merge_trains_enabled]) end context 'when user does not have permission' do let(:developer_user) { create(:user) } before do project.add_developer(developer_user) end it 'does not update the content' do expect do put api("/projects/#{project.id}", developer_user), params: project_params end.not_to change { project.reload.merge_trains_enabled } expect(response).to have_gitlab_http_status(:forbidden) end end end context 'merge trains feature feature not available' do before do stub_licensed_features(merge_trains: false) end let(:project_params) { { merge_trains_enabled: true } } it 'does not update the content' do expect { subject }.not_to change { project.merge_trains_enabled } expect(response).to have_gitlab_http_status(:ok) expect(json_response).not_to have_key 'merge_trains_enabled' end end context 'when setting ci_restrict_pipeline_cancellation_role' do let(:new_role) { 'maintainer' } let(:project_params) { { ci_restrict_pipeline_cancellation_role: new_role } } context 'when licence is available' do before do stub_licensed_features(ci_pipeline_cancellation_restrictions: true) end it 'updates the value' do expect { subject }.to change { project.reload.ci_cancellation_restriction.role } expect(response).to have_gitlab_http_status(:ok) expect(json_response['ci_restrict_pipeline_cancellation_role']).to eq new_role end end context 'when licence is not available' do it 'does not update the value' do expect { subject }.not_to change { project.reload.ci_cancellation_restriction.role } expect(response).to have_gitlab_http_status(:ok) expect(json_response).not_to have_key 'ci_restrict_pipeline_cancellation_role' end end end context 'when updating ci_id_token_sub_claim_components' do let(:project_params) { { ci_id_token_sub_claim_components: sub_claim_components } } let(:sub_claim_components) { ['project_path'] } it 'updates the project setting and returns its new value' do subject expect(response).to have_gitlab_http_status(:ok) expect(json_response['ci_id_token_sub_claim_components']).to eq(sub_claim_components) expect(project.reload.ci_id_token_sub_claim_components).to eq(sub_claim_components) end context 'when value is invalid' do let(:sub_claim_components) { ['invalid-component'] } it 'fails with errors' do subject expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['message']['ci_cd_settings.id_token_sub_claim_components']).to include( "invalid-component is not an allowed sub claim component", "project_path must be the first element of the sub claim") end end end context 'when updating external classification' do before do enable_external_authorization_service_check stub_licensed_features(external_authorization_service_api_management: true) end let(:project_params) { { external_authorization_classification_label: 'new label' } } it 'updates the classification label' do subject expect(response).to have_gitlab_http_status(:ok) expect(project.reload.external_authorization_classification_label).to eq('new label') end end context 'when updating mirror related attributes' do let(:import_url) { generate(:url) } let(:project_params) do { mirror: true, import_url: import_url, mirror_trigger_builds: true, only_mirror_protected_branches: true, mirror_overwrites_diverged_branches: true } end before do allow(Gitlab::GitalyClient::RemoteService).to receive(:exists?).with(import_url).and_return(true) end context 'when pull mirroring is not available' do before do stub_ee_application_setting(mirror_available: false) end it 'does not update mirror related attributes' do subject expect(response).to have_gitlab_http_status(:ok) expect(project.reload.mirror).to be false end it 'updates mirror related attributes when user is admin' do admin = create(:admin) unrelated_user = create(:user) project_params[:mirror_user_id] = unrelated_user.id project.add_maintainer(admin) expect_any_instance_of(EE::ProjectImportState).to receive(:force_import_job!).once put(api("/projects/#{project.id}", admin, admin_mode: true), params: project_params) expect(response).to have_gitlab_http_status(:ok) expect(project.reload).to have_attributes( mirror: true, import_url: import_url, mirror_user_id: unrelated_user.id, mirror_trigger_builds: true, only_mirror_protected_branches: true, mirror_overwrites_diverged_branches: true ) end end context 'when import_url is not a valid git endpoint' do it 'disallows creating a project with an import_url that is not reachable' do allow(Gitlab::GitalyClient::RemoteService).to receive(:exists?).with(import_url).and_return(false) subject expect(response).to have_gitlab_http_status(:unprocessable_entity) expect(json_response['message']).to eq('Unable to access repository with the URL and credentials provided') end end it 'updates mirror related attributes' do expect_any_instance_of(EE::ProjectImportState).to receive(:force_import_job!).once subject expect(response).to have_gitlab_http_status(:ok) expect(project.reload).to have_attributes( mirror: true, import_url: import_url, mirror_user_id: user.id, mirror_trigger_builds: true, only_mirror_protected_branches: true, mirror_overwrites_diverged_branches: true ) end context 'when only disabling pull mirror' do let(:project) { create(:project, mirror: true, import_url: import_url, mirror_user: user, namespace: user.namespace) } let(:project_params) do { mirror: false } end it 'updates mirror to false' do expect { subject }.to change { project.reload.mirror }.from(true).to(false) expect(response).to have_gitlab_http_status(:ok) end end it 'updates project without mirror attributes when the project is unable to set up repository mirroring' do stub_licensed_features(repository_mirrors: false) subject expect(response).to have_gitlab_http_status(:ok) expect(project.reload.mirror).to be false end it 'renders an API error when mirror user is invalid' do invalid_mirror_user = create(:user) project.add_developer(invalid_mirror_user) project_params[:mirror_user_id] = invalid_mirror_user.id subject expect(response).to have_gitlab_http_status(:bad_request) expect(json_response["message"]["mirror_user_id"].first).to eq("is invalid") end it 'returns 403 when the user does not have access to mirror settings' do developer = create(:user) project.add_developer(developer) put(api("/projects/#{project.id}", developer), params: project_params) expect(response).to have_gitlab_http_status(:forbidden) end context 'with mirror_branch_regex and only_mirror_protected_branches' do let(:project_params) do { mirror: true, import_url: import_url, only_mirror_protected_branches: false, mirror_branch_regex: 'text' } end it 'fails' do subject expect(response).to have_gitlab_http_status(:bad_request) end end context 'with only_mirror_protected_branches' do context 'when enabling only_mirror_protected_branches' do let(:project_params) do { mirror: true, import_url: import_url, only_mirror_protected_branches: true } end before do project.update!(mirror_branch_regex: 'text') end it 'removes mirror_branch_regex' do expect_any_instance_of(EE::ProjectImportState).to receive(:force_import_job!).once subject expect(response).to have_gitlab_http_status(:ok) expect(project.reload).to have_attributes( only_mirror_protected_branches: true, mirror_branch_regex: nil ) end end context 'when disabling only_mirror_protected_branches' do let(:project_params) do { mirror: true, import_url: import_url, only_mirror_protected_branches: false } end before do project.update!(mirror_branch_regex: 'text') end it 'keeps mirror_branch_regex' do expect_any_instance_of(EE::ProjectImportState).to receive(:force_import_job!).once subject expect(response).to have_gitlab_http_status(:ok) expect(project.reload).to have_attributes( only_mirror_protected_branches: false, mirror_branch_regex: 'text' ) end end end context 'when removing mirror_branch_regex' do let(:project_params) do { mirror: true, import_url: import_url, mirror_branch_regex: nil } end context 'with mirror_branch_regex present' do before do project.update!(mirror_branch_regex: 'text') end it 'removes mirror_branch_regex' do expect_any_instance_of(EE::ProjectImportState).to receive(:force_import_job!).once subject expect(response).to have_gitlab_http_status(:ok) expect(project.reload.mirror_branch_regex).to be_nil end end context 'with mirror_branch_regex nil and only_mirror_protected_branches is truthy' do before do project.update!(mirror_branch_regex: nil, only_mirror_protected_branches: true) end it 'does not change only_mirror_protected_branches value' do expect_any_instance_of(EE::ProjectImportState).to receive(:force_import_job!).once subject expect(response).to have_gitlab_http_status(:ok) expect(project.reload.mirror_branch_regex).to be_nil expect(project.reload.only_mirror_protected_branches).to be_truthy end end context 'with mirror_branch_regex nil and only_mirror_protected_branches is false' do before do project.update!(mirror_branch_regex: nil, only_mirror_protected_branches: false) end it 'does not change only_mirror_protected_branches value' do expect_any_instance_of(EE::ProjectImportState).to receive(:force_import_job!).once subject expect(response).to have_gitlab_http_status(:ok) expect(project.reload.mirror_branch_regex).to be_nil expect(project.reload.only_mirror_protected_branches).to be_falsey end end end context 'with mirror_branch_regex' do let(:project_params) do { mirror: true, import_url: import_url, mirror_branch_regex: 'text' } end before do project.update!(only_mirror_protected_branches: true) end it 'succeeds' do expect_any_instance_of(EE::ProjectImportState).to receive(:force_import_job!).once subject expect(response).to have_gitlab_http_status(:ok) expect(project.reload).to have_attributes( only_mirror_protected_branches: false, mirror_branch_regex: 'text' ) end end end describe 'updating approvals_before_merge attribute' do context 'when authenticated as project owner' do let(:project_params) { { approvals_before_merge: 3 } } it 'updates approvals_before_merge' do subject expect(response).to have_gitlab_http_status(:ok) expect(json_response['approvals_before_merge']).to eq(3) end end end context 'when protected_environments is available' do before do stub_licensed_features(protected_environments: true) end let(:project_params) { { allow_pipeline_trigger_approve_deployment: true } } it 'updates the content' do expect { subject }.to change { project.reload.allow_pipeline_trigger_approve_deployment }.from(false).to(true) expect(response).to have_gitlab_http_status(:ok) expect(json_response['allow_pipeline_trigger_approve_deployment']).to eq(project_params[:allow_pipeline_trigger_approve_deployment]) end end context 'when protected_environments not available' do before do stub_licensed_features(protected_environments: false) end let(:project_params) { { allow_pipeline_trigger_approve_deployment: true } } it 'does not update the content' do expect { subject }.to not_change { project.reload.allow_pipeline_trigger_approve_deployment } expect(response).to have_gitlab_http_status(:ok) expect(json_response).not_to have_key 'allow_pipeline_trigger_approve_deployment' end end context 'when jira_issue_association_enforcement is available' do before do stub_licensed_features(jira_issue_association_enforcement: true) end let(:project_params) { { prevent_merge_without_jira_issue: true } } it 'updates prevent_merge_without_jira_issue' do expect { subject }.to change { project.reload.prevent_merge_without_jira_issue }.from(false).to(true) expect(response).to have_gitlab_http_status(:ok) expect(json_response['prevent_merge_without_jira_issue']).to eq(project_params[:prevent_merge_without_jira_issue]) end end context 'when jira_issue_association_enforcement is not available' do before do stub_licensed_features(jira_issue_association_enforcement: false) end let(:project_params) { { prevent_merge_without_jira_issue: true } } it 'does not update prevent_merge_without_jira_issue' do expect { subject }.to not_change { project.reload.prevent_merge_without_jira_issue } expect(response).to have_gitlab_http_status(:ok) expect(json_response['prevent_merge_without_jira_issue']).to eq(nil) end end context 'when setting auto_duo_code_review_enabled' do let(:project_params) { { auto_duo_code_review_enabled: true } } context 'when licence is available' do before do stub_licensed_features(review_merge_request: true) end it 'updates the value' do expect { subject }.to change { project.reload.auto_duo_code_review_enabled } expect(response).to have_gitlab_http_status(:ok) expect(json_response['auto_duo_code_review_enabled']).to eq true end end context 'when licence is not available' do it 'does not update the value' do expect { subject }.not_to change { project.reload.auto_duo_code_review_enabled } expect(response).to have_gitlab_http_status(:ok) expect(json_response['auto_duo_code_review_enabled']).to eq nil end end end end describe 'DELETE /projects/:id' do let(:group) { create(:group, owners: user) } let(:project) { create(:project, group: group) } let(:params) { {} } context 'when attempting to delete security policy project' do before do stub_licensed_features(security_orchestration_policies: true) create(:security_orchestration_policy_configuration, security_policy_management_project: project) end it 'returns error' do delete api("/projects/#{project.id}", user) expect(response).to have_gitlab_http_status(:bad_request) expect(json_response["message"]).to eq('Project cannot be deleted because it is linked as a security policy project') end end end describe 'POST /projects/:id/fork' do subject(:fork_call) { post api("/projects/#{group_project.id}/fork", user), params: { namespace: target_namespace.id } } let!(:target_namespace) do create(:group, owners: user) end let!(:group_project) { create(:project, namespace: group) } let(:group) { create(:group) } before do group.add_reporter(user) end context 'when project namespace has prohibit_outer_forks enabled' do let(:group) do create(:saml_provider, :enforced_group_managed_accounts, prohibited_outer_forks: true).group end let(:user) do create(:user, managing_group: group).tap do |u| create(:group_saml_identity, user: u, saml_provider: group.saml_provider) end end before do stub_licensed_features(group_saml: true, group_forking_protection: true) end context 'and target namespace is outer' do it 'renders 404' do expect { fork_call }.not_to change { ::Project.count } expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq "404 Target Namespace Not Found" end end context 'and target namespace is inner to project namespace' do let!(:target_namespace) { create(:group, parent: group) } it 'forks the project' do target_namespace.add_owner(user) expect { fork_call }.to change { ::Project.count }.by(1) end end end end describe 'POST /projects/:id/fork/:forked_from_id' do let_it_be_with_reload(:source_group) { create(:group) } let_it_be_with_reload(:project_fork_source) { create(:project, :public, namespace: source_group) } let(:path) { "/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}" } before do target_namespace.add_developer(user) project_fork_target.add_owner(user) stub_licensed_features(group_forking_protection: true) end shared_examples 'forks the project' do it 'forks the project' do post api(path, user) expect(response).to have_gitlab_http_status(:created) expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id) expect(project_fork_target.fork_network_member).to be_present end end shared_context 'same namespace' do let_it_be_with_reload(:target_namespace) do create(:group, parent: source_group, project_creation_level: ::Gitlab::Access::DEVELOPER_PROJECT_ACCESS) end let_it_be_with_reload(:project_fork_target) { create(:project, :public, namespace: target_namespace) } end shared_context 'different namespace' do let_it_be_with_reload(:target_namespace) do create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_PROJECT_ACCESS) end let_it_be_with_reload(:project_fork_target) { create(:project, :public, namespace: target_namespace) } end context 'when project namespace has prevent_forking_outside_group enabled' do before do source_group.namespace_settings.update!(prevent_forking_outside_group: true) end context 'and target namespace is inside the source group' do include_context 'same namespace' it_behaves_like 'forks the project' end context "and target namespace is outside the source group" do include_context 'different namespace' it 'renders 404' do post api(path, user) expect(response).to have_gitlab_http_status(:unauthorized) expect(json_response['message']).to eq "401 Unauthorized - Target Namespace" end end end context 'when project namespace has prevent_forking_outside_group disabled' do before do source_group.namespace_settings.update!(prevent_forking_outside_group: false) end context 'and target namespace is inside the source group' do include_context 'same namespace' it_behaves_like 'forks the project' end context 'and target namespace is outside the source group' do include_context 'different namespace' it_behaves_like 'forks the project' end end end describe 'POST /projects/:id/import_project_members/:project_id' do let_it_be(:group) { create(:group) } let_it_be(:project) { create(:project) } let_it_be(:target_project) { create(:project, group: group) } before_all do project.add_maintainer(another_user) target_project.add_maintainer(another_user) end context 'when the target project has locked their membership' do context 'via the parent group' do before do group.update!(membership_lock: true) end it 'returns 403' do expect do post api("/projects/#{target_project.id}/import_project_members/#{project.id}", another_user) end.not_to change { target_project.members.count } expect(response).to have_gitlab_http_status(:unprocessable_entity) expect(json_response['message']).to eq('Forbidden') expect(json_response['reason']).to eq('import_project_team_forbidden_error') end end context 'via LDAP' do before do stub_application_setting(lock_memberships_to_ldap: true) end it 'returns 403' do expect do post api("/projects/#{target_project.id}/import_project_members/#{project.id}", another_user) end.not_to change { target_project.members.count } expect(response).to have_gitlab_http_status(:unprocessable_entity) expect(json_response['message']).to eq('Forbidden') expect(json_response['reason']).to eq('import_project_team_forbidden_error') end end end context 'block seat overages', :saas do let_it_be(:subscription) { create(:gitlab_subscription, :premium, namespace: group, seats: 1) } context 'when block seat overages is enabled' do before do group.namespace_settings.update!(seat_control: :block_overages) end it 'rejects adding more members than there are available seats' do post api("/projects/#{target_project.id}/import_project_members/#{project.id}", another_user) expect(response).to have_gitlab_http_status(:unprocessable_entity) expect(json_response).to eq({ 'message' => 'There are not enough available seats to invite this many users. ' \ 'Ask a user with the Owner role to purchase more seats.', 'reason' => 'seat_limit_exceeded_error' }) end end context 'when block seat overages is disabled' do before do group.namespace_settings.update!(seat_control: :off) end it 'accepts adding more members than there are available seats' do post api("/projects/#{target_project.id}/import_project_members/#{project.id}", another_user) expect(response).to have_gitlab_http_status(:created) expect(json_response).to eq({ 'status' => 'success' }) end end end end describe 'POST /projects/:id/share', :saas do let_it_be(:group) { create(:group, :private) } let_it_be(:project) { create(:project) } let_it_be(:member_role) { create(:member_role, namespace: project.namespace, base_access_level: Gitlab::Access::DEVELOPER) } let(:path) { "/projects/#{project.id}/share" } let(:params) do { group_id: group.id, group_access: member_role.base_access_level, member_role_id: member_role.id } end subject(:request) { post api(path, user), params: params } before do group.add_developer(user) project.add_maintainer(user) end context 'feature is available' do before do stub_licensed_features(custom_roles: true) end it 'shares the project with the group with member role' do expect { request }.to change { ProjectGroupLink.count }.by(1) expect(response).to have_gitlab_http_status(:created) expect(json_response['member_role_id']).to eq(member_role.id) end context 'but assign_custom_roles_to_project_links_saas feature flag is disabled' do before do stub_feature_flags(assign_custom_roles_to_project_links_saas: false) end it 'shares the project with the group without member role' do expect { request }.to change { ProjectGroupLink.count }.by(1) expect(response).to have_gitlab_http_status(:created) expect(json_response['member_role_id']).to be_nil end end end context 'feature is not available' do before do stub_licensed_features(custom_roles: false) end it 'shares the project with the group without member role' do expect { request }.to change { ProjectGroupLink.count }.by(1) expect(response).to have_gitlab_http_status(:created) expect(json_response['member_role_id']).to be_nil end end end end